| // Copyright (c) 2026, 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. | |
| /// Each wasm module is represented by a `ir.Module` instance which refers to | |
| /// everything in it, including `ir.Instructions` that represent bodies of | |
| /// functions (as well as initializer expressions for globals and tables). | |
| /// | |
| /// The `ir.Instructions` include debug information that allows one to map each | |
| /// wasm instruction to a source file, line and column (and possibly other state | |
| /// in the future). | |
| /// | |
| /// To represent this in a memory efficient way, we use a compact bytecode-like | |
| /// encoding of this debug information in a `Uint8List` next to the main | |
| /// instruction stream. | |
| /// | |
| /// One can read the instruction stream and the debug information in parallel to | |
| /// obtain debug info for a particular instruction (the [DebugInfoReader] can be | |
| /// andvanced until the corresponding offset of the instruction of interest). | |
| /// | |
| /// When serializing the `ir.Module` and it's `ir.Instructions` to a binary we | |
| /// inform a [DebugInfoSerializer] about offsets of the instructions as well as | |
| /// the debug info where offsets are byte offsets (rather than instruction | |
| /// offsets). | |
| /// | |
| /// When deserializing into an `ir.Module` we inform a [DebugInfoDeserializer] | |
| /// about function and instruction offsets, allowing it to build debug info | |
| /// while decoding the binary which will then be attached to the corresponding | |
| /// `ir.Instructions` object. | |
| library; | |
| import 'dart:typed_data'; | |
| /// Information about source files and symbol names referenced in a Wasm module. | |
| class DebugInfoTables { | |
| final Map<Uri, int> _fileIndices = {}; | |
| final List<Uri> files = []; | |
| final Map<String, int> _nameIndices = {}; | |
| final List<String> names = []; | |
| int getFileIndex(Uri uri) => | |
| _fileIndices[uri] ??= (files..add(uri)).length - 1; | |
| int getNameIndex(String name) => | |
| _nameIndices[name] ??= (names..add(name)).length - 1; | |
| } | |
| /// Interface for streaming debug info decoders used during module deserialization. | |
| abstract interface class DebugInfoDeserializer { | |
| DebugInfoTables? get debugInfoTables; | |
| /// Called before deserializing instructions of a function. | |
| /// [fileOffset] is the file offset where the function's instructions begin. | |
| void startFunction(int fileOffset); | |
| /// Called when an instruction is about to be deserialized. | |
| /// [instructionIndex] is the 0-based index of this instruction in the function. | |
| /// [fileOffset] is the exact file offset of this instruction in the Wasm binary. | |
| void onInstruction(int instructionIndex, int fileOffset); | |
| /// Called when instruction deserialization for the current function ends. | |
| /// [fileOffset] is the file offset where the function's instructions end. | |
| /// Returns the instruction-offset based bytecode for `ir.Instructions.debugInfo`. | |
| Uint8List? endFunction(int fileOffset); | |
| } | |
| /// Interface for building debug info during module serialization. | |
| abstract interface class DebugInfoSerializer { | |
| /// Adds function debug info with byte offsets relative to the start of the | |
| /// function's instructions. | |
| /// [functionCodeOffset] is the byte offset of the function's instructions | |
| /// within the Code section payload. | |
| /// [byteDebugInfo] is the byte-offset based bytecode for the function. | |
| void addFunction(int functionCodeOffset, Uint8List byteDebugInfo); | |
| /// Called after the Code section is serialized and its payload [fileOffset] | |
| /// in the Wasm binary is known. | |
| void setCodeSectionFileOffset(int fileOffset); | |
| } | |
| /// Compact mapping of wasm instruction or byte offsets to debug information. | |
| /// | |
| /// The `offset`s are | |
| /// | |
| /// * instruction offsets for debug infos installed on the in-memory wasm module | |
| /// representation (i.e. `ir.Instructions` of the `ir.Module`s). | |
| /// | |
| /// * byte offsets for debug infos during serialization when given to | |
| /// [DebugInfoSerializer]. | |
| /// | |
| /// For more details, see library documentation. | |
| class DebugInfoReader { | |
| final Uint8List _bytes; | |
| final DebugInfoTables? _debugInfoTables; | |
| int _byteOffset = 0; | |
| int _offset = 0; | |
| int _fileIndex = 0; | |
| int _line = 0; | |
| int _col = 0; | |
| int _nameIndex = -1; | |
| bool _hasSourcePosition = false; | |
| DebugInfoReader(this._bytes, [this._debugInfoTables]); | |
| /// The offset of the current mapping. | |
| /// | |
| /// The offset may represent an instruction offset in the body of a function | |
| /// or a byte offset (depending on usage). | |
| int get offset => _offset; | |
| /// Whether the code at [offset] has a source position. | |
| bool get hasSourcePosition => _hasSourcePosition; | |
| /// File URI of the mapped source. | |
| Uri get fileUri { | |
| assert(_hasSourcePosition); | |
| return _debugInfoTables!.files[_fileIndex]; | |
| } | |
| /// 0-based line number in the source file. | |
| int get line { | |
| assert(_hasSourcePosition); | |
| return _line; | |
| } | |
| /// 0-based column number in the source file. | |
| int get col { | |
| assert(_hasSourcePosition); | |
| return _col; | |
| } | |
| /// Index of the source file in `DebugInfoTables.files`. | |
| int get fileIndex { | |
| assert(_hasSourcePosition); | |
| return _fileIndex; | |
| } | |
| /// Index of the name in `DebugInfoTables.names`, or `-1` if unnamed. | |
| int get nameIndex { | |
| assert(_hasSourcePosition); | |
| return _nameIndex; | |
| } | |
| /// Name of the mapped code (if available). | |
| String? get name { | |
| assert(_hasSourcePosition); | |
| if (_nameIndex < 0) return null; | |
| return _debugInfoTables!.names[_nameIndex]; | |
| } | |
| /// Advances to the last source mapping with an offset `<= [targetOffset]`. | |
| /// | |
| /// Returns `true` if the reader advanced to a new source mapping. | |
| bool moveUntil(int targetOffset) { | |
| bool moved = false; | |
| while (true) { | |
| final byteOffset = _byteOffset; | |
| final offset = _offset; | |
| final fileIndex = _fileIndex; | |
| final line = _line; | |
| final col = _col; | |
| final nameIndex = _nameIndex; | |
| final hasSourcePosition = _hasSourcePosition; | |
| if (!moveNext()) return moved; | |
| if (_offset > targetOffset) { | |
| _byteOffset = byteOffset; | |
| _offset = offset; | |
| _fileIndex = fileIndex; | |
| _line = line; | |
| _col = col; | |
| _nameIndex = nameIndex; | |
| _hasSourcePosition = hasSourcePosition; | |
| return moved; | |
| } | |
| moved = true; | |
| } | |
| } | |
| /// Advances to the next source mapping. Returns `false` if there are no more | |
| /// mappings. | |
| bool moveNext() { | |
| if (_byteOffset >= _bytes.length) return false; | |
| final op = _bytes[_byteOffset++]; | |
| if (op < _Opcode.specialSameLineMax) { | |
| final deltaOffset = op >> 4; | |
| final deltaCol = (op & 0x0F) - _SpecialOp.sameLineColBias; | |
| _offset += deltaOffset; | |
| _col += deltaCol; | |
| _hasSourcePosition = true; | |
| return true; | |
| } else if (op < _Opcode.specialNextLineMax) { | |
| final adj = op - _Opcode.specialNextLineBase; | |
| final deltaOffset = adj >> 3; | |
| final deltaCol = (adj & 0x07) - _SpecialOp.nextLineColBias; | |
| _offset += deltaOffset; | |
| _line += 1; | |
| _col += deltaCol; | |
| _hasSourcePosition = true; | |
| return true; | |
| } | |
| switch (op) { | |
| case _Opcode.changeLineCol: | |
| _offset += _readVarint(); | |
| _line += _readZigZag(); | |
| _col += _readZigZag(); | |
| _hasSourcePosition = true; | |
| return true; | |
| case _Opcode.setUnmapped: | |
| _offset += _readVarint(); | |
| _hasSourcePosition = false; | |
| return true; | |
| case _Opcode.setAll: | |
| _offset += _readVarint(); | |
| _fileIndex = _readVarint(); | |
| _line = _readVarint(); | |
| _col = _readVarint(); | |
| final rawName = _readVarint(); | |
| _nameIndex = rawName == 0 ? -1 : rawName - 1; | |
| _hasSourcePosition = true; | |
| return true; | |
| default: | |
| throw StateError('Invalid debug info bytecode opcode: $op'); | |
| } | |
| } | |
| int _readVarint() { | |
| int result = 0; | |
| int shift = 0; | |
| while (true) { | |
| final byte = _bytes[_byteOffset++]; | |
| result |= (byte & 0x7F) << shift; | |
| if ((byte & 0x80) == 0) break; | |
| shift += 7; | |
| } | |
| return result; | |
| } | |
| int _readZigZag() { | |
| final value = _readVarint(); | |
| return (value >>> 1) ^ -(value & 1); | |
| } | |
| } | |
| /// Compact mapping of wasm instructions to debug information. | |
| class DebugInfoWriter { | |
| static final Uint8List _emptyBuffer = Uint8List(0); | |
| static const int _initialCapacity = 32; | |
| final DebugInfoTables? debugInfoTables; | |
| Uint8List _buffer = _emptyBuffer; | |
| int _length = 0; | |
| // Last flushed entry (baseline for delta encoding). | |
| int _lastOffset = -1; | |
| int _lastFileIndex = 0; | |
| int _lastLine = 0; | |
| int _lastCol = 0; | |
| int _lastNameIndex = -1; | |
| bool _lastHasSourcePosition = false; | |
| // Pending entry (not yet encoded). | |
| bool _hasPending = false; | |
| int _pendingOffset = -1; | |
| int _pendingFileIndex = 0; | |
| int _pendingLine = 0; | |
| int _pendingCol = 0; | |
| int _pendingNameIndex = -1; | |
| bool _pendingHasSourcePosition = false; | |
| Uint8List? _built; | |
| DebugInfoWriter([this.debugInfoTables]); | |
| bool get isEmpty => | |
| _built != null ? _built!.isEmpty : (_length == 0 && !_hasPending); | |
| void _addByte(int byte) { | |
| if (_length == _buffer.length) { | |
| final newCapacity = _buffer.isEmpty | |
| ? _initialCapacity | |
| : _buffer.length * 2; | |
| final newBuffer = Uint8List(newCapacity); | |
| if (_length > 0) { | |
| newBuffer.setRange(0, _length, _buffer); | |
| } | |
| _buffer = newBuffer; | |
| } | |
| _buffer[_length++] = byte; | |
| } | |
| /// Sets the source position for the code at [offset]. | |
| /// | |
| /// During IR construction, [offset] is the 0-based instruction index in the | |
| /// instruction sequence. During serialization, [offset] is the byte offset | |
| /// in the serializer / section / Wasm module. | |
| void setSourcePosition( | |
| int offset, | |
| Uri fileUri, | |
| int line, | |
| int col, | |
| String? name, | |
| ) { | |
| final int fileIndex = debugInfoTables!.getFileIndex(fileUri); | |
| final int nameIndex = name != null | |
| ? debugInfoTables!.getNameIndex(name) | |
| : -1; | |
| setSourcePositionWithIndices(offset, fileIndex, line, col, nameIndex); | |
| } | |
| /// Sets the source position for the code at [offset] using table indices. | |
| /// | |
| /// During IR construction, [offset] is the 0-based instruction index in the | |
| /// instruction sequence. During serialization, [offset] is the byte offset | |
| /// in the serializer / section / Wasm module. | |
| void setSourcePositionWithIndices( | |
| int offset, | |
| int fileIndex, | |
| int line, | |
| int col, | |
| int nameIndex, | |
| ) { | |
| if (_hasPending) { | |
| if (offset == _pendingOffset) { | |
| // Overwrite pending entry at the same offset. | |
| _pendingFileIndex = fileIndex; | |
| _pendingLine = line; | |
| _pendingCol = col; | |
| _pendingNameIndex = nameIndex; | |
| _pendingHasSourcePosition = true; | |
| return; | |
| } | |
| assert(offset > _pendingOffset); | |
| _flushPending(); | |
| } | |
| _hasPending = true; | |
| _pendingOffset = offset; | |
| _pendingFileIndex = fileIndex; | |
| _pendingLine = line; | |
| _pendingCol = col; | |
| _pendingNameIndex = nameIndex; | |
| _pendingHasSourcePosition = true; | |
| } | |
| /// Clears the source position for the code at [offset], marking it unmapped. | |
| /// | |
| /// During IR construction, [offset] is the 0-based instruction index in the | |
| /// instruction sequence. During serialization, [offset] is the byte offset | |
| /// in the serializer / section / Wasm module. | |
| void clearSourcePosition(int offset) { | |
| if (_hasPending) { | |
| if (offset == _pendingOffset) { | |
| _pendingHasSourcePosition = false; | |
| return; | |
| } | |
| assert(offset > _pendingOffset); | |
| _flushPending(); | |
| } | |
| _hasPending = true; | |
| _pendingOffset = offset; | |
| _pendingHasSourcePosition = false; | |
| } | |
| void _flushPending() { | |
| if (!_hasPending) return; | |
| _hasPending = false; | |
| // Check if this pending entry is redundant compared to last flushed entry. | |
| if (_lastOffset != -1) { | |
| if (_lastHasSourcePosition == _pendingHasSourcePosition) { | |
| if (!_pendingHasSourcePosition) return; // Both unmapped. | |
| if (_lastFileIndex == _pendingFileIndex && | |
| _lastLine == _pendingLine && | |
| _lastCol == _pendingCol && | |
| _lastNameIndex == _pendingNameIndex) { | |
| return; // Identical source location. | |
| } | |
| } | |
| } | |
| final deltaOffset = _lastOffset == -1 | |
| ? _pendingOffset | |
| : _pendingOffset - _lastOffset; | |
| assert(deltaOffset >= 0); | |
| if (!_pendingHasSourcePosition) { | |
| _addByte(_Opcode.setUnmapped); | |
| _writeVarint(deltaOffset); | |
| } else if (_lastOffset == -1 || | |
| !_lastHasSourcePosition || | |
| _lastFileIndex != _pendingFileIndex || | |
| _lastNameIndex != _pendingNameIndex) { | |
| _addByte(_Opcode.setAll); | |
| _writeVarint(deltaOffset); | |
| _writeVarint(_pendingFileIndex); | |
| _writeVarint(_pendingLine); | |
| _writeVarint(_pendingCol); | |
| _writeVarint(_pendingNameIndex == -1 ? 0 : _pendingNameIndex + 1); | |
| } else { | |
| final deltaLine = _pendingLine - _lastLine; | |
| final deltaCol = _pendingCol - _lastCol; | |
| if (deltaLine == 0 && | |
| deltaOffset <= _SpecialOp.sameLineOffsetMax && | |
| deltaCol >= _SpecialOp.sameLineColMin && | |
| deltaCol <= _SpecialOp.sameLineColMax) { | |
| _addByte((deltaOffset << 4) | (deltaCol + _SpecialOp.sameLineColBias)); | |
| } else if (deltaLine == 1 && | |
| deltaOffset <= _SpecialOp.nextLineOffsetMax && | |
| deltaCol >= _SpecialOp.nextLineColMin && | |
| deltaCol <= _SpecialOp.nextLineColMax) { | |
| _addByte( | |
| _Opcode.specialNextLineBase | | |
| (deltaOffset << 3) | | |
| (deltaCol + _SpecialOp.nextLineColBias), | |
| ); | |
| } else { | |
| _addByte(_Opcode.changeLineCol); | |
| _writeVarint(deltaOffset); | |
| _writeZigZag(deltaLine); | |
| _writeZigZag(deltaCol); | |
| } | |
| } | |
| _lastOffset = _pendingOffset; | |
| _lastFileIndex = _pendingFileIndex; | |
| _lastLine = _pendingLine; | |
| _lastCol = _pendingCol; | |
| _lastNameIndex = _pendingNameIndex; | |
| _lastHasSourcePosition = _pendingHasSourcePosition; | |
| } | |
| void _writeVarint(int value) { | |
| assert(value >= 0); | |
| while (value >= 0x80) { | |
| _addByte((value & 0x7F) | 0x80); | |
| value >>= 7; | |
| } | |
| _addByte(value & 0x7F); | |
| } | |
| void _writeZigZag(int value) { | |
| final encoded = (value << 1) ^ (value >> 63); | |
| _writeVarint(encoded); | |
| } | |
| Uint8List build() { | |
| if (_built != null) return _built!; | |
| _flushPending(); | |
| if (_length == 0) { | |
| return _built = _emptyBuffer; | |
| } | |
| final result = Uint8List(_length); | |
| result.setRange(0, _length, _buffer); | |
| _buffer = _emptyBuffer; | |
| _length = 0; | |
| return _built = result; | |
| } | |
| DebugInfoReader get reader => DebugInfoReader(build(), debugInfoTables); | |
| } | |
| /// Bytecode opcodes for delta-encoded debug information. | |
| abstract final class _Opcode { | |
| /// Opcodes `0x00..0x7F`: 1-byte encoding for `deltaLine == 0`. | |
| static const int specialSameLineMax = 0x80; | |
| /// Opcodes `0x80..0xBF`: 1-byte encoding for `deltaLine == 1`. | |
| static const int specialNextLineBase = 0x80; | |
| static const int specialNextLineMax = 0xC0; | |
| /// Multibyte opcode: `deltaOffset` (varint), `deltaLine` (zigzag), `deltaCol` (zigzag). | |
| static const int changeLineCol = 0xC0; | |
| /// Multibyte opcode: `deltaOffset` (varint). Marks subsequent code unmapped. | |
| static const int setUnmapped = 0xC4; | |
| /// Multibyte opcode: `deltaOffset` (varint), `fileIndex` (varint), `line` (varint), | |
| /// `col` (varint), `nameIndex + 1` (varint, 0 means unnamed). | |
| static const int setAll = 0xC5; | |
| } | |
| /// Encoding parameters and bit layout for 1-byte special opcodes. | |
| abstract final class _SpecialOp { | |
| // deltaLine == 0: 3 bits offset (0..7), 4 bits col (-8..7, biased by +8) | |
| static const int sameLineOffsetMax = 7; | |
| static const int sameLineColBias = 8; | |
| static const int sameLineColMin = -8; | |
| static const int sameLineColMax = 7; | |
| // deltaLine == 1: 3 bits offset (0..7), 3 bits col (-4..3, biased by +4) | |
| static const int nextLineOffsetMax = 7; | |
| static const int nextLineColBias = 4; | |
| static const int nextLineColMin = -4; | |
| static const int nextLineColMax = 3; | |
| } |