diff --git a/pkg/dart2wasm/lib/code_generator.dart b/pkg/dart2wasm/lib/code_generator.dart index ca0e6a38d3c..7b374f38cd6 100644 --- a/pkg/dart2wasm/lib/code_generator.dart +++ b/pkg/dart2wasm/lib/code_generator.dart @@ -2077,6 +2077,11 @@ abstract class AstCodeGenerator b.ref_null(w.HeapType.none); return w.RefType(w.HeapType.none, nullable: true); } + if (selector.synthesizeNoReturn) { + assert(selector.signature.outputs.isEmpty); + b.unreachable(); + return voidMarker; + } return translator.outputOrVoid(signature.outputs); } @@ -6099,6 +6104,9 @@ abstract class CallTarget { /// Whether callers should synthesize a `null` return value. bool get synthesizeNullReturnValue => false; + /// Whether callee never returns and callers should emit `unreachable`. + bool get synthesizeNoReturn => false; + /// Whether this call target supports inlining. bool get supportsInlining => false; @@ -6133,6 +6141,9 @@ class AstCallTarget extends CallTarget { bool get synthesizeNullReturnValue => _translator.synthesizeNullReturnValue(_reference); + @override + bool get synthesizeNoReturn => _translator.synthesizeNoReturn(_reference); + @override String get name => _translator.functions.getFunctionName(_reference); diff --git a/pkg/dart2wasm/lib/dispatch_table.dart b/pkg/dart2wasm/lib/dispatch_table.dart index aa722bb9ff3..b80820b5160 100644 --- a/pkg/dart2wasm/lib/dispatch_table.dart +++ b/pkg/dart2wasm/lib/dispatch_table.dart @@ -67,6 +67,12 @@ class SelectorInfo { /// Will be set during `_computeSignature`. late final bool synthesizeNullReturnValue; + /// Whether the call will never return and callers can emit an + /// `unreachable()` after the call. + /// + /// Will be set during `_computeSignature`. + late final bool synthesizeNoReturn; + /// The selector's member's name. final String name; @@ -210,14 +216,18 @@ class SelectorInfo { outputSets.length, (i) => _upperBound(outputSets[i], ensureBoxed: false), ); - if (outputs case [w.RefType(heapType: w.HeapType.none, nullable: true)]) { - // All functions are guaranteed to return null. - // Will prune the signature and make call sites synthesize `null` if - // needed. + if (outputs case [ + w.RefType(heapType: w.HeapType.none, nullable: final nullable), + ]) { + // All functions are guaranteed to return null or are unreachable. + // => Prune signature to not return anything + // => Tell callers to synthesize `null` or emit `unreachable`. outputs.clear(); - synthesizeNullReturnValue = true; + synthesizeNullReturnValue = nullable; + synthesizeNoReturn = !nullable; } else { synthesizeNullReturnValue = isSetterOrIndexSetter; + synthesizeNoReturn = false; } return translator.typesBuilder.defineFunction([ inputs[0], diff --git a/pkg/dart2wasm/lib/functions.dart b/pkg/dart2wasm/lib/functions.dart index e3017283b88..881e8f8d9bf 100644 --- a/pkg/dart2wasm/lib/functions.dart +++ b/pkg/dart2wasm/lib/functions.dart @@ -107,6 +107,7 @@ class FunctionCollector { null, isImportOrExport: true, synthesizeNullReturnValue: false, + synthesizeNoReturn: false, ); return _functions[member.reference] = translator @@ -141,6 +142,7 @@ class FunctionCollector { null, isImportOrExport: true, synthesizeNullReturnValue: false, + synthesizeNoReturn: false, ) : translator.signatureForDirectCall(target); @@ -253,6 +255,7 @@ class FunctionCollector { w.FunctionType _getFunctionType(Reference target) { final Member member = target.asMember; final synthesizeNullReturnValue = this.synthesizeNullReturnValue(target); + final synthesizeNoReturn = this.synthesizeNoReturn(target); if (target.isBodyReference) { // This is the function body that is always called directly (never via @@ -262,11 +265,16 @@ class FunctionCollector { translator, member, synthesizeNullReturnValue, + synthesizeNoReturn, ); } return member.accept1( - _FunctionTypeGenerator(translator, synthesizeNullReturnValue), + _FunctionTypeGenerator( + translator, + synthesizeNullReturnValue, + synthesizeNoReturn, + ), target, ); } @@ -277,13 +285,25 @@ class FunctionCollector { if (member.name == indexSetName) return true; final returnType = translator.typeOfReturnValue(member); - final wasmType = translator.translateType(returnType); + final wasmType = translator.translateReturnType(returnType); if (wasmType case w.RefType(heapType: w.HeapType.none, nullable: true)) { return true; } return false; } + bool synthesizeNoReturn(Reference target) { + final member = target.asMember; + if (member is! Procedure) return false; + + final returnType = translator.typeOfReturnValue(member); + final wasmType = translator.translateReturnType(returnType); + if (wasmType case w.RefType(heapType: w.HeapType.none, nullable: false)) { + return true; + } + return false; + } + String getFunctionName(Reference target) { final Member member = target.asMember; String memberName = member.toString(); @@ -433,8 +453,13 @@ class FunctionCollector { class _FunctionTypeGenerator extends MemberVisitor1 { final Translator translator; final bool synthesizeNullReturnValue; + final bool synthesizeNoReturn; - _FunctionTypeGenerator(this.translator, this.synthesizeNullReturnValue); + _FunctionTypeGenerator( + this.translator, + this.synthesizeNullReturnValue, + this.synthesizeNoReturn, + ); @override w.FunctionType visitField(Field node, Reference target) { @@ -445,6 +470,7 @@ class _FunctionTypeGenerator extends MemberVisitor1 { target, null, synthesizeNullReturnValue: synthesizeNullReturnValue, + synthesizeNoReturn: synthesizeNoReturn, ); } assert( @@ -465,6 +491,7 @@ class _FunctionTypeGenerator extends MemberVisitor1 { target, translator.translateType(receiverType), synthesizeNullReturnValue: synthesizeNullReturnValue, + synthesizeNoReturn: synthesizeNoReturn, ); } @@ -477,6 +504,7 @@ class _FunctionTypeGenerator extends MemberVisitor1 { target, null, synthesizeNullReturnValue: synthesizeNullReturnValue, + synthesizeNoReturn: synthesizeNoReturn, ); } @@ -502,6 +530,7 @@ class _FunctionTypeGenerator extends MemberVisitor1 { target, receiverType, synthesizeNullReturnValue: synthesizeNullReturnValue, + synthesizeNoReturn: synthesizeNoReturn, ); } @@ -738,6 +767,7 @@ w.FunctionType makeFunctionTypeForBody( Translator translator, Member member, bool synthesizeNullReturnValue, + bool synthesizeNoReturn, ) { assert(member.isInstanceMember); assert(member is Procedure); @@ -758,7 +788,7 @@ w.FunctionType makeFunctionTypeForBody( translator.translateType(translator.typeOfCheckedParameterVariable(p)), ]; - final hasNoReturnValue = synthesizeNullReturnValue; + final hasNoReturnValue = synthesizeNullReturnValue || synthesizeNoReturn; final outputs = [ if (!hasNoReturnValue) translator.translateReturnType(translator.typeOfReturnValue(member)), @@ -841,6 +871,7 @@ w.FunctionType _makeFunctionType( Reference target, w.ValueType? receiverType, { required bool synthesizeNullReturnValue, + required bool synthesizeNoReturn, bool isImportOrExport = false, }) { Member member = target.asMember; @@ -881,7 +912,7 @@ w.FunctionType _makeFunctionType( (t is InterfaceType && t.classNode == translator.wasmVoidClass); final List outputs; - final hasNoReturnValue = target.isSetter || synthesizeNullReturnValue; + final hasNoReturnValue = synthesizeNullReturnValue || synthesizeNoReturn; if (hasNoReturnValue) { outputs = const []; } else { diff --git a/pkg/dart2wasm/lib/translator.dart b/pkg/dart2wasm/lib/translator.dart index 16018cca2a8..fde0fb174fe 100644 --- a/pkg/dart2wasm/lib/translator.dart +++ b/pkg/dart2wasm/lib/translator.dart @@ -783,6 +783,11 @@ class Translator with KernelNodes { b.ref_null(w.HeapType.none); return [w.RefType(w.HeapType.none, nullable: true)]; } + if (callTarget.synthesizeNoReturn) { + assert(outputs.isEmpty); + b.unreachable(); + return const []; + } return outputs; } @@ -1782,16 +1787,24 @@ class Translator with KernelNodes { final table = dispatchTable; final selector = table.selectorForTarget(target); if (selector.containsTarget(target)) { - assert( - !selector.synthesizeNullReturnValue || - selector.signature.outputs.isEmpty, - ); return selector.synthesizeNullReturnValue; } } return functions.synthesizeNullReturnValue(target); } + bool synthesizeNoReturn(Reference target) { + final member = target.asMember; + if (member.isInstanceMember) { + final table = dispatchTable; + final selector = table.selectorForTarget(target); + if (selector.containsTarget(target)) { + return selector.synthesizeNoReturn; + } + } + return functions.synthesizeNoReturn(target); + } + ParameterInfo paramInfoForDirectCall(Reference target) { if (target.asMember.isInstanceMember) { final selector = dispatchTable.selectorForTarget(target); @@ -2920,9 +2933,6 @@ class _ClosureDynamicEntryGenerator implements CodeGenerator { outputs.single, translator.outputOrVoid(function.type.outputs), ); - } else if (function.type.outputs.isNotEmpty) { - assert(target.synthesizeNullReturnValue); - b.ref_null(w.HeapType.none); } b.end(); // end function diff --git a/pkg/dart2wasm/test/ir_tests/always_throws.dart b/pkg/dart2wasm/test/ir_tests/always_throws.dart new file mode 100644 index 00000000000..6cd207e8c3e --- /dev/null +++ b/pkg/dart2wasm/test/ir_tests/always_throws.dart @@ -0,0 +1,22 @@ +// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// functionFilter=foo +// typeFilter=NoMatch +// globalFilter=NoMatch +// compilerOption=-O0 + +void main() { + foo(); +} + +void foo() { + print('foo'); + print(fooAlwaysThrows()); +} + +Never fooAlwaysThrows() { + print('fooAlwaysThrows'); + throw Object(); +} diff --git a/pkg/dart2wasm/test/ir_tests/always_throws.wat b/pkg/dart2wasm/test/ir_tests/always_throws.wat new file mode 100644 index 00000000000..af8ea7e5fe3 --- /dev/null +++ b/pkg/dart2wasm/test/ir_tests/always_throws.wat @@ -0,0 +1,27 @@ +(module $module0 + (type $#Top <...>) + (type $JSExternWrapper <...>) + (type $Object <...>) + (global $"\"fooAlwaysThrows\"" (ref $JSExternWrapper) <...>) + (global $"\"foo\"" (ref $JSExternWrapper) <...>) + (func $Error._throwWithCurrentStackTrace (param $object (ref $#Top)) <...>) + (func $Object (result (ref $Object)) <...>) + (func $foo (result (ref null $#Top)) + global.get $"\"foo\"" + call $print + ref.null none + drop + call $fooAlwaysThrows + unreachable + ) + (func $fooAlwaysThrows + global.get $"\"fooAlwaysThrows\"" + call $print + ref.null none + drop + call $Object + call $"Error._throwWithCurrentStackTrace " + unreachable + ) + (func $print (param $object (ref null $#Top)) <...>) +) \ No newline at end of file diff --git a/pkg/dart2wasm/test/ir_tests/deferred.fine_grained_module3.wat b/pkg/dart2wasm/test/ir_tests/deferred.fine_grained_module3.wat index d6da3596654..bbcdb7f9165 100644 --- a/pkg/dart2wasm/test/ir_tests/deferred.fine_grained_module3.wat +++ b/pkg/dart2wasm/test/ir_tests/deferred.fine_grained_module3.wat @@ -211,6 +211,7 @@ global.get $"\"[]\"" i32.const 21 call_indirect $module0.cross-module-funcs-0 (param i64 i64 (ref null $JSExternWrapper)) + unreachable end local.get $var1 struct.get $WasmListBase $_data diff --git a/pkg/dart2wasm/test/ir_tests/deferred.type_checks_module1.wat b/pkg/dart2wasm/test/ir_tests/deferred.type_checks_module1.wat index 350fc250f18..30d270a8eb2 100644 --- a/pkg/dart2wasm/test/ir_tests/deferred.type_checks_module1.wat +++ b/pkg/dart2wasm/test/ir_tests/deferred.type_checks_module1.wat @@ -8,7 +8,6 @@ (type $_InterfaceType <...>) (type $_Type <...>) (global $"\")\"_11" (import "$" "2") (ref $JSExternWrapper)) - (global $"\"Attempt to execute code remove<...>\"" (import "$" "(") (ref $JSExternWrapper)) (global $_InterfaceType (import "$" "0") (ref $_InterfaceType)) (table $$.% (import "$" "%") 742 funcref) (table $$.' (import "$" "'") 20 funcref) @@ -158,9 +157,6 @@ if i32.const 2 call_indirect $$.' - global.get $"\"Attempt to execute code remove<...>\"" - i32.const 3 - call_indirect $$.' (param (ref $#Top)) unreachable end local.get $var0 diff --git a/pkg/dart2wasm/test/ir_tests/dispatch_table_reuse_module1.wat b/pkg/dart2wasm/test/ir_tests/dispatch_table_reuse_module1.wat index aa1accc0a85..1366de94893 100644 --- a/pkg/dart2wasm/test/ir_tests/dispatch_table_reuse_module1.wat +++ b/pkg/dart2wasm/test/ir_tests/dispatch_table_reuse_module1.wat @@ -77,6 +77,7 @@ local.get $var3 i32.const 5 call_indirect $module0.cross-module-funcs-0 (param (ref null $#Top) (ref $_Type)) + unreachable end local.get $var1 ref.cast $Object