blob: 4072a3c5031634fe76e4be1e4320b7a9761bdbb3 [file] [edit]
// 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, or `null` if unmapped or `debugInfoTables` was not provided.
Uri? get fileUri {
if (!_hasSourcePosition || _debugInfoTables == null) return null;
final files = _debugInfoTables.files;
return _fileIndex < files.length ? files[_fileIndex] : null;
}
/// 0-based line number in the source file.
int get line => _line;
/// 0-based column number in the source file.
int get col => _col;
/// Index of the source file in `DebugInfoTables.files`.
int get fileIndex => _fileIndex;
/// Index of the name in `DebugInfoTables.names`, or `-1` if unnamed.
int get nameIndex => _nameIndex;
/// Name of the mapped code (e.g. member name), or `null` if unnamed or `debugInfoTables` was not provided.
String? get name {
if (!_hasSourcePosition || _nameIndex < 0 || _debugInfoTables == null) {
return null;
}
final names = _debugInfoTables.names;
return _nameIndex < names.length ? names[_nameIndex] : null;
}
/// 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;
}