[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:
committed by
Commit Queue
parent
18816329f6
commit
f99f5b1a2b
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -780,7 +780,8 @@ class IsCheckerCallTarget extends CallTarget {
|
||||
signature.inputs,
|
||||
signature.outputs,
|
||||
),
|
||||
name);
|
||||
name)
|
||||
..isPure = true;
|
||||
translator.compilationQueue.add(CompilationTask(function, inliningCodeGen));
|
||||
return function;
|
||||
})();
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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(' ');
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user