[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 <omersa@google.com>
Commit-Queue: Martin Kustermann <kustermann@google.com>
This commit is contained in:
Martin Kustermann
2025-10-15 14:25:20 -07:00
committed by Commit Queue
parent 174c15212a
commit 6c228bff4a
7 changed files with 201 additions and 8 deletions
+15 -3
View File
@@ -257,8 +257,20 @@ class FunctionCollector {
if (target.isUncheckedEntryReference) {
return "$memberName (unchecked entry)";
}
final noInline =
translator.getPragma<bool>(member, "wasm:never-inline", true);
// We add "<noInline>" to the function name. When we invoke `wasm-opt` we
// then pass the `--no-inline=*<noInline>*` flag, which will prevent
// binaryen from inlining those functions.
//
// => Effectively we make `@pragma('wasm:never-inline')` work for binaryen
// as well.
final inlinePostfix = noInline == true ? ' <noInline>' : '';
if (target.isBodyReference) {
return "$memberName (body)";
return "$memberName (body)$inlinePostfix";
}
if (memberName.endsWith('.')) {
@@ -283,11 +295,11 @@ class FunctionCollector {
if (target.isInitializerReference) {
return 'new $memberName (initializer)';
} else if (target.isConstructorBodyReference) {
return 'new $memberName (constructor body)';
return 'new $memberName (constructor body)$inlinePostfix';
} else if (member is Procedure && member.isFactory) {
return 'new $memberName';
} else {
return memberName;
return '$memberName$inlinePostfix';
}
}
+139
View File
@@ -0,0 +1,139 @@
// Copyright (c) 2025, 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.
import 'dart:io';
import 'dart:io' as io;
import 'dart:typed_data';
import 'package:args/args.dart';
import 'package:path/path.dart' as path;
import 'package:wasm_builder/src/ir/ir.dart';
import 'package:wasm_builder/src/serialize/deserializer.dart';
import 'package:wasm_builder/src/serialize/printer.dart';
import 'self_compile_test.dart' show withTempDir;
void main(List<String> args) async {
final result = argParser.parse(args);
final help = result.flag('help');
final write = result.flag('write');
final runFromSource = result.flag('src');
if (help) {
print('Usage:\n${argParser.usage}');
io.exit(0);
}
if (result.rest.isNotEmpty) {
print('Unknown arguments: ${result.rest.join(' ')}');
print('Usage:\n${argParser.usage}');
io.exit(1);
}
await withTempDir((String tempDir) async {
for (final dartFilename in listIrTests()) {
void failTest() {
print('-> test "$dartFilename" failed\n');
io.exitCode = 254;
}
final dartCode = File(dartFilename).readAsStringSync();
final watFile = File(path.setExtension(dartFilename, '.wat'));
final wasmFile = File(path.join(
tempDir, path.setExtension(path.basename(dartFilename), '.wasm')));
print('Testing $dartFilename');
final result = await Process.run('/usr/bin/env', [
'bash',
'pkg/dart2wasm/tool/compile_benchmark',
if (runFromSource) '--src',
'--no-strip-wasm',
'-o',
wasmFile.path,
dartFilename
]);
if (result.exitCode != 0) {
print('Compilation failed:');
print('stdout:\n${result.stdout}');
print('stderr:\n${result.stderr}\n');
failTest();
continue;
}
print('Compiled to ${wasmFile.path}');
final wasmBytes = wasmFile.readAsBytesSync();
final wat =
moduleToString(parseModule(wasmBytes), parseNameFilters(dartCode));
if (write) {
watFile.writeAsStringSync(wat);
continue;
}
if (!watFile.existsSync()) {
print('Expected "${watFile.path}" to exist.');
failTest();
continue;
}
final oldWat = watFile.readAsStringSync();
if (oldWat != wat) {
print(
'-> Expectation mismatch. Run with `-w` to update expectation file.');
failTest();
continue;
}
}
});
}
final argParser = ArgParser()
..addFlag('help',
abbr: 'h', defaultsTo: false, help: 'Prints available options.')
..addFlag('src', defaultsTo: false, help: 'Runs the compiler from source.')
..addFlag('write',
abbr: 'w', defaultsTo: false, help: 'Writes new expectation files.');
Iterable<String> listIrTests() {
return Directory('pkg/dart2wasm/test/ir_tests')
.listSync(recursive: true)
.whereType<File>()
.map((file) => file.path)
.where((path) => path.endsWith('.dart'));
}
Module parseModule(Uint8List wasmBytes) {
final deserializer = Deserializer(wasmBytes);
return Module.deserialize(deserializer);
}
String moduleToString(Module module, List<RegExp> functionNameFilters) {
bool printFunctionBody(BaseFunction function) {
final name = function.functionName;
if (name == null) return false;
return functionNameFilters.any((pattern) => name.contains(pattern));
}
final mp = ModulePrinter(module, printFunctionBody: printFunctionBody);
for (final function in module.functions.defined) {
if (printFunctionBody(function)) {
mp.enqueueFunction(function);
}
}
return mp.print();
}
List<RegExp> parseNameFilters(String dartCode) {
const functionFilter = '// functionFilter=';
final filters = <RegExp>[];
for (final line in dartCode.split('\n')) {
if (line.startsWith(functionFilter)) {
final filter = line.substring(functionFilter.length).trim();
if (filter.isNotEmpty) {
filters.add(RegExp(filter));
}
}
}
return filters;
}
+6
View File
@@ -0,0 +1,6 @@
// functionFilter=main
@pragma('wasm:never-inline')
void main() {
print('hello world');
}
+11
View File
@@ -0,0 +1,11 @@
(module $module0
(type $#Top (struct (field $field0 i32)))
(type $JSStringImpl (sub final $#Top (struct (field $field0 i32) (field $field1 externref))))
(global $"S.hello world" (import "S" "hello world") externref)
(global $"C327 \"hello world\"" (ref $JSStringImpl) (i32.const 4) (global.get $"S.hello world") (struct.new $JSStringImpl))
(func $"main <noInline>"
global.get $"C327 \"hello world\""
call $print
)
(func $print (param $var0 (ref $#Top)))
)
@@ -738,6 +738,8 @@ class CompileWasmCommand extends CompileSubcommandCommand {
--enable-bulk-memory
--enable-threads
--no-inline=*<noInline>*
--closed-world
--traps-never-happen
--type-unfinalizing
@@ -761,6 +763,8 @@ class CompileWasmCommand extends CompileSubcommandCommand {
--enable-bulk-memory
--enable-threads
--no-inline=*<noInline>*
-Os
'''); // end of binaryenFlagsDeferredLoading
@@ -125,6 +125,14 @@ class DefinedFunction extends BaseFunction implements Serializable {
p.write(')');
}
void printDeclarationTo(IrPrinter p) {
p.write('(func \$$functionName ');
p.withLocalNames(localNames, () {
type.printSignatureWithNamesTo(p, oneLine: true);
});
p.writeln(')');
}
@override
String toString() => functionName ?? "#$finalizableIndex";
}
@@ -26,7 +26,14 @@ class ModulePrinter {
final _typeQueue = Queue<ir.DefType>();
final _functionsQueue = Queue<ir.DefinedFunction>();
ModulePrinter(this._module);
/// Closure that tells us whether the body of a function should be printed or
/// not.
late final bool Function(ir.BaseFunction) _printFunctionBody;
ModulePrinter(this._module,
{bool Function(ir.BaseFunction)? printFunctionBody}) {
_printFunctionBody = printFunctionBody ?? (_) => true;
}
IrPrinter newIrPrinter() => IrPrinter._(_module, _typeNamer, _globalNamer,
_functionNamer, _tagNamer, _tableNamer);
@@ -74,7 +81,8 @@ class ModulePrinter {
while (_functionsQueue.isNotEmpty || _typeQueue.isNotEmpty) {
while (_functionsQueue.isNotEmpty) {
final fun = _functionsQueue.removeFirst();
_generateFunction(fun);
_generateFunction(fun, includingBody: _printFunctionBody(fun));
}
}
@@ -172,10 +180,15 @@ class ModulePrinter {
_functions[fun] = p.getText();
}
void _generateFunction(ir.DefinedFunction fun) {
void _generateFunction(ir.DefinedFunction fun,
{required bool includingBody}) {
final p = newIrPrinter();
fun.printTo(p);
_functions[fun] = p.getText();
if (includingBody) {
fun.printTo(p);
} else {
fun.printDeclarationTo(p);
}
_functions[fun] = p.getText().trimRight();
}
void _generateType(ir.DefType type) {