[dart2wasm] Add wasm module printing functionality.
This adds support for printing module IR as text format. For convenience we add a `pkg/dart2wasm/bin/wami.dart` that produces very similar output to V8's `wami`. The goal is to use this to write size/perf optimization tests by dumping IR into expectation files (will add this infrastructure in a future CL) Issue https://github.com/dart-lang/sdk/issues/60928 Change-Id: I42d19c2b8c6242f55693d6ed5d844a5c1ecb1f39 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/454600 Commit-Queue: Martin Kustermann <kustermann@google.com> Reviewed-by: Ömer Ağacan <omersa@google.com>
This commit is contained in:
committed by
Commit Queue
parent
d70aa28821
commit
e63dcafc0c
@@ -0,0 +1,16 @@
|
||||
// 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 'package:wasm_builder/wasm_builder.dart';
|
||||
|
||||
void main(List<String> args) {
|
||||
final input = args[0];
|
||||
final wasmBytes = File(input).readAsBytesSync();
|
||||
|
||||
final deserializer = Deserializer(wasmBytes);
|
||||
final module = Module.deserialize(deserializer);
|
||||
print(module.printAsWat());
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// 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:typed_data';
|
||||
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:wasm_builder/wasm_builder.dart';
|
||||
|
||||
import 'self_compile_test.dart' show withTempDir, run;
|
||||
|
||||
Future main() async {
|
||||
if (!Platform.isLinux && !Platform.isMacOS) return;
|
||||
|
||||
await withTempDir((String tempDir) async {
|
||||
final dartFilename = 'third_party/flute/benchmarks/lib/complex.dart';
|
||||
|
||||
final wasmFilename = path.join(tempDir, 'flute.wasm');
|
||||
final optWasmFilename = path.join(tempDir, 'flute.opt.wasm');
|
||||
final wasmFile = File(wasmFilename);
|
||||
final optWasmFile = File(wasmFilename);
|
||||
|
||||
// Ensure we can print unoptimized dart2wasm modules
|
||||
await run([
|
||||
Platform.executable,
|
||||
'compile',
|
||||
'wasm',
|
||||
'-O0',
|
||||
dartFilename,
|
||||
'-o',
|
||||
wasmFilename,
|
||||
]);
|
||||
wasmPrint(wasmFile.readAsBytesSync());
|
||||
|
||||
// Ensure we can print wasm-opt optimized wasm modules
|
||||
await run([
|
||||
Platform.executable,
|
||||
'compile',
|
||||
'wasm',
|
||||
'-O3',
|
||||
dartFilename,
|
||||
'-o',
|
||||
optWasmFilename,
|
||||
]);
|
||||
wasmPrint(optWasmFile.readAsBytesSync());
|
||||
|
||||
// Temporary files will be deleted when returning to [withTempDir].
|
||||
});
|
||||
}
|
||||
|
||||
void wasmPrint(Uint8List wasmBytes) {
|
||||
final deserializer = Deserializer(wasmBytes);
|
||||
final module = Module.deserialize(deserializer);
|
||||
print('len = ${module.printAsWat().length}');
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import '../serialize/serialize.dart';
|
||||
import '../serialize/printer.dart';
|
||||
import 'ir.dart';
|
||||
|
||||
/// A local variable defined in a function.
|
||||
@@ -12,6 +13,15 @@ class Local {
|
||||
|
||||
Local(this.index, this.type);
|
||||
|
||||
void printTo(IrPrinter p, Map<int, String> localNames,
|
||||
{bool isParam = false}) {
|
||||
p.write(isParam ? 'param' : 'local');
|
||||
p.write(' ');
|
||||
p.writeLocalIndexReference(index);
|
||||
p.write(' ');
|
||||
p.writeValueType(type);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => "$index";
|
||||
}
|
||||
@@ -82,6 +92,39 @@ class DefinedFunction extends BaseFunction implements Serializable {
|
||||
s.writeData(localS);
|
||||
}
|
||||
|
||||
void printTo(IrPrinter p) {
|
||||
p.write('(func ');
|
||||
p.writeFunctionReference(this);
|
||||
String? exportName;
|
||||
for (final f in enclosingModule.exports.exported) {
|
||||
if (f is FunctionExport && f.function == this) {
|
||||
exportName = f.name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (exportName != null) {
|
||||
p.write(' ');
|
||||
p.writeExport(exportName);
|
||||
}
|
||||
|
||||
p.withLocalNames(localNames, () {
|
||||
if (type.inputs.isNotEmpty || type.outputs.isNotEmpty) {
|
||||
p.write(' ');
|
||||
type.printSignatureWithNamesTo(p, oneLine: true);
|
||||
}
|
||||
p.writeln('');
|
||||
p.withIndent(() {
|
||||
for (int i = type.inputs.length; i < locals.length; ++i) {
|
||||
p.write('(');
|
||||
locals[i].printTo(p, localNames);
|
||||
p.writeln(')');
|
||||
}
|
||||
body.printTo(p);
|
||||
});
|
||||
});
|
||||
p.write(')');
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => functionName ?? "#$finalizableIndex";
|
||||
}
|
||||
@@ -105,6 +148,15 @@ class ImportedFunction extends BaseFunction implements Import {
|
||||
s.writeUnsigned(type.index);
|
||||
}
|
||||
|
||||
void printTo(IrPrinter p) {
|
||||
p.write('(func ');
|
||||
p.writeFunctionReference(this);
|
||||
p.writeImport(module, name);
|
||||
p.write(' ');
|
||||
type.printOneLineSignatureTo(p);
|
||||
p.write(')');
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => "$module.$name";
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import '../serialize/serialize.dart';
|
||||
import '../serialize/printer.dart';
|
||||
import 'ir.dart';
|
||||
|
||||
/// An (imported or defined) global variable.
|
||||
@@ -26,6 +27,8 @@ abstract class Global with Indexable, Exportable {
|
||||
Export buildExport(String name) {
|
||||
return GlobalExport(name, this);
|
||||
}
|
||||
|
||||
void printTo(IrPrinter p) => throw 'not implemented';
|
||||
}
|
||||
|
||||
/// A global variable defined in a module.
|
||||
@@ -41,6 +44,21 @@ class DefinedGlobal extends Global implements Serializable {
|
||||
s.write(type);
|
||||
s.write(initializer);
|
||||
}
|
||||
|
||||
@override
|
||||
void printTo(IrPrinter p) {
|
||||
// This may generate globals this one refers to.
|
||||
final ip = p.dup();
|
||||
initializer.printInitializerTo(ip);
|
||||
|
||||
p.write('(global ');
|
||||
p.writeGlobalReference(this);
|
||||
p.write(' ');
|
||||
type.printTo(p);
|
||||
p.write(' ');
|
||||
p.write(ip.getText().trim());
|
||||
p.write(')');
|
||||
}
|
||||
}
|
||||
|
||||
/// An imported global variable.
|
||||
@@ -62,6 +80,17 @@ class ImportedGlobal extends Global implements Import {
|
||||
s.writeByte(0x03);
|
||||
s.write(type);
|
||||
}
|
||||
|
||||
@override
|
||||
void printTo(IrPrinter p) {
|
||||
p.write('(global ');
|
||||
p.writeGlobalReference(this);
|
||||
p.write(' ');
|
||||
p.writeImport(module, name);
|
||||
p.write(' ');
|
||||
p.writeValueType(type.type);
|
||||
p.write(')');
|
||||
}
|
||||
}
|
||||
|
||||
class GlobalExport extends Export {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@
|
||||
|
||||
import '../../source_map.dart';
|
||||
import '../serialize/serialize.dart';
|
||||
import '../serialize/printer.dart';
|
||||
import 'ir.dart';
|
||||
|
||||
class Instructions implements Serializable {
|
||||
@@ -69,6 +70,79 @@ class Instructions implements Serializable {
|
||||
s.sourceMapSerializer.addMapping(s.offset, null);
|
||||
}
|
||||
|
||||
void printInitializerTo(IrPrinter p) {
|
||||
for (int k = 0; k < instructions.length; ++k) {
|
||||
final i = instructions[k];
|
||||
if (i is End) return;
|
||||
p.write(k > 0 ? ' (' : '(');
|
||||
i.printTo(p);
|
||||
p.write(')');
|
||||
}
|
||||
}
|
||||
|
||||
void printTo(IrPrinter p) {
|
||||
p.beginLabeledBlock(null);
|
||||
for (int k = 0; k < instructions.length; ++k) {
|
||||
final i = instructions[k];
|
||||
|
||||
final isTry = i is BeginNoEffectTry ||
|
||||
i is BeginOneOutputTry ||
|
||||
i is BeginFunctionTry;
|
||||
final isTryTable = i is BeginNoEffectTryTable ||
|
||||
i is BeginOneOutputTryTable ||
|
||||
i is BeginFunctionTryTable;
|
||||
final isIf =
|
||||
i is BeginNoEffectIf || i is BeginOneOutputIf || i is BeginFunctionIf;
|
||||
final isBlock = i is BeginNoEffectBlock ||
|
||||
i is BeginOneOutputBlock ||
|
||||
i is BeginFunctionBlock;
|
||||
final isLoop = i is BeginNoEffectLoop ||
|
||||
i is BeginOneOutputLoop ||
|
||||
i is BeginFunctionLoop;
|
||||
if (isTry || isIf || isBlock || isTryTable || isLoop) {
|
||||
p.beginLabeledBlock(i);
|
||||
i.printTo(p);
|
||||
p.writeln();
|
||||
p.indent();
|
||||
continue;
|
||||
}
|
||||
|
||||
final isCatch = i is CatchLegacy || i is CatchAllLegacy;
|
||||
final isElse = i is Else;
|
||||
if (isCatch || isElse) {
|
||||
p.deindent();
|
||||
i.printTo(p);
|
||||
p.writeln();
|
||||
p.indent();
|
||||
continue;
|
||||
}
|
||||
|
||||
final isEnd = i is End;
|
||||
if (isEnd) {
|
||||
final labelInfo = p.endLabeledBlock();
|
||||
if (labelInfo?.target != null) {
|
||||
// The outermost label belongs to the function and it wasn't indented
|
||||
// so we don't have to deindent either.
|
||||
p.deindent();
|
||||
}
|
||||
final isLast = k == (instructions.length - 1);
|
||||
if (!isLast) {
|
||||
i.printTo(p);
|
||||
if (labelInfo != null && labelInfo.used) {
|
||||
p.write(' ');
|
||||
p.write(labelInfo.name!);
|
||||
}
|
||||
p.writeln();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
i.printTo(p);
|
||||
p.writeln();
|
||||
}
|
||||
p.endLabeledBlock();
|
||||
}
|
||||
|
||||
static Instructions deserializeConst(
|
||||
Deserializer d,
|
||||
Types types,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import '../serialize/serialize.dart';
|
||||
import '../serialize/printer.dart';
|
||||
import 'ir.dart';
|
||||
|
||||
/// A logically const wasm module ready to encode. Created with `ModuleBuilder`.
|
||||
@@ -233,4 +234,28 @@ class Module implements Serializable {
|
||||
sourceMapUrl,
|
||||
);
|
||||
}
|
||||
|
||||
String printAsWat() {
|
||||
final mp = ModulePrinter(this);
|
||||
|
||||
// Enqueue all types, tags, globals, functions thereby making the
|
||||
// printed module contain most things we care about.
|
||||
for (final type in types.defined) {
|
||||
if (type is! FunctionType) {
|
||||
mp.enqueueType(type);
|
||||
}
|
||||
}
|
||||
for (final tag in [...tags.imported, ...tags.defined]) {
|
||||
mp.enqueueTag(tag);
|
||||
}
|
||||
|
||||
for (final global in [...globals.imported, ...globals.defined]) {
|
||||
mp.enqueueGlobal(global);
|
||||
}
|
||||
|
||||
for (final function in [...functions.imported, ...functions.defined]) {
|
||||
mp.enqueueFunction(function);
|
||||
}
|
||||
return mp.print();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import '../serialize/serialize.dart';
|
||||
import '../serialize/printer.dart';
|
||||
import 'ir.dart';
|
||||
|
||||
/// An (imported or defined) table.
|
||||
@@ -44,6 +45,27 @@ class DefinedTable extends Table {
|
||||
|
||||
DefinedTable(super.enclosingModule, this.elements, super.finalizableIndex,
|
||||
super.type, super.minSize, super.maxSize);
|
||||
|
||||
void printTo(IrPrinter p) {
|
||||
// NOTE: This format differs from what V8's `wami` will print.
|
||||
// It makes it easier to see the exact values of the table.
|
||||
p.write('(table ');
|
||||
p.writeTableReference(this, alwaysPrint: true);
|
||||
p.write(' ${elements.length} ');
|
||||
p.writeValueType(type);
|
||||
p.writeln();
|
||||
p.withIndent(() {
|
||||
for (int i = 0; i < elements.length; ++i) {
|
||||
final function = elements[i];
|
||||
if (function != null) {
|
||||
p.write('(at $i ');
|
||||
p.writeFunctionReference(function);
|
||||
p.writeln(')');
|
||||
}
|
||||
}
|
||||
});
|
||||
p.write(')');
|
||||
}
|
||||
}
|
||||
|
||||
/// An imported table.
|
||||
|
||||
@@ -17,4 +17,6 @@ class Tables {
|
||||
Table operator [](int index) => index < imported.length
|
||||
? imported[index]
|
||||
: defined[index - imported.length];
|
||||
|
||||
int get length => imported.length + defined.length;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import '../serialize/serialize.dart';
|
||||
import '../serialize/printer.dart';
|
||||
import 'ir.dart';
|
||||
|
||||
/// An exported tag from the current module.
|
||||
@@ -36,6 +37,8 @@ abstract class Tag with Indexable, Exportable {
|
||||
Export buildExport(String name) {
|
||||
return TagExport(name, this);
|
||||
}
|
||||
|
||||
void printTo(IrPrinter p);
|
||||
}
|
||||
|
||||
/// A tag defined in the current module.
|
||||
@@ -48,6 +51,15 @@ class DefinedTag extends Tag implements Serializable {
|
||||
s.writeByte(0x00);
|
||||
s.write(type);
|
||||
}
|
||||
|
||||
@override
|
||||
void printTo(IrPrinter p) {
|
||||
p.write('(tag ');
|
||||
p.writeTagReference(this);
|
||||
p.write(' ');
|
||||
type.printOneLineSignatureTo(p);
|
||||
p.write(')');
|
||||
}
|
||||
}
|
||||
|
||||
/// A tag imported from another module.
|
||||
@@ -70,6 +82,17 @@ class ImportedTag extends Tag implements Import {
|
||||
s.writeByte(0x00);
|
||||
s.write(type);
|
||||
}
|
||||
|
||||
@override
|
||||
void printTo(IrPrinter p) {
|
||||
p.write('(tag ');
|
||||
p.writeTagReference(this);
|
||||
p.write(' ');
|
||||
p.writeImport(module, name);
|
||||
p.write(' ');
|
||||
type.printOneLineSignatureTo(p);
|
||||
p.write(')');
|
||||
}
|
||||
}
|
||||
|
||||
class Tags {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import '../serialize/serialize.dart';
|
||||
import '../serialize/printer.dart';
|
||||
|
||||
// Representations of all Wasm types.
|
||||
|
||||
@@ -298,7 +299,7 @@ class RefType extends ValueType {
|
||||
if (nullable == heapType.nullableByDefault) {
|
||||
return "${heapType.shorthandName}ref";
|
||||
}
|
||||
return "ref${nullable ? " null " : " "}$heapType";
|
||||
return "ref ${nullable ? "null " : ""}$heapType";
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -685,7 +686,7 @@ class NoExternHeapType extends HeapType {
|
||||
void serialize(Serializer s) => s.writeByte(0x72); // -0x0e
|
||||
|
||||
@override
|
||||
String toString() => "extern";
|
||||
String toString() => "noextern";
|
||||
|
||||
@override
|
||||
String get shorthandName => "nullextern";
|
||||
@@ -873,6 +874,20 @@ abstract class DefType extends HeapType {
|
||||
}
|
||||
|
||||
void deserializeFillInner(Deserializer d, List<DefType> existing);
|
||||
|
||||
void printTypeDefTo(IrPrinter p) {
|
||||
// This may generate other types that this one refers to.
|
||||
final ip = p.dup();
|
||||
printTypeDefToInternal(ip);
|
||||
|
||||
p.write('(type ');
|
||||
p.writeDefTypeReference(this);
|
||||
p.write(' ');
|
||||
p.write(ip.getText());
|
||||
p.write(')');
|
||||
}
|
||||
|
||||
void printTypeDefToInternal(IrPrinter p);
|
||||
}
|
||||
|
||||
class UnresolvedDefType extends DefType {
|
||||
@@ -906,6 +921,10 @@ class UnresolvedDefType extends DefType {
|
||||
@override
|
||||
void deserializeFillInner(Deserializer d, List<DefType> existing) =>
|
||||
throw 'Cannot deserialize unresolved type';
|
||||
|
||||
@override
|
||||
void printTypeDefToInternal(IrPrinter p) =>
|
||||
throw 'Cannot print unresolved type';
|
||||
}
|
||||
|
||||
/// The `exn` heap type.
|
||||
@@ -1030,6 +1049,59 @@ class FunctionType extends DefType {
|
||||
outputs.addAll(d.readList((d) => ValueType.deserialize(d, existing)));
|
||||
}
|
||||
|
||||
@override
|
||||
void printTypeDefToInternal(IrPrinter p) {
|
||||
p.withLocalNames({}, () {
|
||||
p.write('(func ');
|
||||
printSignatureWithNamesTo(p, oneLine: false);
|
||||
p.write(')');
|
||||
});
|
||||
}
|
||||
|
||||
void printOneLineSignatureTo(IrPrinter p) {
|
||||
if (inputs.isNotEmpty) {
|
||||
p.write('(param ');
|
||||
for (int i = 0; i < inputs.length; ++i) {
|
||||
if (i > 0) p.write(' ');
|
||||
p.writeValueType(inputs[i]);
|
||||
}
|
||||
p.write(')');
|
||||
}
|
||||
if (inputs.isNotEmpty && outputs.isNotEmpty) {
|
||||
p.write(' ');
|
||||
}
|
||||
for (int i = 0; i < outputs.length; ++i) {
|
||||
if (i > 0) p.write(' ');
|
||||
p.write('(result ');
|
||||
p.writeValueType(outputs[i]);
|
||||
p.write(')');
|
||||
}
|
||||
}
|
||||
|
||||
void printSignatureWithNamesTo(IrPrinter p, {bool oneLine = true}) {
|
||||
final indent = !oneLine && (inputs.length + outputs.length) > 2;
|
||||
final sep = indent ? '\n ' : ' ';
|
||||
|
||||
if (indent) p.write(sep);
|
||||
for (int i = 0; i < inputs.length; ++i) {
|
||||
if (i > 0) p.write(sep);
|
||||
p.write('(param ');
|
||||
p.writeLocalIndexReference(i);
|
||||
p.write(' ');
|
||||
p.writeValueType(inputs[i]);
|
||||
p.write(')');
|
||||
}
|
||||
if (inputs.isNotEmpty && outputs.isNotEmpty) {
|
||||
p.write(sep);
|
||||
}
|
||||
for (int i = 0; i < outputs.length; ++i) {
|
||||
if (i > 0) p.write(sep);
|
||||
p.write('(result ');
|
||||
p.writeValueType(outputs[i]);
|
||||
p.write(')');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => "(${inputs.join(", ")}) -> (${outputs.join(", ")})";
|
||||
}
|
||||
@@ -1113,6 +1185,47 @@ class StructType extends DataType {
|
||||
void deserializeFillInner(Deserializer d, List<DefType> existing) {
|
||||
fields.addAll(d.readList((d) => FieldType.deserialize(d, existing)));
|
||||
}
|
||||
|
||||
@override
|
||||
void printTypeDefToInternal(IrPrinter p) {
|
||||
final sup = superType;
|
||||
|
||||
p.write('(');
|
||||
if (sup != null) {
|
||||
p.write('sub ');
|
||||
if (!hasAnySubtypes) {
|
||||
p.write('final ');
|
||||
}
|
||||
p.writeDefTypeReference(sup);
|
||||
p.write(' (');
|
||||
}
|
||||
p.write('struct');
|
||||
p.withIndent(() {
|
||||
for (int i = 0; i < fields.length; ++i) {
|
||||
if (fields.length > 2) {
|
||||
p.writeln();
|
||||
} else {
|
||||
p.write(' ');
|
||||
}
|
||||
|
||||
final field = fields[i];
|
||||
final name = '\$${fieldNames[i] ?? 'field$i'}';
|
||||
p.write('(field $name ');
|
||||
if (field.mutable) {
|
||||
p.write('(mut ');
|
||||
p.writeStorageTypeTypeReference(field.type);
|
||||
p.write(')');
|
||||
} else {
|
||||
p.writeStorageTypeTypeReference(field.type);
|
||||
}
|
||||
p.write(')');
|
||||
}
|
||||
});
|
||||
if (sup != null) {
|
||||
p.write(')');
|
||||
}
|
||||
p.write(')');
|
||||
}
|
||||
}
|
||||
|
||||
/// A custom `array` type.
|
||||
@@ -1157,6 +1270,20 @@ class ArrayType extends DataType {
|
||||
void deserializeFillInner(Deserializer d, List<DefType> existing) {
|
||||
elementType = FieldType.deserialize(d, existing);
|
||||
}
|
||||
|
||||
@override
|
||||
void printTypeDefToInternal(IrPrinter p) {
|
||||
p.write('(array ');
|
||||
p.write('(field ');
|
||||
if (elementType.mutable) {
|
||||
p.write('(mut ');
|
||||
}
|
||||
p.writeStorageTypeTypeReference(elementType.type);
|
||||
if (elementType.mutable) {
|
||||
p.write(')');
|
||||
}
|
||||
p.write('))');
|
||||
}
|
||||
}
|
||||
|
||||
class _WithMutability<T extends StorageType> implements Serializable {
|
||||
@@ -1193,6 +1320,12 @@ class GlobalType extends _WithMutability<ValueType> {
|
||||
_WithMutability.deserialize(d, (d) => ValueType.deserialize(d, types));
|
||||
return GlobalType(type, mutable: mutable);
|
||||
}
|
||||
|
||||
void printTo(IrPrinter p) {
|
||||
if (mutable) p.write('(mut ');
|
||||
p.writeValueType(type);
|
||||
if (mutable) p.write(')');
|
||||
}
|
||||
}
|
||||
|
||||
/// A type for a struct field or an array element.
|
||||
|
||||
@@ -0,0 +1,663 @@
|
||||
// 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:core';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:collection';
|
||||
|
||||
import '../ir/ir.dart' as ir;
|
||||
|
||||
class ModulePrinter {
|
||||
final ir.Module _module;
|
||||
|
||||
late final _typeNamer = _TypeNamer(enqueueType);
|
||||
late final _globalNamer = _GlobalNamer(enqueueGlobal);
|
||||
late final _functionNamer = _FunctionNamer(enqueueFunction);
|
||||
late final _tagNamer = _TagNamer(enqueueTag);
|
||||
late final _tableNamer = _TableNamer(enqueueTable);
|
||||
|
||||
final _types = <ir.DefType, String>{};
|
||||
final _tags = <ir.Tag, String>{};
|
||||
final _tables = <ir.Table, String>{};
|
||||
final _globals = <ir.Global, String>{};
|
||||
final _functions = <ir.BaseFunction, String>{};
|
||||
|
||||
final _typeQueue = Queue<ir.DefType>();
|
||||
final _functionsQueue = Queue<ir.DefinedFunction>();
|
||||
|
||||
ModulePrinter(this._module);
|
||||
|
||||
IrPrinter newIrPrinter() => IrPrinter._(_module, _typeNamer, _globalNamer,
|
||||
_functionNamer, _tagNamer, _tableNamer);
|
||||
|
||||
void enqueueType(ir.DefType type) {
|
||||
if (!_types.containsKey(type)) {
|
||||
_types[type] = '';
|
||||
_generateType(type);
|
||||
}
|
||||
}
|
||||
|
||||
void enqueueGlobal(ir.Global global) {
|
||||
if (!_globals.containsKey(global)) {
|
||||
_globals[global] = '';
|
||||
_generateGlobal(global);
|
||||
}
|
||||
}
|
||||
|
||||
void enqueueFunction(ir.BaseFunction fun) {
|
||||
if (!_functions.containsKey(fun)) {
|
||||
_functions[fun] = '';
|
||||
if (fun is ir.ImportedFunction) {
|
||||
_generateImportedFunction(fun);
|
||||
} else {
|
||||
_functionsQueue.add(fun as ir.DefinedFunction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void enqueueTag(ir.Tag tag) {
|
||||
if (!_tags.containsKey(tag)) {
|
||||
_tags[tag] = '';
|
||||
_generateTag(tag);
|
||||
}
|
||||
}
|
||||
|
||||
void enqueueTable(ir.Table table) {
|
||||
if (!_tables.containsKey(table)) {
|
||||
_tables[table] = '';
|
||||
_generateTable(table);
|
||||
}
|
||||
}
|
||||
|
||||
String print() {
|
||||
while (_functionsQueue.isNotEmpty || _typeQueue.isNotEmpty) {
|
||||
while (_functionsQueue.isNotEmpty) {
|
||||
final fun = _functionsQueue.removeFirst();
|
||||
_generateFunction(fun);
|
||||
}
|
||||
}
|
||||
|
||||
final mp = IndentPrinter();
|
||||
mp.writeln('(module \$${_module.moduleName}');
|
||||
mp.withIndent(() {
|
||||
for (final group in _module.types.recursionGroups) {
|
||||
final filtered = group.where((t) => _types.containsKey(t)).toList();
|
||||
if (filtered.isNotEmpty) {
|
||||
if (filtered.length == 1) {
|
||||
mp.write(_types[filtered.single]!);
|
||||
mp.writeln();
|
||||
} else {
|
||||
mp.writeln('(rec');
|
||||
mp.withIndent(() {
|
||||
for (final type in filtered) {
|
||||
mp.write(_types[type]!);
|
||||
mp.writeln();
|
||||
}
|
||||
});
|
||||
mp.writeln(')');
|
||||
}
|
||||
}
|
||||
}
|
||||
for (final fun in _module.functions.imported) {
|
||||
final s = _functions[fun];
|
||||
if (s != null) {
|
||||
mp.write(s);
|
||||
mp.writeln();
|
||||
}
|
||||
}
|
||||
for (final global in _module.globals.imported) {
|
||||
final s = _globals[global];
|
||||
if (s != null) {
|
||||
mp.write(s);
|
||||
mp.writeln();
|
||||
}
|
||||
}
|
||||
for (final tag in _module.tags.defined) {
|
||||
final s = _tags[tag];
|
||||
if (s != null) {
|
||||
mp.write(s);
|
||||
mp.writeln();
|
||||
}
|
||||
}
|
||||
for (final global in _module.globals.defined) {
|
||||
final s = _globals[global];
|
||||
if (s != null) {
|
||||
mp.write(s);
|
||||
mp.writeln();
|
||||
}
|
||||
}
|
||||
for (final table in _module.tables.defined) {
|
||||
final s = _tables[table];
|
||||
if (s != null) {
|
||||
mp.write(s);
|
||||
mp.writeln();
|
||||
}
|
||||
}
|
||||
for (final fun in _module.functions.defined) {
|
||||
final s = _functions[fun];
|
||||
if (s != null) {
|
||||
mp.write(s);
|
||||
mp.writeln();
|
||||
}
|
||||
}
|
||||
});
|
||||
mp.write(')');
|
||||
return mp.getText();
|
||||
}
|
||||
|
||||
void _generateTag(ir.Tag tag) {
|
||||
final p = newIrPrinter();
|
||||
tag.printTo(p);
|
||||
_tags[tag] = p.getText();
|
||||
}
|
||||
|
||||
void _generateTable(ir.Table table) {
|
||||
if (table is! ir.DefinedTable) return;
|
||||
|
||||
final p = newIrPrinter();
|
||||
table.printTo(p);
|
||||
_tables[table] = p.getText();
|
||||
}
|
||||
|
||||
void _generateGlobal(ir.Global global) {
|
||||
final p = newIrPrinter();
|
||||
global.printTo(p);
|
||||
_globals[global] = p.getText();
|
||||
}
|
||||
|
||||
void _generateImportedFunction(ir.ImportedFunction fun) {
|
||||
final p = newIrPrinter();
|
||||
fun.printTo(p);
|
||||
_functions[fun] = p.getText();
|
||||
}
|
||||
|
||||
void _generateFunction(ir.DefinedFunction fun) {
|
||||
final p = newIrPrinter();
|
||||
fun.printTo(p);
|
||||
_functions[fun] = p.getText();
|
||||
}
|
||||
|
||||
void _generateType(ir.DefType type) {
|
||||
final p = newIrPrinter();
|
||||
type.printTypeDefTo(p);
|
||||
_types[type] = p.getText();
|
||||
}
|
||||
}
|
||||
|
||||
class IndentPrinter {
|
||||
final _buffer = StringBuffer();
|
||||
int _indent = 0;
|
||||
bool _startOfLine = true;
|
||||
|
||||
void write(String s) {
|
||||
final lines = s.split('\n');
|
||||
for (int i = 0; i < lines.length; ++i) {
|
||||
_writePartOfLine(lines[i]);
|
||||
if (i < (lines.length - 1)) {
|
||||
_writeNewLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void writeImport(String module, String name) {
|
||||
write('(import "');
|
||||
write(_escapeString(module));
|
||||
write('" "');
|
||||
write(_escapeString(name));
|
||||
write('")');
|
||||
}
|
||||
|
||||
void writeExport(String name) {
|
||||
write('(export "${_escapeString(name)}")');
|
||||
}
|
||||
|
||||
void writeln([String? s]) {
|
||||
if (s != null) {
|
||||
write(s);
|
||||
}
|
||||
write('\n');
|
||||
}
|
||||
|
||||
void withIndent(void Function() fun) {
|
||||
final before = _indent;
|
||||
_indent++;
|
||||
fun();
|
||||
assert(before == (_indent - 1));
|
||||
_indent = before;
|
||||
}
|
||||
|
||||
void indent() {
|
||||
_indent++;
|
||||
}
|
||||
|
||||
void deindent() {
|
||||
_indent--;
|
||||
assert(_indent >= 0);
|
||||
}
|
||||
|
||||
void _writePartOfLine(String text) {
|
||||
assert(!text.contains('\n'));
|
||||
if (text.isEmpty) return;
|
||||
if (_startOfLine) {
|
||||
_buffer.write(' ' * _indent);
|
||||
_startOfLine = false;
|
||||
}
|
||||
_buffer.write(text);
|
||||
}
|
||||
|
||||
void _writeNewLine() {
|
||||
_buffer.write('\n');
|
||||
_startOfLine = true;
|
||||
}
|
||||
|
||||
String getText() => '$_buffer';
|
||||
}
|
||||
|
||||
class IrPrinter extends IndentPrinter {
|
||||
final ir.Module module;
|
||||
|
||||
final _TypeNamer _typeNamer;
|
||||
final _GlobalNamer _globalNamer;
|
||||
final _FunctionNamer _functionNamer;
|
||||
final _TagNamer _tagNamer;
|
||||
final _TableNamer _tableNamer;
|
||||
|
||||
_LocalNamer? _localNamer;
|
||||
final _labelNamer = _LabelNamer();
|
||||
|
||||
IrPrinter._(this.module, this._typeNamer, this._globalNamer,
|
||||
this._functionNamer, this._tagNamer, this._tableNamer);
|
||||
|
||||
/// Returns a new [IrPrinter] with same settings, but empty indentation,
|
||||
/// empty text content and no local namer.
|
||||
IrPrinter dup() => IrPrinter._(
|
||||
module, _typeNamer, _globalNamer, _functionNamer, _tagNamer, _tableNamer);
|
||||
|
||||
void beginLabeledBlock(ir.Instruction? instruction) {
|
||||
_labelNamer.stack.add(LabelInfo(instruction));
|
||||
}
|
||||
|
||||
LabelInfo? endLabeledBlock() {
|
||||
if (_labelNamer.stack.isEmpty) return null;
|
||||
final last = _labelNamer.stack.removeLast();
|
||||
return last;
|
||||
}
|
||||
|
||||
void writeLabelDefinition(int labelIndex) {
|
||||
write(_labelNamer.nameLabel(labelIndex, use: false));
|
||||
}
|
||||
|
||||
void writeLabelReference(int labelIndex) {
|
||||
write(_labelNamer.nameLabel(labelIndex));
|
||||
}
|
||||
|
||||
void writeLocalReference(ir.Local local) {
|
||||
write(_localNamer!.nameLocal(local.index));
|
||||
}
|
||||
|
||||
void writeLocalIndexReference(int localIndex) {
|
||||
write(_localNamer!.nameLocal(localIndex));
|
||||
}
|
||||
|
||||
void withLocalNames(Map<int, String> names, void Function() fun) {
|
||||
_localNamer = _LocalNamer(names);
|
||||
fun();
|
||||
_localNamer = null;
|
||||
}
|
||||
|
||||
void writeStorageTypeTypeReference(ir.StorageType type, {bool ref = true}) {
|
||||
if (type is ir.PackedType) {
|
||||
write('$type');
|
||||
return;
|
||||
}
|
||||
writeValueType(type as ir.ValueType);
|
||||
}
|
||||
|
||||
String _defTypeName(ir.DefType type, bool nullable, bool ref) {
|
||||
final name = _typeNamer.nameDefType(type);
|
||||
if (ref) {
|
||||
return nullable ? '(ref null $name)' : '(ref $name)';
|
||||
}
|
||||
return nullable ? 'null $name' : name;
|
||||
}
|
||||
|
||||
void writeRefTypeReference(ir.RefType type) {
|
||||
writeValueType(type, ref: false);
|
||||
}
|
||||
|
||||
void writeValueType(ir.ValueType type, {bool ref = true}) {
|
||||
if (type is ir.NumType) {
|
||||
write('$type');
|
||||
return;
|
||||
}
|
||||
if (type is ir.RefType) {
|
||||
final heapType = type.heapType;
|
||||
if (heapType is ir.DefType) {
|
||||
write(_defTypeName(heapType, type.nullable, ref));
|
||||
return;
|
||||
}
|
||||
|
||||
if (heapType.nullableByDefault == true && type.nullable) {
|
||||
write('$type');
|
||||
return;
|
||||
}
|
||||
if (heapType.nullableByDefault != true && !type.nullable) {
|
||||
write('$type');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
write('($type)');
|
||||
}
|
||||
|
||||
void writeDefTypeReference(ir.DefType type) {
|
||||
write(_typeNamer.nameDefType(type));
|
||||
}
|
||||
|
||||
void writeFunctionType(ir.FunctionType type) {
|
||||
type.printOneLineSignatureTo(this);
|
||||
}
|
||||
|
||||
void writeHeapTypeReference(ir.HeapType heapType) {
|
||||
if (heapType is ir.DefType) {
|
||||
write(_defTypeName(heapType, false, false));
|
||||
return;
|
||||
}
|
||||
write('$heapType');
|
||||
}
|
||||
|
||||
void writeTableReference(ir.Table? table, {bool alwaysPrint = false}) {
|
||||
final name = _tableNamer.nameTable(table);
|
||||
if (alwaysPrint) {
|
||||
write(name);
|
||||
return;
|
||||
}
|
||||
if (module.tables.length > 1) {
|
||||
write(' ');
|
||||
write(name);
|
||||
}
|
||||
}
|
||||
|
||||
void writeFieldReference(ir.StructType type, int fieldIndex) {
|
||||
final name = type.fieldNames[fieldIndex] ?? 'field$fieldIndex';
|
||||
write(_defTypeName(type, false, false));
|
||||
write(' \$$name');
|
||||
}
|
||||
|
||||
void writeGlobalReference(ir.Global global) {
|
||||
write(_globalNamer.nameGlobal(global));
|
||||
}
|
||||
|
||||
void writeFunctionReference(ir.BaseFunction function) {
|
||||
write(_functionNamer.nameFunction(function));
|
||||
}
|
||||
|
||||
void writeTagReference(ir.Tag tag) {
|
||||
write(_tagNamer.nameTag(tag));
|
||||
}
|
||||
|
||||
void writeDataReference(ir.BaseDataSegment dataSegment) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
void writeMemoryReference(ir.Memory memory) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
class _Namer<T> {
|
||||
int _nextId = 0;
|
||||
|
||||
final Map<T, String> _names = {};
|
||||
final void Function(T) _onReference;
|
||||
|
||||
_Namer(this._onReference);
|
||||
|
||||
String _name(T key, String? name, String unnamedPrefix) {
|
||||
final existing = _names[key];
|
||||
if (existing != null) return existing;
|
||||
|
||||
_onReference(key);
|
||||
final sanitizedName =
|
||||
name != null ? _sanitizeName(name) : '$unnamedPrefix${_nextId++}';
|
||||
return _names[key] ??= '\$$sanitizedName';
|
||||
}
|
||||
}
|
||||
|
||||
class _FunctionNamer extends _Namer<ir.BaseFunction> {
|
||||
_FunctionNamer(super.onReference);
|
||||
|
||||
String nameFunction(ir.BaseFunction function) {
|
||||
return super._name(function, function.functionName, '');
|
||||
}
|
||||
}
|
||||
|
||||
class _TagNamer extends _Namer<ir.Tag> {
|
||||
_TagNamer(super.onReference);
|
||||
|
||||
String nameTag(ir.Tag tag) {
|
||||
return super._name(tag, null, 'tag');
|
||||
}
|
||||
}
|
||||
|
||||
class _TableNamer extends _Namer<ir.Table> {
|
||||
_TableNamer(super.onReference);
|
||||
|
||||
String nameTable(ir.Table? table) {
|
||||
if (table == null) {
|
||||
return '\$table0';
|
||||
}
|
||||
return super._name(table, null, 'table');
|
||||
}
|
||||
}
|
||||
|
||||
class _TypeNamer extends _Namer<ir.DefType> {
|
||||
_TypeNamer(super.onReference);
|
||||
|
||||
String nameDefType(ir.DefType type) {
|
||||
return super._name(type, type is ir.DataType ? type.name : null, 'type');
|
||||
}
|
||||
}
|
||||
|
||||
class _LocalNamer extends _Namer<int> {
|
||||
final Map<int, String> _namedVariables;
|
||||
|
||||
_LocalNamer(this._namedVariables) : super((_) {});
|
||||
|
||||
String nameLocal(int index) {
|
||||
return super._name(index, _namedVariables[index], 'var');
|
||||
}
|
||||
}
|
||||
|
||||
class _GlobalNamer extends _Namer<ir.Global> {
|
||||
_GlobalNamer(super.onReference);
|
||||
|
||||
String nameGlobal(ir.Global global) {
|
||||
String? gn = global.globalName;
|
||||
if (gn == null && global is ir.ImportedGlobal) {
|
||||
gn = '${global.module}.${global.name}';
|
||||
}
|
||||
return super._name(global, gn, 'global');
|
||||
}
|
||||
}
|
||||
|
||||
class _LabelNamer {
|
||||
int _nextId = 0;
|
||||
final stack = <LabelInfo>[];
|
||||
_LabelNamer();
|
||||
|
||||
String nameLabel(int n, {bool use = true}) {
|
||||
final index = stack.length - 1 - n;
|
||||
final info = stack[index];
|
||||
if (use) info.used = true;
|
||||
return info.name ??= '\$label${_nextId++}';
|
||||
}
|
||||
}
|
||||
|
||||
class LabelInfo {
|
||||
// This is optional as the function (body) itself introduces a label that a
|
||||
// branch instruction can break to (aka return).
|
||||
final ir.Instruction? target;
|
||||
|
||||
String? name;
|
||||
bool used = false;
|
||||
|
||||
LabelInfo(this.target);
|
||||
}
|
||||
|
||||
String _escapeString(String s) {
|
||||
final units = s.codeUnits;
|
||||
final sb = StringBuffer();
|
||||
int startIndex = 0;
|
||||
while (startIndex < units.length) {
|
||||
int endIndex = units.length;
|
||||
int endUnit = 0;
|
||||
for (int i = startIndex; i < units.length; ++i) {
|
||||
final unit = units[i];
|
||||
if (unit < 0x20 ||
|
||||
0x7e < unit ||
|
||||
unit == _backslash ||
|
||||
unit == _doubleQuote) {
|
||||
endUnit = unit;
|
||||
endIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
sb.write(s.substring(startIndex, endIndex));
|
||||
startIndex = endIndex + 1;
|
||||
if (endIndex < units.length) {
|
||||
if (endUnit == _backslash) {
|
||||
sb.write('\\\\');
|
||||
continue;
|
||||
}
|
||||
if (endUnit == _doubleQuote) {
|
||||
sb.write('\\"');
|
||||
continue;
|
||||
}
|
||||
if (endUnit == _newline) {
|
||||
sb.write('\\n');
|
||||
continue;
|
||||
}
|
||||
if (endUnit == _cr) {
|
||||
sb.write('\\r');
|
||||
continue;
|
||||
}
|
||||
if (endUnit == _tab) {
|
||||
sb.write('\\t');
|
||||
continue;
|
||||
}
|
||||
if (endUnit.isLeadSurrogate) {
|
||||
if ((endIndex + 1) < units.length) {
|
||||
final tail = units[endIndex + 1];
|
||||
if (tail.isTailSurrogate) {
|
||||
startIndex++;
|
||||
sb.writeEscapedPairedRune(endUnit, tail);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
sb.writeEscapedUnpairedRune(endUnit);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return '$sb';
|
||||
}
|
||||
|
||||
String _sanitizeName(String s) {
|
||||
final units = s.codeUnits;
|
||||
for (int i = 0; i < units.length; ++i) {
|
||||
final unit = units[i];
|
||||
final sanitizedUnit = _nameAsciiMapping[unit & 0x7f];
|
||||
if (unit != sanitizedUnit) {
|
||||
return '''"${_escapeString(s)}"''';
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
final Uint8List _nameAsciiMapping = (() {
|
||||
// Names are only allowed to have certain characters in them in the wat
|
||||
// format. See allowed characters at
|
||||
// https://webassembly.github.io/spec/core/text/values.html#text-id
|
||||
final map = Uint8List(128);
|
||||
for (int unit = 'a'.ordinal; unit <= 'z'.ordinal; ++unit) {
|
||||
map[unit] = unit;
|
||||
}
|
||||
for (int unit = 'A'.ordinal; unit <= 'Z'.ordinal; ++unit) {
|
||||
map[unit] = unit;
|
||||
}
|
||||
for (int unit = '0'.ordinal; unit <= '9'.ordinal; ++unit) {
|
||||
map[unit] = unit;
|
||||
}
|
||||
map['!'.ordinal] = '!'.ordinal;
|
||||
map['#'.ordinal] = '#'.ordinal;
|
||||
map['\$'.ordinal] = '\$'.ordinal;
|
||||
map['%'.ordinal] = '%'.ordinal;
|
||||
map['&'.ordinal] = '&'.ordinal;
|
||||
map['\''.ordinal] = '\''.ordinal;
|
||||
map['*'.ordinal] = '*'.ordinal;
|
||||
map['+'.ordinal] = '+'.ordinal;
|
||||
map['-'.ordinal] = '-'.ordinal;
|
||||
map['.'.ordinal] = '.'.ordinal;
|
||||
map['/'.ordinal] = '/'.ordinal;
|
||||
map[':'.ordinal] = ':'.ordinal;
|
||||
map['<'.ordinal] = '<'.ordinal;
|
||||
map['='.ordinal] = '='.ordinal;
|
||||
map['>'.ordinal] = '>'.ordinal;
|
||||
map['?'.ordinal] = '?'.ordinal;
|
||||
map['@'.ordinal] = '@'.ordinal;
|
||||
map['\\'.ordinal] = '\\'.ordinal;
|
||||
map['^'.ordinal] = '^'.ordinal;
|
||||
map['_'.ordinal] = '_'.ordinal;
|
||||
map['`'.ordinal] = '`'.ordinal;
|
||||
map['|'.ordinal] = '|'.ordinal;
|
||||
map['~'.ordinal] = '~'.ordinal;
|
||||
return map;
|
||||
})();
|
||||
|
||||
extension on String {
|
||||
int get ordinal => codeUnitAt(0);
|
||||
}
|
||||
|
||||
extension on StringBuffer {
|
||||
void writeEscapedUnpairedRune(int rune) {
|
||||
if (rune < 0x7ff) {
|
||||
writeHex(0xC0 | (rune >> 6));
|
||||
writeHex(0x80 | (rune & 0x3f));
|
||||
return;
|
||||
}
|
||||
writeHex(0xE0 | (rune >> 12));
|
||||
writeHex(0x80 | ((rune >> 6) & 0x3f));
|
||||
writeHex(0x80 | (rune & 0x3f));
|
||||
}
|
||||
|
||||
void writeEscapedPairedRune(int leadSurrogate, int tailSurrogate) {
|
||||
final rune = _combineSurrogatePair(leadSurrogate, tailSurrogate);
|
||||
writeHex(0xF0 | (rune >> 18));
|
||||
writeHex(0x80 | ((rune >> 12) & 0x3f));
|
||||
writeHex(0x80 | ((rune >> 6) & 0x3f));
|
||||
writeHex(0x80 | (rune & 0x3f));
|
||||
}
|
||||
|
||||
void writeHex(int value) {
|
||||
write('\\');
|
||||
write((value >> 4).toRadixString(16));
|
||||
write((value & 0xf).toRadixString(16));
|
||||
}
|
||||
}
|
||||
|
||||
extension on int {
|
||||
bool get isLeadSurrogate => (this & 0xFC00) == 0xD800;
|
||||
bool get isTailSurrogate => (this & 0xFC00) == 0xDC00;
|
||||
}
|
||||
|
||||
int _combineSurrogatePair(int lead, int tail) {
|
||||
return 0x10000 + ((lead & 0x3ff) << 10) + (tail & 0x3ff);
|
||||
}
|
||||
|
||||
const _doubleQuote = 0x22;
|
||||
const _tab = 0x9;
|
||||
const _newline = 0x0a;
|
||||
const _cr = 0xd;
|
||||
const _backslash = 0x5c;
|
||||
Reference in New Issue
Block a user