[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:
committed by
Commit Queue
parent
174c15212a
commit
6c228bff4a
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// functionFilter=main
|
||||
|
||||
@pragma('wasm:never-inline')
|
||||
void main() {
|
||||
print('hello world');
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user