Skip to content

Commit 6c228bf

Browse files
mkustermannCommit Queue
authored andcommitted
[dart2wasm] Add simple way to test IR changes
In order to test generated code for a function one can * place a dart file in `pkg/dart2wasm/tets/ir_tests` * annotate functions that shouldn't be inlined * describe which functions we want to dump in the expectation file * generate an expectation file. This will allow generating renatively small expectation files for only functions we care about and types/globals/... those functions need. Change-Id: Ic7b6b6dece16ab453202aa2c4f9412de2fc251ae Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/454840 Reviewed-by: Ömer Ağacan <[email protected]> Commit-Queue: Martin Kustermann <[email protected]>
1 parent 174c152 commit 6c228bf

File tree

7 files changed

+201
-8
lines changed

7 files changed

+201
-8
lines changed

pkg/dart2wasm/lib/functions.dart

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -257,8 +257,20 @@ class FunctionCollector {
257257
if (target.isUncheckedEntryReference) {
258258
return "$memberName (unchecked entry)";
259259
}
260+
261+
final noInline =
262+
translator.getPragma<bool>(member, "wasm:never-inline", true);
263+
264+
// We add "<noInline>" to the function name. When we invoke `wasm-opt` we
265+
// then pass the `--no-inline=*<noInline>*` flag, which will prevent
266+
// binaryen from inlining those functions.
267+
//
268+
// => Effectively we make `@pragma('wasm:never-inline')` work for binaryen
269+
// as well.
270+
final inlinePostfix = noInline == true ? ' <noInline>' : '';
271+
260272
if (target.isBodyReference) {
261-
return "$memberName (body)";
273+
return "$memberName (body)$inlinePostfix";
262274
}
263275

264276
if (memberName.endsWith('.')) {
@@ -283,11 +295,11 @@ class FunctionCollector {
283295
if (target.isInitializerReference) {
284296
return 'new $memberName (initializer)';
285297
} else if (target.isConstructorBodyReference) {
286-
return 'new $memberName (constructor body)';
298+
return 'new $memberName (constructor body)$inlinePostfix';
287299
} else if (member is Procedure && member.isFactory) {
288300
return 'new $memberName';
289301
} else {
290-
return memberName;
302+
return '$memberName$inlinePostfix';
291303
}
292304
}
293305

pkg/dart2wasm/test/ir_test.dart

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:io';
6+
import 'dart:io' as io;
7+
import 'dart:typed_data';
8+
9+
import 'package:args/args.dart';
10+
import 'package:path/path.dart' as path;
11+
12+
import 'package:wasm_builder/src/ir/ir.dart';
13+
import 'package:wasm_builder/src/serialize/deserializer.dart';
14+
import 'package:wasm_builder/src/serialize/printer.dart';
15+
16+
import 'self_compile_test.dart' show withTempDir;
17+
18+
void main(List<String> args) async {
19+
final result = argParser.parse(args);
20+
final help = result.flag('help');
21+
final write = result.flag('write');
22+
final runFromSource = result.flag('src');
23+
24+
if (help) {
25+
print('Usage:\n${argParser.usage}');
26+
io.exit(0);
27+
}
28+
if (result.rest.isNotEmpty) {
29+
print('Unknown arguments: ${result.rest.join(' ')}');
30+
print('Usage:\n${argParser.usage}');
31+
io.exit(1);
32+
}
33+
34+
await withTempDir((String tempDir) async {
35+
for (final dartFilename in listIrTests()) {
36+
void failTest() {
37+
print('-> test "$dartFilename" failed\n');
38+
io.exitCode = 254;
39+
}
40+
41+
final dartCode = File(dartFilename).readAsStringSync();
42+
final watFile = File(path.setExtension(dartFilename, '.wat'));
43+
final wasmFile = File(path.join(
44+
tempDir, path.setExtension(path.basename(dartFilename), '.wasm')));
45+
46+
print('Testing $dartFilename');
47+
48+
final result = await Process.run('/usr/bin/env', [
49+
'bash',
50+
'pkg/dart2wasm/tool/compile_benchmark',
51+
if (runFromSource) '--src',
52+
'--no-strip-wasm',
53+
'-o',
54+
wasmFile.path,
55+
dartFilename
56+
]);
57+
if (result.exitCode != 0) {
58+
print('Compilation failed:');
59+
print('stdout:\n${result.stdout}');
60+
print('stderr:\n${result.stderr}\n');
61+
failTest();
62+
continue;
63+
}
64+
65+
print('Compiled to ${wasmFile.path}');
66+
67+
final wasmBytes = wasmFile.readAsBytesSync();
68+
final wat =
69+
moduleToString(parseModule(wasmBytes), parseNameFilters(dartCode));
70+
if (write) {
71+
watFile.writeAsStringSync(wat);
72+
continue;
73+
}
74+
if (!watFile.existsSync()) {
75+
print('Expected "${watFile.path}" to exist.');
76+
failTest();
77+
continue;
78+
}
79+
80+
final oldWat = watFile.readAsStringSync();
81+
if (oldWat != wat) {
82+
print(
83+
'-> Expectation mismatch. Run with `-w` to update expectation file.');
84+
failTest();
85+
continue;
86+
}
87+
}
88+
});
89+
}
90+
91+
final argParser = ArgParser()
92+
..addFlag('help',
93+
abbr: 'h', defaultsTo: false, help: 'Prints available options.')
94+
..addFlag('src', defaultsTo: false, help: 'Runs the compiler from source.')
95+
..addFlag('write',
96+
abbr: 'w', defaultsTo: false, help: 'Writes new expectation files.');
97+
98+
Iterable<String> listIrTests() {
99+
return Directory('pkg/dart2wasm/test/ir_tests')
100+
.listSync(recursive: true)
101+
.whereType<File>()
102+
.map((file) => file.path)
103+
.where((path) => path.endsWith('.dart'));
104+
}
105+
106+
Module parseModule(Uint8List wasmBytes) {
107+
final deserializer = Deserializer(wasmBytes);
108+
return Module.deserialize(deserializer);
109+
}
110+
111+
String moduleToString(Module module, List<RegExp> functionNameFilters) {
112+
bool printFunctionBody(BaseFunction function) {
113+
final name = function.functionName;
114+
if (name == null) return false;
115+
return functionNameFilters.any((pattern) => name.contains(pattern));
116+
}
117+
118+
final mp = ModulePrinter(module, printFunctionBody: printFunctionBody);
119+
for (final function in module.functions.defined) {
120+
if (printFunctionBody(function)) {
121+
mp.enqueueFunction(function);
122+
}
123+
}
124+
return mp.print();
125+
}
126+
127+
List<RegExp> parseNameFilters(String dartCode) {
128+
const functionFilter = '// functionFilter=';
129+
final filters = <RegExp>[];
130+
for (final line in dartCode.split('\n')) {
131+
if (line.startsWith(functionFilter)) {
132+
final filter = line.substring(functionFilter.length).trim();
133+
if (filter.isNotEmpty) {
134+
filters.add(RegExp(filter));
135+
}
136+
}
137+
}
138+
return filters;
139+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// functionFilter=main
2+
3+
@pragma('wasm:never-inline')
4+
void main() {
5+
print('hello world');
6+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
(module $module0
2+
(type $#Top (struct (field $field0 i32)))
3+
(type $JSStringImpl (sub final $#Top (struct (field $field0 i32) (field $field1 externref))))
4+
(global $"S.hello world" (import "S" "hello world") externref)
5+
(global $"C327 \"hello world\"" (ref $JSStringImpl) (i32.const 4) (global.get $"S.hello world") (struct.new $JSStringImpl))
6+
(func $"main <noInline>"
7+
global.get $"C327 \"hello world\""
8+
call $print
9+
)
10+
(func $print (param $var0 (ref $#Top)))
11+
)

pkg/dartdev/lib/src/commands/compile.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,8 @@ class CompileWasmCommand extends CompileSubcommandCommand {
738738
--enable-bulk-memory
739739
--enable-threads
740740
741+
--no-inline=*<noInline>*
742+
741743
--closed-world
742744
--traps-never-happen
743745
--type-unfinalizing
@@ -761,6 +763,8 @@ class CompileWasmCommand extends CompileSubcommandCommand {
761763
--enable-bulk-memory
762764
--enable-threads
763765
766+
--no-inline=*<noInline>*
767+
764768
-Os
765769
'''); // end of binaryenFlagsDeferredLoading
766770

pkg/wasm_builder/lib/src/ir/function.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,14 @@ class DefinedFunction extends BaseFunction implements Serializable {
125125
p.write(')');
126126
}
127127

128+
void printDeclarationTo(IrPrinter p) {
129+
p.write('(func \$$functionName ');
130+
p.withLocalNames(localNames, () {
131+
type.printSignatureWithNamesTo(p, oneLine: true);
132+
});
133+
p.writeln(')');
134+
}
135+
128136
@override
129137
String toString() => functionName ?? "#$finalizableIndex";
130138
}

pkg/wasm_builder/lib/src/serialize/printer.dart

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,14 @@ class ModulePrinter {
2626
final _typeQueue = Queue<ir.DefType>();
2727
final _functionsQueue = Queue<ir.DefinedFunction>();
2828

29-
ModulePrinter(this._module);
29+
/// Closure that tells us whether the body of a function should be printed or
30+
/// not.
31+
late final bool Function(ir.BaseFunction) _printFunctionBody;
32+
33+
ModulePrinter(this._module,
34+
{bool Function(ir.BaseFunction)? printFunctionBody}) {
35+
_printFunctionBody = printFunctionBody ?? (_) => true;
36+
}
3037

3138
IrPrinter newIrPrinter() => IrPrinter._(_module, _typeNamer, _globalNamer,
3239
_functionNamer, _tagNamer, _tableNamer);
@@ -74,7 +81,8 @@ class ModulePrinter {
7481
while (_functionsQueue.isNotEmpty || _typeQueue.isNotEmpty) {
7582
while (_functionsQueue.isNotEmpty) {
7683
final fun = _functionsQueue.removeFirst();
77-
_generateFunction(fun);
84+
85+
_generateFunction(fun, includingBody: _printFunctionBody(fun));
7886
}
7987
}
8088

@@ -172,10 +180,15 @@ class ModulePrinter {
172180
_functions[fun] = p.getText();
173181
}
174182

175-
void _generateFunction(ir.DefinedFunction fun) {
183+
void _generateFunction(ir.DefinedFunction fun,
184+
{required bool includingBody}) {
176185
final p = newIrPrinter();
177-
fun.printTo(p);
178-
_functions[fun] = p.getText();
186+
if (includingBody) {
187+
fun.printTo(p);
188+
} else {
189+
fun.printDeclarationTo(p);
190+
}
191+
_functions[fun] = p.getText().trimRight();
179192
}
180193

181194
void _generateType(ir.DefType type) {

0 commit comments

Comments
 (0)