[dart2wasm] Allow marking functions as pure functions

If a function doesn't have an effect we can now mark it via
`@pragma('wasm:pure-function')`. We'll then emit this as metadata
in the `binaryen.remove.if.unused` custom section.

This allows `wasm-opt` to remove calls to such functions if the result
of the call isn't used.

For now we mark a few string functions as pure.

Closes https://github.com/dart-lang/sdk/issues/62665

Change-Id: I8d38fb5894fd98248dc4d648d99c8cdcddc271a5
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/481802
Commit-Queue: Martin Kustermann <kustermann@google.com>
Reviewed-by: Nate Biggs <natebiggs@google.com>
This commit is contained in:
Martin Kustermann
2026-02-23 01:39:37 -08:00
committed by Commit Queue
parent 18816329f6
commit f99f5b1a2b
11 changed files with 167 additions and 14 deletions
+10 -3
View File
@@ -52,13 +52,16 @@ class FunctionCollector {
void _importOrExport(Procedure member) {
final importName = util.getWasmImportPragma(translator.coreTypes, member);
if (importName != null) {
final isPure =
util.hasWasmPureFunctionPragma(translator.coreTypes, member);
final ftype = _makeFunctionType(translator, member.reference, null,
isImportOrExport: true);
_functions[member.reference] = translator
.moduleForReference(member.reference)
.functions
.import(importName.moduleName, importName.itemName, ftype,
"$importName (import)");
"$importName (import)")
..isPure = isPure;
}
// Ensure any procedures marked as exported are enqueued.
@@ -98,6 +101,8 @@ class FunctionCollector {
w.BaseFunction getFunction(Reference target) {
return _functions.putIfAbsent(target, () {
final member = target.asMember;
final isPure =
util.hasWasmPureFunctionPragma(translator.coreTypes, member);
// If this function is a `@pragma('wasm:import', '<module>.<name>')` we
// import the function and return it.
@@ -112,7 +117,8 @@ class FunctionCollector {
.moduleForReference(member.reference)
.functions
.import(importName.moduleName, importName.itemName, ftype,
"$importName (import)");
"$importName (import)")
..isPure = isPure;
}
}
@@ -136,7 +142,8 @@ class FunctionCollector {
? _makeFunctionType(translator, target, null, isImportOrExport: true)
: translator.signatureForDirectCall(target);
final function = module.functions.define(ftype, getFunctionName(target));
final function = module.functions.define(ftype, getFunctionName(target))
..isPure = isPure;
if (exportName != null) module.exports.export(exportName, function);
// Export the function from the main module if it is callable from
+2 -1
View File
@@ -51,7 +51,8 @@ class Globals {
final getter = _globalGetters.putIfAbsent(global, () {
final getterType =
owningModule.types.defineFunction(const [], [global.type.type]);
final getterFunction = owningModule.functions.define(getterType);
final getterFunction = owningModule.functions.define(getterType)
..isPure = true;
final getterBody = getterFunction.body;
getterBody.global_get(global);
getterBody.end();
+2 -1
View File
@@ -780,7 +780,8 @@ class IsCheckerCallTarget extends CallTarget {
signature.inputs,
signature.outputs,
),
name);
name)
..isPure = true;
translator.compilationQueue.add(CompilationTask(function, inliningCodeGen));
return function;
})();
+6
View File
@@ -108,6 +108,12 @@ String? getWasmWeakExportPragma(CoreTypes coreTypes, Member member) {
defaultValue: member.name.text);
}
bool hasWasmPureFunctionPragma(CoreTypes coreTypes, Member member) {
return getPragma<bool>(coreTypes, member, 'wasm:pure-function',
defaultValue: true) ==
true;
}
/// Add a `@pragma('wasm:entry-point')` annotation to an annotatable.
T addWasmEntryPointPragma<T extends Annotatable>(T node, CoreTypes coreTypes) =>
addPragma(node, 'wasm:entry-point', coreTypes);
@@ -0,0 +1,27 @@
// 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=runApp|foo
// tableFilter=NoMatch
// globalFilter=NoMatch
// typeFilter=NoMatch
// compilerOption=-O2
void main() => runApp();
@pragma('wasm:never-inline')
void runApp() {
foo('1');
foo('2');
print(foo('3'));
print(foo('4'));
}
@pragma('wasm:never-inline')
@pragma('wasm:pure-function')
dynamic foo(String arg) {
'foo($arg)'.length;
'bar($arg)'.length;
return arg.length;
}
@@ -0,0 +1,41 @@
(module $$
(type $#Top <...>)
(type $BoxedInt <...>)
(type $JSStringImpl <...>)
(@binaryen.removable.if.unused)
(func $"wasm:js-string.length (import)" (import "wasm:js-string" "length") (param externref) (result i32))
(global $"\")\"" (ref $JSStringImpl) <...>)
(global $"\"3\"" (ref $JSStringImpl) <...>)
(global $"\"4\"" (ref $JSStringImpl) <...>)
(global $"\"bar(\"" (ref $JSStringImpl) <...>)
(global $"\"foo(\"" (ref $JSStringImpl) <...>)
(@binaryen.removable.if.unused)
(func $"foo <noInline>" (param $var0 (ref $JSStringImpl)) (result (ref $BoxedInt))
global.get $"\"foo(\""
local.get $var0
global.get $"\")\""
call $JSStringImpl._interpolate3
drop
global.get $"\"bar(\""
local.get $var0
global.get $"\")\""
call $JSStringImpl._interpolate3
drop
i32.const 68
local.get $var0
struct.get $JSStringImpl $_ref
call $"wasm:js-string.length (import)"
i64.extend_i32_u
struct.new $BoxedInt
)
(func $"runApp <noInline>"
global.get $"\"3\""
call $"foo <noInline>"
call $print
global.get $"\"4\""
call $"foo <noInline>"
call $print
)
(func $JSStringImpl._interpolate3 (param $var0 (ref $JSStringImpl)) (param $var1 (ref $#Top)) (param $var2 (ref $JSStringImpl)) (result (ref $JSStringImpl)) <...>)
(func $print (param $var0 (ref $#Top)) <...>)
)
@@ -31,7 +31,8 @@ class FunctionBuilder extends ir.BaseFunction
@override
ir.DefinedFunction forceBuild() => ir.DefinedFunction(
enclosingModule, body.build(), finalizableIndex, type, functionName);
enclosingModule, body.build(), finalizableIndex, type, functionName)
..isPure = isPure;
@override
String toString() => functionName ?? "#$finalizableIndex";
+12
View File
@@ -35,6 +35,12 @@ abstract class BaseFunction with Indexable, Exportable {
@override
final Module enclosingModule;
/// Whether this function is pure and has no effect.
///
/// If marked as spure, we'll emit metadata in the
/// `binaryen.removable.if.unused` custom section.
bool isPure = false;
BaseFunction(this.enclosingModule, this.finalizableIndex, this.type,
[this.functionName]);
@@ -93,6 +99,9 @@ class DefinedFunction extends BaseFunction implements Serializable {
}
void printTo(IrPrinter p) {
if (isPure) {
p.writeln('(@binaryen.removable.if.unused)');
}
p.write('(func ');
p.writeFunctionReference(this);
String? exportName;
@@ -158,6 +167,9 @@ class ImportedFunction extends BaseFunction implements Import {
}
void printTo(IrPrinter p) {
if (isPure) {
p.writeln('(@binaryen.removable.if.unused)');
}
p.write('(func ');
p.writeFunctionReference(this);
p.write(' ');
+5 -5
View File
@@ -112,12 +112,9 @@ class Module implements Serializable {
CodeSection(functions.defined, watchPoints).serialize(s);
DataSection(dataSegments.defined, watchPoints).serialize(s);
NameSection(
moduleName,
<BaseFunction>[...functions.imported, ...functions.defined],
types.recursionGroups,
<Global>[...globals.imported, ...globals.defined],
watchPoints)
moduleName, functions, types.recursionGroups, globals, watchPoints)
.serialize(s);
RemovableIfUnusedSection(functions).serialize(s);
SourceMapSection(sourceMapUrl).serialize(s);
}
@@ -220,6 +217,9 @@ class Module implements Serializable {
functions,
types,
globals);
RemovableIfUnusedSection.deserialize(
customSections[RemovableIfUnusedSection.customSectionName]?.single,
functions);
final sourceMapUrl = SourceMapSection.deserialize(
customSections[SourceMapSection.customSectionName]?.single);
@@ -734,9 +734,9 @@ class NameSection extends CustomSection {
static const String customSectionName = 'name';
final String? moduleName;
final List<ir.BaseFunction> functions;
final ir.Functions functions;
final List<List<ir.DefType>> types;
final List<ir.Global> globals;
final ir.Globals globals;
NameSection(
this.moduleName,
@@ -805,7 +805,8 @@ class NameSection extends CustomSection {
int functionsWithLocalNamesCount = 0;
final localNames = Serializer();
for (final function in functions) {
for (int i = 0; i < functions.length; i++) {
final function = functions[i];
if (function is ir.DefinedFunction) {
if (function.localNames.isNotEmpty) {
localNames.writeUnsigned(function.finalizableIndex.value);
@@ -978,3 +979,51 @@ class SourceMapSection extends CustomSection {
return Uri.parse(d.readName());
}
}
class RemovableIfUnusedSection extends CustomSection {
static const String customSectionName = 'binaryen.removable.if.unused';
final ir.Functions functions;
RemovableIfUnusedSection(this.functions) : super([]);
@override
void serializeContents(Serializer s) {
final functionsToAnnotate = [
...functions.imported.where((f) => f.isPure),
...functions.defined.where((f) => f.isPure),
];
if (functionsToAnnotate.isNotEmpty) {
s.writeName(customSectionName);
s.writeUnsigned(functionsToAnnotate.length);
for (final function in functionsToAnnotate) {
s.writeUnsigned(function.index);
s.writeUnsigned(1); // Number of hints
s.writeUnsigned(0); // Offset (0 == function-level)
s.writeUnsigned(0); // always 0
}
}
}
static void deserialize(Deserializer? d, ir.Functions functions) {
if (d == null) return;
final count = d.readUnsigned();
for (int i = 0; i < count; i++) {
final functionIndex = d.readUnsigned();
final numHints = d.readUnsigned();
for (int j = 0; j < numHints; j++) {
final offset = d.readUnsigned(); // Offset (0 == function-level)
if (offset != 0) {
throw UnsupportedError(
'Only function-level ($customSectionName) annotation supported.');
}
final data = d.readUnsigned(); // always 0
if (data != 0) {
throw StateError('Expected 0 but got $data');
}
functions[functionIndex].isPure = true;
}
}
}
}
@@ -1000,24 +1000,30 @@ int _jsCompare(WasmExternRef? s1, WasmExternRef? s2) =>
external WasmI32 _jsStringCharCodeAtImport(WasmExternRef? s, WasmI32 index);
@pragma("wasm:import", "wasm:js-string.compare")
@pragma("wasm:pure-function")
external WasmI32 _jsStringCompareImport(WasmExternRef? s1, WasmExternRef? s2);
@pragma("wasm:import", "wasm:js-string.concat")
@pragma("wasm:pure-function")
external WasmExternRef _jsStringConcatImport(
WasmExternRef? s1,
WasmExternRef? s2,
);
@pragma("wasm:import", "wasm:js-string.equals")
@pragma("wasm:pure-function")
external WasmI32 _jsStringEqualsImport(WasmExternRef? s1, WasmExternRef? s2);
@pragma("wasm:import", "wasm:js-string.fromCharCode")
@pragma("wasm:pure-function")
external WasmExternRef _jsStringFromCharCodeImport(WasmI32 c);
@pragma("wasm:import", "wasm:js-string.length")
@pragma("wasm:pure-function")
external WasmI32 _jsStringLengthImport(WasmExternRef? s);
@pragma("wasm:import", "wasm:js-string.substring")
@pragma("wasm:pure-function")
external WasmExternRef _jsStringSubstringImport(
WasmExternRef? s,
WasmI32 startIndex,
@@ -1025,6 +1031,7 @@ external WasmExternRef _jsStringSubstringImport(
);
@pragma("wasm:import", "wasm:js-string.fromCharCodeArray")
@pragma("wasm:pure-function")
external WasmExternRef jsStringFromCharCodeArray(
WasmArray<WasmI16>? array,
WasmI32 start,
@@ -1039,4 +1046,5 @@ external WasmI32 jsStringIntoCharCodeArray(
);
@pragma("wasm:import", "wasm:js-string.test")
@pragma("wasm:pure-function")
external WasmI32 jsStringTest(WasmExternRef? s);