[dart2wasm] Explicitly model functions that do not return

Similar to [0] which recognizes functions that return always nulls, we
recognize functions that never return and

* make the wasm function have no outputs
* make call sites emit `unreachable()` after the call (to inform
  binaryen & wasm runtime that this is unreachable)

Before we had an artificial construct where we made such functions
have a `w.RefType(HeapType.none, nullable: false)` return type (i.e.
bottom, i.e. no values) and encoded that way it's unreachable.

We also change some exported wasm functions to explicitly use wasm
types in the signature (namely `WasmVoid` instead of Dart `void`).

[0] https://dart-review.googlesource.com/c/sdk/+/497620

Change-Id: I3724e777cda23c0cf2c8a7dd2e473f3fef0a4f54
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/499240
Commit-Queue: Martin Kustermann <kustermann@google.com>
Reviewed-by: Srujan Gaddam <srujzs@google.com>
This commit is contained in:
Martin Kustermann
2026-05-13 00:23:45 -07:00
committed by dart-scoped@luci-project-accounts.iam.gserviceaccount.com
parent d0e0290702
commit 147ed24717
9 changed files with 130 additions and 21 deletions
+11
View File
@@ -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);
+15 -5
View File
@@ -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],
+36 -5
View File
@@ -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<w.FunctionType, Reference> {
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<w.FunctionType, Reference> {
target,
null,
synthesizeNullReturnValue: synthesizeNullReturnValue,
synthesizeNoReturn: synthesizeNoReturn,
);
}
assert(
@@ -465,6 +491,7 @@ class _FunctionTypeGenerator extends MemberVisitor1<w.FunctionType, Reference> {
target,
translator.translateType(receiverType),
synthesizeNullReturnValue: synthesizeNullReturnValue,
synthesizeNoReturn: synthesizeNoReturn,
);
}
@@ -477,6 +504,7 @@ class _FunctionTypeGenerator extends MemberVisitor1<w.FunctionType, Reference> {
target,
null,
synthesizeNullReturnValue: synthesizeNullReturnValue,
synthesizeNoReturn: synthesizeNoReturn,
);
}
@@ -502,6 +530,7 @@ class _FunctionTypeGenerator extends MemberVisitor1<w.FunctionType, Reference> {
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<w.ValueType> outputs;
final hasNoReturnValue = target.isSetter || synthesizeNullReturnValue;
final hasNoReturnValue = synthesizeNullReturnValue || synthesizeNoReturn;
if (hasNoReturnValue) {
outputs = const [];
} else {
+17 -7
View File
@@ -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
@@ -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();
}
@@ -0,0 +1,27 @@
(module $module0
(type $#Top <...>)
(type $JSExternWrapper <...>)
(type $Object <...>)
(global $"\"fooAlwaysThrows\"" (ref $JSExternWrapper) <...>)
(global $"\"foo\"" (ref $JSExternWrapper) <...>)
(func $Error._throwWithCurrentStackTrace <noInline> (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 <noInline>"
unreachable
)
(func $print (param $object (ref null $#Top)) <...>)
)
@@ -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
@@ -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
@@ -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