[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:
Martin Kustermann
2025-10-15 12:36:50 -07:00
committed by Commit Queue
parent d70aa28821
commit e63dcafc0c
12 changed files with 2390 additions and 2 deletions
+16
View File
@@ -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());
}
+56
View File
@@ -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}');
}
+52
View File
@@ -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";
}
+29
View File
@@ -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,
+25
View File
@@ -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();
}
}
+22
View File
@@ -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.
+2
View File
@@ -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;
}
+23
View File
@@ -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 {
+135 -2
View File
@@ -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;