diff --git a/pkg/dart2wasm/lib/functions.dart b/pkg/dart2wasm/lib/functions.dart index 768ab79c169..b62566aec56 100644 --- a/pkg/dart2wasm/lib/functions.dart +++ b/pkg/dart2wasm/lib/functions.dart @@ -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', '.')` 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 diff --git a/pkg/dart2wasm/lib/globals.dart b/pkg/dart2wasm/lib/globals.dart index c2a089cfe6f..d7a6f4f6345 100644 --- a/pkg/dart2wasm/lib/globals.dart +++ b/pkg/dart2wasm/lib/globals.dart @@ -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(); diff --git a/pkg/dart2wasm/lib/types.dart b/pkg/dart2wasm/lib/types.dart index 9d5090c210c..1b023c20a62 100644 --- a/pkg/dart2wasm/lib/types.dart +++ b/pkg/dart2wasm/lib/types.dart @@ -780,7 +780,8 @@ class IsCheckerCallTarget extends CallTarget { signature.inputs, signature.outputs, ), - name); + name) + ..isPure = true; translator.compilationQueue.add(CompilationTask(function, inliningCodeGen)); return function; })(); diff --git a/pkg/dart2wasm/lib/util.dart b/pkg/dart2wasm/lib/util.dart index dceb3f808c7..dd4b0fabd01 100644 --- a/pkg/dart2wasm/lib/util.dart +++ b/pkg/dart2wasm/lib/util.dart @@ -108,6 +108,12 @@ String? getWasmWeakExportPragma(CoreTypes coreTypes, Member member) { defaultValue: member.name.text); } +bool hasWasmPureFunctionPragma(CoreTypes coreTypes, Member member) { + return getPragma(coreTypes, member, 'wasm:pure-function', + defaultValue: true) == + true; +} + /// Add a `@pragma('wasm:entry-point')` annotation to an annotatable. T addWasmEntryPointPragma(T node, CoreTypes coreTypes) => addPragma(node, 'wasm:entry-point', coreTypes); diff --git a/pkg/dart2wasm/test/ir_tests/pure_function.dart b/pkg/dart2wasm/test/ir_tests/pure_function.dart new file mode 100644 index 00000000000..84b94e53eb0 --- /dev/null +++ b/pkg/dart2wasm/test/ir_tests/pure_function.dart @@ -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; +} diff --git a/pkg/dart2wasm/test/ir_tests/pure_function.wat b/pkg/dart2wasm/test/ir_tests/pure_function.wat new file mode 100644 index 00000000000..56b2e9c9708 --- /dev/null +++ b/pkg/dart2wasm/test/ir_tests/pure_function.wat @@ -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 " (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 " + global.get $"\"3\"" + call $"foo " + call $print + global.get $"\"4\"" + call $"foo " + 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)) <...>) +) \ No newline at end of file diff --git a/pkg/wasm_builder/lib/src/builder/function.dart b/pkg/wasm_builder/lib/src/builder/function.dart index fe7ca3564a1..827edb91ab3 100644 --- a/pkg/wasm_builder/lib/src/builder/function.dart +++ b/pkg/wasm_builder/lib/src/builder/function.dart @@ -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"; diff --git a/pkg/wasm_builder/lib/src/ir/function.dart b/pkg/wasm_builder/lib/src/ir/function.dart index 4d438655924..022086f8cf0 100644 --- a/pkg/wasm_builder/lib/src/ir/function.dart +++ b/pkg/wasm_builder/lib/src/ir/function.dart @@ -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(' '); diff --git a/pkg/wasm_builder/lib/src/ir/module.dart b/pkg/wasm_builder/lib/src/ir/module.dart index 06850acce41..8f12e99e22f 100644 --- a/pkg/wasm_builder/lib/src/ir/module.dart +++ b/pkg/wasm_builder/lib/src/ir/module.dart @@ -112,12 +112,9 @@ class Module implements Serializable { CodeSection(functions.defined, watchPoints).serialize(s); DataSection(dataSegments.defined, watchPoints).serialize(s); NameSection( - moduleName, - [...functions.imported, ...functions.defined], - types.recursionGroups, - [...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); diff --git a/pkg/wasm_builder/lib/src/serialize/sections.dart b/pkg/wasm_builder/lib/src/serialize/sections.dart index bd013248915..20922e4e7f6 100644 --- a/pkg/wasm_builder/lib/src/serialize/sections.dart +++ b/pkg/wasm_builder/lib/src/serialize/sections.dart @@ -734,9 +734,9 @@ class NameSection extends CustomSection { static const String customSectionName = 'name'; final String? moduleName; - final List functions; + final ir.Functions functions; final List> types; - final List 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; + } + } + } +} diff --git a/sdk/lib/_internal/wasm/lib/js_string.dart b/sdk/lib/_internal/wasm/lib/js_string.dart index c482f854ac8..2ab6aef38ac 100644 --- a/sdk/lib/_internal/wasm/lib/js_string.dart +++ b/sdk/lib/_internal/wasm/lib/js_string.dart @@ -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? 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);