| // 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:collection'; | |
| import 'dart:typed_data'; | |
| import '../ir/ir.dart' as ir; | |
| import 'sections.dart' show ExtraCustomSection; | |
| class ModulePrinter { | |
| final ir.Module _module; | |
| late final typeNamer = TypeNamer( | |
| settings.scrubAbsoluteUris, | |
| _module, | |
| enqueueType, | |
| ); | |
| late final globalNamer = GlobalNamer( | |
| settings.scrubAbsoluteUris, | |
| _module, | |
| enqueueGlobal, | |
| ); | |
| late final functionNamer = FunctionNamer( | |
| settings.scrubAbsoluteUris, | |
| _module, | |
| enqueueFunction, | |
| ); | |
| late final tagNamer = TagNamer( | |
| settings.scrubAbsoluteUris, | |
| _module, | |
| enqueueTag, | |
| ); | |
| late final tableNamer = TableNamer( | |
| settings.scrubAbsoluteUris, | |
| _module, | |
| enqueueTable, | |
| ); | |
| late final dataNamer = DataNamer( | |
| settings.scrubAbsoluteUris, | |
| _module, | |
| enqueueDataSegment, | |
| ); | |
| late final memoryNamer = MemoryNamer( | |
| settings.scrubAbsoluteUris, | |
| _module, | |
| enqueueMemory, | |
| ); | |
| late final extraCustomSectionNamer = ExtraCustomSectionNamer( | |
| settings.scrubAbsoluteUris, | |
| _module, | |
| enqueueExtraCustomSection, | |
| ); | |
| final _types = <ir.DefType, String>{}; | |
| final _tags = <ir.Tag, String>{}; | |
| final _tables = <ir.Table, String>{}; | |
| final _elementSegments = <ir.ElementSegment>{}; | |
| final _activeElementSegments = <ir.Table, Map<int, String>>{}; | |
| final _declarativeElements = <ir.DeclarativeElementSegment, String>{}; | |
| final _globals = <ir.Global, String>{}; | |
| final _functions = <ir.BaseFunction, String>{}; | |
| final _dataSegments = <ir.DataSegment, String>{}; | |
| final _memories = <ir.Memory, String>{}; | |
| final _customSections = <ExtraCustomSection, String>{}; | |
| final _typeQueue = Queue<ir.DefType>(); | |
| final _functionsQueue = Queue<ir.DefinedFunction>(); | |
| /// Closure that tells us whether the body of a function should be printed or | |
| /// not. | |
| final ModulePrintSettings settings; | |
| final _sourceFileCache = <Uri, List<String>?>{}; | |
| ModulePrinter(this._module, {this.settings = const ModulePrintSettings()}); | |
| IrPrinter newIrPrinter() => IrPrinter._( | |
| settings.preferMultiline, | |
| settings.printSourcePositions, | |
| settings.printUrl, | |
| settings.scrubAbsoluteUris, | |
| settings.sourceFileProvider, | |
| _sourceFileCache, | |
| _module, | |
| typeNamer, | |
| globalNamer, | |
| functionNamer, | |
| tagNamer, | |
| tableNamer, | |
| dataNamer, | |
| memoryNamer, | |
| ); | |
| void enqueueType(ir.DefType type) { | |
| if (!_types.containsKey(type)) { | |
| _types[type] = ''; | |
| _generateDefType( | |
| type, | |
| includeConstituents: settings.printTypeConstituents( | |
| typeNamer.name(type, activateOnReferenceCallback: false), | |
| ), | |
| ); | |
| } | |
| } | |
| void enqueueGlobal(ir.Global global) { | |
| if (!_globals.containsKey(global)) { | |
| _globals[global] = ''; | |
| _generateGlobal( | |
| global, | |
| includeInitializer: settings.printGlobalInitializer( | |
| globalNamer.name(global, activateOnReferenceCallback: false), | |
| ), | |
| ); | |
| } | |
| } | |
| 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 enqueueMemory(ir.Memory memory) { | |
| if (!_memories.containsKey(memory)) { | |
| _memories[memory] = ''; | |
| _generateMemory(memory); | |
| } | |
| } | |
| void enqueueTable(ir.Table table) { | |
| if (!_tables.containsKey(table)) { | |
| _tables[table] = ''; | |
| _generateTable(table); | |
| for (final segment in _module.elements.segments) { | |
| if (segment is ir.ActiveElementSegment && segment.table == table) { | |
| enqueueElementSegment(segment); | |
| } | |
| } | |
| } | |
| } | |
| void enqueueElementSegment(ir.ElementSegment segment) { | |
| if (_elementSegments.add(segment)) { | |
| switch (segment) { | |
| case ir.ActiveFunctionElementSegment( | |
| startIndex: final start, | |
| :final table, | |
| :final entries, | |
| ): | |
| final printedEntries = _activeElementSegments.putIfAbsent( | |
| table, | |
| () => {}, | |
| ); | |
| if (settings.printTableElements( | |
| tableNamer.name(segment.table, activateOnReferenceCallback: false), | |
| )) { | |
| for (int i = 0; i < entries.length; ++i) { | |
| final index = start + i; | |
| final ip = newIrPrinter(); | |
| ip.write('('); | |
| ir.RefFunc(entries[i]).printTo(ip); | |
| ip.write(')'); | |
| printedEntries[index] = ip.getText(); | |
| } | |
| } | |
| break; | |
| case ir.ActiveExpressionElementSegment( | |
| startIndex: final start, | |
| :final table, | |
| :final expressions, | |
| ): | |
| final printedEntries = _activeElementSegments.putIfAbsent( | |
| table, | |
| () => {}, | |
| ); | |
| if (settings.printTableElements( | |
| tableNamer.name(segment.table, activateOnReferenceCallback: false), | |
| )) { | |
| for (int i = 0; i < expressions.length; ++i) { | |
| final index = start + i; | |
| final expression = expressions[i]; | |
| final ip = newIrPrinter(); | |
| for (int j = 0; j < expression.length; ++j) { | |
| ip.write(j > 0 ? ' (' : '('); | |
| expression[j].printTo(ip); | |
| ip.write(')'); | |
| } | |
| printedEntries[index] = ip.getText(); | |
| } | |
| } | |
| break; | |
| case ir.DeclarativeElementSegment(:final entries): | |
| final ip = newIrPrinter(); | |
| ip.write('(elem declare'); | |
| if (ip.preferMultiline) { | |
| ip.indent(); | |
| } | |
| for (final entry in entries) { | |
| if (ip.preferMultiline) { | |
| ip.writeln(); | |
| } else { | |
| ip.write(' '); | |
| } | |
| ip.write('('); | |
| ir.RefFunc(entry).printTo(ip); | |
| ip.write(')'); | |
| } | |
| if (ip.preferMultiline) { | |
| ip.deindent(); | |
| } | |
| ip.write(')'); | |
| _declarativeElements[segment] = ip.getText(); | |
| break; | |
| } | |
| } | |
| } | |
| void enqueueDataSegment(ir.DataSegment dataSegment) { | |
| if (!_dataSegments.containsKey(dataSegment)) { | |
| // Since below `printTo` will call namer to name the data segment which | |
| // will trigger this callback again if not pre-initialized to ''. | |
| _dataSegments[dataSegment] = ''; | |
| final ip = newIrPrinter(); | |
| dataSegment.printTo(ip); | |
| _dataSegments[dataSegment] = ip.getText(); | |
| } | |
| } | |
| void enqueueExtraCustomSection(ExtraCustomSection section) { | |
| if (!_customSections.containsKey(section)) { | |
| _customSections[section] = ''; | |
| final ip = newIrPrinter(); | |
| section.printTo(ip); | |
| _customSections[section] = ip.getText(); | |
| } | |
| } | |
| String print() { | |
| while (_functionsQueue.isNotEmpty || _typeQueue.isNotEmpty) { | |
| while (_functionsQueue.isNotEmpty) { | |
| final fun = _functionsQueue.removeFirst(); | |
| _generateFunction( | |
| fun, | |
| includingBody: settings.printFunctionBody( | |
| functionNamer.name(fun, activateOnReferenceCallback: false), | |
| ), | |
| ); | |
| } | |
| } | |
| final mp = IndentPrinter(); | |
| mp.writeln('(module \$${_module.moduleName}'); | |
| mp.withIndent(() { | |
| final groups = _module.types.recursionGroups | |
| .where((group) => group.any(_types.containsKey)) | |
| .toList(); | |
| if (settings.printInSortedOrder) { | |
| groups.sort((a, b) { | |
| final firstA = typeNamer.name( | |
| a.firstWhere( | |
| (t) => !typeNamer | |
| .name(t, activateOnReferenceCallback: false) | |
| .startsWith('\$brand'), | |
| ), | |
| activateOnReferenceCallback: false, | |
| ); | |
| final firstB = typeNamer.name( | |
| b.firstWhere( | |
| (t) => !typeNamer | |
| .name(t, activateOnReferenceCallback: false) | |
| .startsWith('\$brand'), | |
| ), | |
| activateOnReferenceCallback: false, | |
| ); | |
| return firstA.compareTo(firstB); | |
| }); | |
| } | |
| for (final group in groups) { | |
| final filtered = group.where(_types.containsKey).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(')'); | |
| } | |
| } | |
| } | |
| void printOrdered<T>( | |
| List<T> all, | |
| Namer<T> namer, | |
| Map<T, String> enqueued, | |
| ) { | |
| /// Either we print the elements in the name order (defined by a | |
| /// [Namer]) or we print them in same order as they appear in the wasm | |
| /// module. | |
| final filtered = all.where((v) => enqueued.containsKey(v)).toList(); | |
| for (final key | |
| in (settings.printInSortedOrder | |
| ? namer.sort(filtered) | |
| : filtered)) { | |
| mp.write(enqueued[key]!); | |
| mp.writeln(); | |
| } | |
| } | |
| printOrdered(_module.memories.imported, memoryNamer, _memories); | |
| printOrdered(_module.functions.imported, functionNamer, _functions); | |
| printOrdered(_module.globals.imported, globalNamer, _globals); | |
| printOrdered(_module.memories.defined, memoryNamer, _memories); | |
| printOrdered(_module.tables.imported, tableNamer, _tables); | |
| printOrdered(_module.tables.defined, tableNamer, _tables); | |
| printOrdered(_module.tags.imported, tagNamer, _tags); | |
| printOrdered(_module.tags.defined, tagNamer, _tags); | |
| printOrdered(_module.globals.defined, globalNamer, _globals); | |
| _activeElementSegments.forEach((table, values) { | |
| final ip = newIrPrinter(); | |
| ip.write('(elem '); | |
| ip.writeTableReference(table, alwaysPrint: true); | |
| if (values.isEmpty) { | |
| ip.write(' <...>'); | |
| } else { | |
| ip.withIndent(() { | |
| final indices = values.keys.toList()..sort(); | |
| for (int i = 0; i < indices.length; ++i) { | |
| ip.writeln(); | |
| final index = indices[i]; | |
| final value = values[index]!; | |
| ip.write('(set $index $value)'); | |
| } | |
| }); | |
| } | |
| ip.write(')'); | |
| mp.writeln(ip.getText()); | |
| }); | |
| _declarativeElements.forEach((_, value) { | |
| mp.writeln(value); | |
| }); | |
| printOrdered(_module.functions.defined, functionNamer, _functions); | |
| printOrdered(_module.dataSegments.defined, dataNamer, _dataSegments); | |
| printOrdered( | |
| _module.extraCustomSections, | |
| extraCustomSectionNamer, | |
| _customSections, | |
| ); | |
| }); | |
| mp.write(')'); | |
| return mp.getText(); | |
| } | |
| void _generateTag(ir.Tag tag) { | |
| final p = newIrPrinter(); | |
| tag.printTo(p); | |
| _tags[tag] = p.getText(); | |
| } | |
| void _generateMemory(ir.Memory memory) { | |
| final p = newIrPrinter(); | |
| memory.printTo(p); | |
| _memories[memory] = p.getText(); | |
| } | |
| void _generateTable(ir.Table table) { | |
| final p = newIrPrinter(); | |
| if (table is ir.DefinedTable) { | |
| table.printTo(p); | |
| } else if (table is ir.ImportedTable) { | |
| table.printTo(p); | |
| } else { | |
| return; | |
| } | |
| _tables[table] = p.getText(); | |
| } | |
| void _generateGlobal(ir.Global global, {required bool includeInitializer}) { | |
| final p = newIrPrinter(); | |
| global.printTo(p, includeInitializer: includeInitializer); | |
| _globals[global] = p.getText(); | |
| } | |
| void _generateImportedFunction(ir.ImportedFunction fun) { | |
| final p = newIrPrinter(); | |
| fun.printTo(p); | |
| _functions[fun] = p.getText(); | |
| } | |
| void _generateFunction( | |
| ir.DefinedFunction fun, { | |
| required bool includingBody, | |
| }) { | |
| final p = newIrPrinter(); | |
| if (includingBody) { | |
| fun.printTo(p); | |
| } else { | |
| fun.printDeclarationTo(p); | |
| } | |
| _functions[fun] = p.getText().trimRight(); | |
| } | |
| void _generateDefType(ir.DefType type, {required bool includeConstituents}) { | |
| final p = newIrPrinter(); | |
| type.printTypeDefTo(p, includeConstituents: includeConstituents); | |
| _types[type] = p.getText(); | |
| } | |
| } | |
| typedef SourceFileProvider = List<String>? Function(Uri uri); | |
| class ModulePrintSettings { | |
| final List<RegExp> functionFilters; | |
| final List<RegExp> tableFilters; | |
| final List<RegExp> globalFilters; | |
| final List<RegExp> typeFilters; | |
| final bool preferMultiline; | |
| final bool scrubAbsoluteUris; | |
| final bool printInSortedOrder; | |
| final bool printSourcePositions; | |
| final bool printUrl; | |
| final SourceFileProvider? sourceFileProvider; | |
| const ModulePrintSettings({ | |
| this.functionFilters = const [], | |
| this.tableFilters = const [], | |
| this.globalFilters = const [], | |
| this.typeFilters = const [], | |
| this.preferMultiline = false, | |
| this.scrubAbsoluteUris = false, | |
| this.printInSortedOrder = false, | |
| this.printSourcePositions = false, | |
| this.printUrl = true, | |
| this.sourceFileProvider, | |
| }); | |
| bool printFunctionBody(String name) { | |
| if (functionFilters.isEmpty) return true; | |
| if (name.isEmpty) return false; | |
| return functionFilters.any((pattern) => name.contains(pattern)); | |
| } | |
| bool printTableElements(String name) { | |
| if (tableFilters.isEmpty) return true; | |
| if (name.isEmpty) return false; | |
| return tableFilters.any((pattern) => name.contains(pattern)); | |
| } | |
| bool printGlobalInitializer(String name) { | |
| if (globalFilters.isEmpty) return true; | |
| if (name.isEmpty) return false; | |
| return globalFilters.any((pattern) => name.contains(pattern)); | |
| } | |
| bool printTypeConstituents(String name) { | |
| if (typeFilters.isEmpty) return true; | |
| if (name.isEmpty) return false; | |
| return typeFilters.any((pattern) => name.contains(pattern)); | |
| } | |
| bool get hasFilters => | |
| functionFilters.isNotEmpty || | |
| tableFilters.isNotEmpty || | |
| globalFilters.isNotEmpty || | |
| typeFilters.isNotEmpty; | |
| } | |
| 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 bool preferMultiline; | |
| final bool printSourcePositions; | |
| final bool _printUrl; | |
| final bool _scrubAbsoluteUris; | |
| final SourceFileProvider? _sourceFileProvider; | |
| final Map<Uri, List<String>?> _sourceFileCache; | |
| final ir.Module module; | |
| final TypeNamer _typeNamer; | |
| final GlobalNamer _globalNamer; | |
| final FunctionNamer _functionNamer; | |
| final TagNamer _tagNamer; | |
| final TableNamer _tableNamer; | |
| final DataNamer _dataNamer; | |
| final MemoryNamer _memoryNamer; | |
| _LocalNamer? _localNamer; | |
| final _labelNamer = _LabelNamer(); | |
| Uri? _lastPrintedFileUri; | |
| int? _lastPrintedLine; | |
| int? _lastPrintedCol; | |
| IrPrinter._( | |
| this.preferMultiline, | |
| this.printSourcePositions, | |
| this._printUrl, | |
| this._scrubAbsoluteUris, | |
| this._sourceFileProvider, | |
| this._sourceFileCache, | |
| this.module, | |
| this._typeNamer, | |
| this._globalNamer, | |
| this._functionNamer, | |
| this._tagNamer, | |
| this._tableNamer, | |
| this._dataNamer, | |
| this._memoryNamer, | |
| ); | |
| /// Returns a new [IrPrinter] with same settings, but empty indentation, | |
| /// empty text content and no local namer. | |
| IrPrinter dup() => IrPrinter._( | |
| preferMultiline, | |
| printSourcePositions, | |
| _printUrl, | |
| _scrubAbsoluteUris, | |
| _sourceFileProvider, | |
| _sourceFileCache, | |
| module, | |
| _typeNamer, | |
| _globalNamer, | |
| _functionNamer, | |
| _tagNamer, | |
| _tableNamer, | |
| _dataNamer, | |
| _memoryNamer, | |
| ); | |
| List<String>? getSourceLines(Uri uri) { | |
| return _sourceFileCache.putIfAbsent( | |
| uri, | |
| () => _sourceFileProvider?.call(uri), | |
| ); | |
| } | |
| void printSourcePosition(Uri fileUri, int line, int col) { | |
| final lineChanged = | |
| fileUri != _lastPrintedFileUri || line != _lastPrintedLine; | |
| final colChanged = col != _lastPrintedCol; | |
| if (!lineChanged && !colChanged) return; | |
| _lastPrintedFileUri = fileUri; | |
| _lastPrintedLine = line; | |
| _lastPrintedCol = col; | |
| final lines = getSourceLines(fileUri); | |
| String filePath = _scrubAbsoluteUris | |
| ? _sanitizeAbsoluteFileUris(fileUri.toString()) | |
| : (fileUri.isScheme('file') | |
| ? fileUri.toFilePath() | |
| : fileUri.toString()); | |
| if (lines != null && line >= 0 && line < lines.length) { | |
| final sourceLine = lines[line]; | |
| final colOffset = col.clamp(0, sourceLine.length); | |
| final before = sourceLine.substring(0, colOffset); | |
| final after = sourceLine.substring(colOffset).trimRight(); | |
| final sourceSnippet = '$before🎯$after'; | |
| write(';; '); | |
| if (_printUrl && lineChanged) { | |
| write(sourceSnippet); | |
| write(' ' * (40 - sourceSnippet.length - 2 * _indent)); | |
| writeln(' $filePath:${line + 1}'); | |
| } else { | |
| writeln(sourceSnippet); | |
| } | |
| } else { | |
| writeln(';; $filePath:${line + 1}:${col + 1}'); | |
| } | |
| } | |
| void printUnmapped() { | |
| if (_lastPrintedFileUri != null) { | |
| writeln(';; <unmapped>'); | |
| } | |
| _lastPrintedFileUri = null; | |
| _lastPrintedLine = null; | |
| _lastPrintedCol = null; | |
| } | |
| 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!.name(local.index)); | |
| } | |
| void writeLocalIndexReference(int localIndex) { | |
| write(_localNamer!.name(localIndex)); | |
| } | |
| void withLocalNames(Map<int, String> names, void Function() fun) { | |
| _localNamer = _LocalNamer( | |
| _functionNamer._scrubAbsoluteFileUris, | |
| module, | |
| 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.name(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.name(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.name(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.name(global)); | |
| } | |
| void writeFunctionReference(ir.BaseFunction function) { | |
| write(_functionNamer.name(function)); | |
| } | |
| void writeTagReference(ir.Tag tag) { | |
| write(_tagNamer.name(tag)); | |
| } | |
| void writeDataReference(ir.DataSegment dataSegment) { | |
| write(_dataNamer.name(dataSegment)); | |
| } | |
| void writeMemoryReference(ir.Memory memory) { | |
| write(_memoryNamer.name(memory)); | |
| } | |
| } | |
| abstract class Namer<T> { | |
| final ir.Module _module; | |
| final bool _scrubAbsoluteFileUris; | |
| int _nextId = 0; | |
| final Map<T, String> _names = {}; | |
| final void Function(T) _onReference; | |
| late final Map<ir.Exportable, String> _exportNames = (() { | |
| final map = <ir.Exportable, String>{}; | |
| for (final export in _module.exports.exported) { | |
| final ir.Exportable? key = switch (export) { | |
| ir.TableExport(:var table) => table, | |
| ir.TagExport(:var tag) => tag, | |
| ir.GlobalExport(:var global) => global, | |
| ir.MemoryExport(:var memory) => memory, | |
| ir.FunctionExport(:var function) => function, | |
| _ => null, | |
| }; | |
| if (key != null) { | |
| map[key] = export.name; | |
| } | |
| } | |
| return map; | |
| })(); | |
| Namer(this._scrubAbsoluteFileUris, this._module, this._onReference); | |
| String name(T key, {bool activateOnReferenceCallback = true}); | |
| String _name( | |
| T key, | |
| String? name, | |
| String unnamedPrefix, | |
| bool activateOnReferenceCallback, | |
| ) { | |
| final existing = _names[key]; | |
| if (existing != null) return existing; | |
| if (activateOnReferenceCallback) { | |
| _onReference(key); | |
| } | |
| if (name == null) { | |
| if (key is ir.Import) { | |
| name = '${key.module}.${key.name}'; | |
| } else if (key is ir.Exportable) { | |
| name = _exportNames[key]; | |
| } | |
| } | |
| if (name != null && _scrubAbsoluteFileUris) { | |
| name = _sanitizeAbsoluteFileUris(name); | |
| } | |
| final sanitizedName = name != null | |
| ? _sanitizeName(name) | |
| : '$unnamedPrefix${activateOnReferenceCallback ? _nextId++ : 0}'; | |
| final quotedName = '\$$sanitizedName'; | |
| return activateOnReferenceCallback | |
| ? _names[key] ??= quotedName | |
| : quotedName; | |
| } | |
| List<T> filter(List<T> values, bool Function(String) filter) => values | |
| .where((value) => filter(name(value, activateOnReferenceCallback: false))) | |
| .toList(); | |
| List<T> sort(List<T> values) => values.toList() | |
| ..sort((a, b) { | |
| return name( | |
| a, | |
| activateOnReferenceCallback: false, | |
| ).compareTo(name(b, activateOnReferenceCallback: false)); | |
| }); | |
| } | |
| class FunctionNamer extends Namer<ir.BaseFunction> { | |
| FunctionNamer(super.scrubAbsoluteUris, super.module, super.onReference); | |
| @override | |
| String name( | |
| ir.BaseFunction function, { | |
| bool activateOnReferenceCallback = true, | |
| }) { | |
| return super._name( | |
| function, | |
| function.functionName, | |
| '', | |
| activateOnReferenceCallback, | |
| ); | |
| } | |
| } | |
| class TagNamer extends Namer<ir.Tag> { | |
| TagNamer(super.scrubAbsoluteUris, super.module, super.onReference); | |
| @override | |
| String name(ir.Tag tag, {bool activateOnReferenceCallback = true}) { | |
| return super._name(tag, null, 'tag', activateOnReferenceCallback); | |
| } | |
| } | |
| class TableNamer extends Namer<ir.Table> { | |
| TableNamer(super.scubUris, super.module, super.onReference); | |
| @override | |
| String name(ir.Table? table, {bool activateOnReferenceCallback = true}) { | |
| table ??= _module.tables.defined.first; | |
| // Try to use cache first to avoid O(n) scan in the exports. | |
| final existing = _names[table]; | |
| if (existing != null) return existing; | |
| final prefix = table is ir.ImportedTable ? 'itable' : 'dtable'; | |
| return super._name(table, null, prefix, activateOnReferenceCallback); | |
| } | |
| } | |
| class TypeNamer extends Namer<ir.DefType> { | |
| TypeNamer(super.scrubAbsoluteUris, super.module, super.onReference); | |
| @override | |
| String name(ir.DefType type, {bool activateOnReferenceCallback = true}) { | |
| return super._name( | |
| type, | |
| type is ir.DataType ? type.name : null, | |
| 'type', | |
| activateOnReferenceCallback, | |
| ); | |
| } | |
| } | |
| class _LocalNamer extends Namer<int> { | |
| final Map<int, String> _namedVariables; | |
| _LocalNamer(bool scrubAbsoluteUris, ir.Module module, this._namedVariables) | |
| : super(scrubAbsoluteUris, module, (_) {}); | |
| @override | |
| String name(int index, {bool activateOnReferenceCallback = true}) { | |
| return super._name( | |
| index, | |
| _namedVariables[index], | |
| 'var', | |
| activateOnReferenceCallback, | |
| ); | |
| } | |
| } | |
| class GlobalNamer extends Namer<ir.Global> { | |
| GlobalNamer(super.scrubAbsoluteUris, super.module, super.onReference); | |
| @override | |
| String name(ir.Global global, {bool activateOnReferenceCallback = true}) { | |
| String? gn = global.globalName; | |
| if (gn == null && global is ir.ImportedGlobal) { | |
| gn = '${global.module}.${global.name}'; | |
| } | |
| return super._name(global, gn, 'global', activateOnReferenceCallback); | |
| } | |
| } | |
| class DataNamer extends Namer<ir.DataSegment> { | |
| DataNamer(super.scubUris, super.module, super.onReference); | |
| @override | |
| String name(ir.DataSegment data, {bool activateOnReferenceCallback = true}) { | |
| return super._name(data, null, 'data', true); | |
| } | |
| } | |
| class MemoryNamer extends Namer<ir.Memory> { | |
| MemoryNamer(super.scubUris, super.module, super.onReference); | |
| @override | |
| String name(ir.Memory memory, {bool activateOnReferenceCallback = true}) { | |
| return super._name( | |
| memory, | |
| memory is ir.ImportedMemory ? '${memory.module}.${memory.name}' : null, | |
| 'memory', | |
| activateOnReferenceCallback, | |
| ); | |
| } | |
| } | |
| class ExtraCustomSectionNamer extends Namer<ExtraCustomSection> { | |
| ExtraCustomSectionNamer(super.scubUris, super.module, super.onReference); | |
| @override | |
| String name( | |
| ExtraCustomSection section, { | |
| bool activateOnReferenceCallback = true, | |
| }) { | |
| return super._name( | |
| section, | |
| section.name, | |
| 'extraCustomSection', | |
| activateOnReferenceCallback, | |
| ); | |
| } | |
| } | |
| 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 _sanitizeAbsoluteFileUris(String name) { | |
| int globalStart = 0; | |
| while (true) { | |
| final start = name.indexOf('file:///', globalStart); | |
| if (start < 0) { | |
| break; | |
| } | |
| final end = name.indexOf('.dart', start); | |
| if (end < 0) { | |
| break; | |
| } | |
| final uri = name.substring(start, end); | |
| final slash = uri.lastIndexOf('/'); | |
| final first = name.substring(0, start); | |
| final filename = name.substring(start + slash + 1, end); | |
| final last = name.substring(end + '.dart'.length); | |
| name = '${first}file:///.../$filename.dart$last'; | |
| globalStart = name.length - last.length; | |
| } | |
| return name; | |
| } | |
| 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; |