[beta] [dart2wasm] Generate source maps
This implements generating source maps for the generated Wasm files.
Copying dart2js's command line interface, a source map file with the
name `<program name>.wasm.map` is generated unless `--no-source-maps` is
passed.
When a source map is generated, the generated .wasm file gets a new
section `sourceMappingURL` with the contents `<program name>.wasm.map`.
This section seems to be undocumented, but Chrome and binaryen recognize
it as the URI to the source map file. Chrome is then loads it
automatically in the DevTools.
- `wasm_builder` package is updated with the new `source_map` library,
which describes the source mapping entries.
- `wasm_builder`'s `InstructionsBuilder` is updated with the new public
members:
- `startSourceMapping`: starts mapping the instructions generated to
the given source code.
- `stopSourceMapping`: stops mapping the instructions generated to a
source code. These instructions won't have a mapping in the source
map.
- `CodeGenerator` sets the source file URI and location in the file
when:
- Starting compiling a new member
- Compiling an expression and statement
Bug: https://github.com/dart-lang/sdk/issues/55763
Change-Id: Ieb24796b4b17a735b846793617664a453f1061ce
Cherry-pick: https://dart-review.googlesource.com/c/sdk/+/370500
Cherry-pick-request: https://github.com/dart-lang/sdk/issues/56239
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/375660
Reviewed-by: Ömer Ağacan <omersa@google.com>
Commit-Queue: Martin Kustermann <kustermann@google.com>
diff --git a/pkg/dart2wasm/lib/closures.dart b/pkg/dart2wasm/lib/closures.dart
index 6b96135..c565f2c 100644
--- a/pkg/dart2wasm/lib/closures.dart
+++ b/pkg/dart2wasm/lib/closures.dart
@@ -947,8 +947,9 @@
class Lambda {
final FunctionNode functionNode;
final w.FunctionBuilder function;
+ final Source functionNodeSource;
- Lambda(this.functionNode, this.function);
+ Lambda(this.functionNode, this.function, this.functionNodeSource);
}
/// The context for one or more closures, containing their captured variables.
@@ -1137,12 +1138,22 @@
int get depth => functionIsSyncStarOrAsync.length - 1;
- CaptureFinder(this.closures, this.member);
+ CaptureFinder(this.closures, this.member)
+ : _currentSource =
+ member.enclosingComponent!.uriToSource[member.fileUri]!;
Translator get translator => closures.translator;
w.ModuleBuilder get m => translator.m;
+ Source _currentSource;
+
+ @override
+ void visitFileUriExpression(FileUriExpression node) {
+ _currentSource = node.enclosingComponent!.uriToSource[node.fileUri]!;
+ super.visitFileUriExpression(node);
+ }
+
@override
void visitFunctionNode(FunctionNode node) {
assert(depth == 0); // Nested function nodes are skipped by [_visitLambda].
@@ -1275,7 +1286,7 @@
functionName = "$member closure $functionNodeName at ${node.location}";
}
final function = m.functions.define(type, functionName);
- closures.lambdas[node] = Lambda(node, function);
+ closures.lambdas[node] = Lambda(node, function, _currentSource);
functionIsSyncStarOrAsync.add(node.asyncMarker == AsyncMarker.SyncStar ||
node.asyncMarker == AsyncMarker.Async);
diff --git a/pkg/dart2wasm/lib/code_generator.dart b/pkg/dart2wasm/lib/code_generator.dart
index 58352c5..5bfa29a 100644
--- a/pkg/dart2wasm/lib/code_generator.dart
+++ b/pkg/dart2wasm/lib/code_generator.dart
@@ -159,6 +159,67 @@
return expectedType;
}
+ Source? _sourceMapSource;
+ int _sourceMapFileOffset = TreeNode.noOffset;
+
+ /// Update the [Source] for the AST nodes being compiled.
+ ///
+ /// The [Source] is used to resolve [TreeNode.fileOffset]s to file URI, line,
+ /// and column numbers, to be able to generate source mappings, in
+ /// [setSourceMapFileOffset].
+ ///
+ /// Setting this `null` disables source mapping for the instructions being
+ /// generated.
+ ///
+ /// This should be called before [setSourceMapFileOffset] as the file offset
+ /// passed to that function is resolved using the [Source].
+ ///
+ /// Returns the old [Source], which can be used to restore the source mapping
+ /// after visiting a sub-tree.
+ Source? setSourceMapSource(Source? source) {
+ final old = _sourceMapSource;
+ _sourceMapSource = source;
+ return old;
+ }
+
+ /// Update the source location of the AST nodes being compiled in the source
+ /// map.
+ ///
+ /// When the offset is [TreeNode.noOffset], this disables mapping the
+ /// generated instructions.
+ ///
+ /// Returns the old file offset, which can be used to restore the source
+ /// mapping after vising a sub-tree.
+ int setSourceMapFileOffset(int fileOffset) {
+ if (!b.recordSourceMaps) {
+ final old = _sourceMapFileOffset;
+ _sourceMapFileOffset = fileOffset;
+ return old;
+ }
+ if (fileOffset == TreeNode.noOffset) {
+ b.stopSourceMapping();
+ final old = _sourceMapFileOffset;
+ _sourceMapFileOffset = fileOffset;
+ return old;
+ }
+ final source = _sourceMapSource!;
+ final fileUri = source.fileUri!;
+ final location = source.getLocation(fileUri, fileOffset);
+ final old = _sourceMapFileOffset;
+ _sourceMapFileOffset = fileOffset;
+ b.startSourceMapping(
+ fileUri, location.line - 1, location.column - 1, member.name.text);
+ return old;
+ }
+
+ /// Calls [setSourceMapSource] and [setSourceMapFileOffset].
+ (Source?, int) setSourceMapSourceAndFileOffset(
+ Source? source, int fileOffset) {
+ final oldSource = setSourceMapSource(source);
+ final oldFileOffset = setSourceMapFileOffset(fileOffset);
+ return (oldSource, oldFileOffset);
+ }
+
/// Generate code for the member.
void generate() {
Member member = this.member;
@@ -208,6 +269,9 @@
return;
}
+ final source = member.enclosingComponent!.uriToSource[member.fileUri]!;
+ setSourceMapSourceAndFileOffset(source, member.fileOffset);
+
if (member is Constructor) {
translator.membersBeingGenerated.add(member);
if (reference.isConstructorBodyReference) {
@@ -484,12 +548,18 @@
for (Field field in info.cls!.fields) {
if (field.isInstanceMember && field.initializer != null) {
+ final source = field.enclosingComponent!.uriToSource[field.fileUri]!;
+ final (oldSource, oldFileOffset) =
+ setSourceMapSourceAndFileOffset(source, field.fileOffset);
+
int fieldIndex = translator.fieldIndex[field]!;
w.Local local = addLocal(info.struct.fields[fieldIndex].type.unpacked);
wrap(field.initializer!, info.struct.fields[fieldIndex].type.unpacked);
b.local_set(local);
fieldLocals[field] = local;
+
+ setSourceMapSourceAndFileOffset(oldSource, oldFileOffset);
}
}
}
@@ -717,6 +787,8 @@
// Initialize closure information from enclosing member.
this.closures = closures;
+ setSourceMapSource(lambda.functionNodeSource);
+
assert(lambda.functionNode.asyncMarker != AsyncMarker.Async);
setupLambdaParametersAndContexts(lambda);
@@ -877,6 +949,15 @@
/// result to the expected type if needed. All expression code generation goes
/// through this method.
w.ValueType wrap(Expression node, w.ValueType expectedType) {
+ var sourceUpdated = false;
+ Source? oldSource;
+ if (node is FileUriNode) {
+ final source =
+ node.enclosingComponent!.uriToSource[(node as FileUriNode).fileUri]!;
+ oldSource = setSourceMapSource(source);
+ sourceUpdated = true;
+ }
+ final oldFileOffset = setSourceMapFileOffset(node.fileOffset);
try {
w.ValueType resultType = node.accept1(this, expectedType);
translator.convertType(function, resultType, expectedType);
@@ -884,15 +965,23 @@
} catch (_) {
_printLocation(node);
rethrow;
+ } finally {
+ if (sourceUpdated) {
+ setSourceMapSource(oldSource);
+ }
+ setSourceMapFileOffset(oldFileOffset);
}
}
void visitStatement(Statement node) {
+ final oldFileOffset = setSourceMapFileOffset(node.fileOffset);
try {
node.accept(this);
} catch (_) {
_printLocation(node);
rethrow;
+ } finally {
+ setSourceMapFileOffset(oldFileOffset);
}
}
diff --git a/pkg/dart2wasm/lib/compile.dart b/pkg/dart2wasm/lib/compile.dart
index 0d2051d..a9652ad 100644
--- a/pkg/dart2wasm/lib/compile.dart
+++ b/pkg/dart2wasm/lib/compile.dart
@@ -33,7 +33,7 @@
show transformComponent;
import 'package:vm/transformations/unreachable_code_elimination.dart'
as unreachable_code_elimination;
-import 'package:wasm_builder/wasm_builder.dart' show Module, Serializer;
+import 'package:wasm_builder/wasm_builder.dart' show Serializer;
import 'compiler_options.dart' as compiler;
import 'constant_evaluator.dart';
@@ -45,18 +45,11 @@
import 'translator.dart';
class CompilerOutput {
- final Module _wasmModule;
+ final Uint8List wasmModule;
final String jsRuntime;
+ final String? sourceMap;
- late final Uint8List wasmModule = _serializeWasmModule();
-
- Uint8List _serializeWasmModule() {
- final s = Serializer();
- _wasmModule.serialize(s);
- return s.data;
- }
-
- CompilerOutput(this._wasmModule, this.jsRuntime);
+ CompilerOutput(this.wasmModule, this.jsRuntime, this.sourceMap);
}
/// Compile a Dart file into a Wasm module.
@@ -64,7 +57,14 @@
/// Returns `null` if an error occurred during compilation. The
/// [handleDiagnosticMessage] callback will have received an error message
/// describing the error.
-Future<CompilerOutput?> compileToModule(compiler.WasmCompilerOptions options,
+///
+/// When generating a source map, `sourceMapUrl` argument should be provided
+/// with the URL of the source map. This value will be added to the Wasm module
+/// in `sourceMappingURL` section. When this argument is null the code
+/// generator does not generate source mappings.
+Future<CompilerOutput?> compileToModule(
+ compiler.WasmCompilerOptions options,
+ Uri? sourceMapUrl,
void Function(DiagnosticMessage) handleDiagnosticMessage) async {
var succeeded = true;
void diagnosticMessageHandler(DiagnosticMessage message) {
@@ -205,10 +205,19 @@
depFile);
}
- final wasmModule = translator.translate();
+ final generateSourceMaps = options.translatorOptions.generateSourceMaps;
+ final wasmModule = translator.translate(sourceMapUrl);
+ final serializer = Serializer();
+ wasmModule.serialize(serializer);
+ final wasmModuleSerialized = serializer.data;
+
+ final sourceMap =
+ generateSourceMaps ? serializer.sourceMapSerializer.serialize() : null;
+
String jsRuntime = jsRuntimeFinalizer.generate(
translator.functions.translatedProcedures,
translator.internalizedStringsForJSRuntime,
mode);
- return CompilerOutput(wasmModule, jsRuntime);
+
+ return CompilerOutput(wasmModuleSerialized, jsRuntime, sourceMap);
}
diff --git a/pkg/dart2wasm/lib/dart2wasm.dart b/pkg/dart2wasm/lib/dart2wasm.dart
index d106088..92a2886 100644
--- a/pkg/dart2wasm/lib/dart2wasm.dart
+++ b/pkg/dart2wasm/lib/dart2wasm.dart
@@ -94,6 +94,10 @@
Flag("enable-experimental-ffi",
(o, value) => o.translatorOptions.enableExperimentalFfi = value,
defaultsTo: _d.translatorOptions.enableExperimentalFfi),
+ // Use same flag with dart2js for disabling source maps.
+ Flag("no-source-maps",
+ (o, value) => o.translatorOptions.generateSourceMaps = !value,
+ defaultsTo: !_d.translatorOptions.generateSourceMaps),
];
Map<fe.ExperimentalFlag, bool> processFeExperimentalFlags(
diff --git a/pkg/dart2wasm/lib/generate_wasm.dart b/pkg/dart2wasm/lib/generate_wasm.dart
index c0e2315..174fd7a 100644
--- a/pkg/dart2wasm/lib/generate_wasm.dart
+++ b/pkg/dart2wasm/lib/generate_wasm.dart
@@ -1,6 +1,7 @@
import 'dart:io';
import 'package:front_end/src/api_unstable/vm.dart' show printDiagnosticMessage;
+import 'package:path/path.dart' as path;
import 'compile.dart';
import 'compiler_options.dart';
@@ -18,10 +19,16 @@
print(' - librariesSpecPath = ${options.librariesSpecPath}');
print(' - packagesPath file = ${options.packagesPath}');
print(' - platformPath file = ${options.platformPath}');
+ print(
+ ' - generate source maps = ${options.translatorOptions.generateSourceMaps}');
}
- CompilerOutput? output = await compileToModule(
- options, (message) => printDiagnosticMessage(message, errorPrinter));
+ final relativeSourceMapUrl = options.translatorOptions.generateSourceMaps
+ ? Uri.file('${path.basename(options.outputFile)}.map')
+ : null;
+
+ CompilerOutput? output = await compileToModule(options, relativeSourceMapUrl,
+ (message) => printDiagnosticMessage(message, errorPrinter));
if (output == null) {
return 1;
@@ -32,8 +39,13 @@
await outFile.writeAsBytes(output.wasmModule);
final jsFile = options.outputJSRuntimeFile ??
- '${options.outputFile.substring(0, options.outputFile.lastIndexOf('.'))}.mjs';
+ path.setExtension(options.outputFile, '.mjs');
await File(jsFile).writeAsString(output.jsRuntime);
+ final sourceMap = output.sourceMap;
+ if (sourceMap != null) {
+ await File('${options.outputFile}.map').writeAsString(sourceMap);
+ }
+
return 0;
}
diff --git a/pkg/dart2wasm/lib/state_machine.dart b/pkg/dart2wasm/lib/state_machine.dart
index 8602bbd..017d2ad 100644
--- a/pkg/dart2wasm/lib/state_machine.dart
+++ b/pkg/dart2wasm/lib/state_machine.dart
@@ -606,6 +606,9 @@
@override
void generate() {
+ final source = member.enclosingComponent!.uriToSource[member.fileUri]!;
+ setSourceMapSource(source);
+ setSourceMapFileOffset(member.fileOffset);
closures = Closures(translator, member);
setupParametersAndContexts(member.reference);
_generateBodies(member.function!);
@@ -614,6 +617,7 @@
@override
w.BaseFunction generateLambda(Lambda lambda, Closures closures) {
this.closures = closures;
+ setSourceMapSource(lambda.functionNodeSource);
setupLambdaParametersAndContexts(lambda);
_generateBodies(lambda.functionNode);
return function;
diff --git a/pkg/dart2wasm/lib/translator.dart b/pkg/dart2wasm/lib/translator.dart
index 525b1d1..75450bd 100644
--- a/pkg/dart2wasm/lib/translator.dart
+++ b/pkg/dart2wasm/lib/translator.dart
@@ -46,6 +46,7 @@
bool verbose = false;
bool enableExperimentalFfi = false;
bool enableExperimentalWasmInterop = false;
+ bool generateSourceMaps = true;
int inliningLimit = 0;
int? sharedMemoryMaxPages;
List<int> watchPoints = [];
@@ -292,8 +293,8 @@
dynamicForwarders = DynamicForwarders(this);
}
- w.Module translate() {
- m = w.ModuleBuilder(watchPoints: options.watchPoints);
+ w.Module translate(Uri? sourceMapUrl) {
+ m = w.ModuleBuilder(sourceMapUrl, watchPoints: options.watchPoints);
voidMarker = w.RefType.def(w.StructType("void"), nullable: true);
// Collect imports and exports as the very first thing so the function types
diff --git a/pkg/dart2wasm/pubspec.yaml b/pkg/dart2wasm/pubspec.yaml
index dfe6f29..8bcbddf 100644
--- a/pkg/dart2wasm/pubspec.yaml
+++ b/pkg/dart2wasm/pubspec.yaml
@@ -15,6 +15,7 @@
kernel: any
vm: any
wasm_builder: any
+ path: any
# Use 'any' constraints here; we get our versions from the DEPS file.
dev_dependencies:
diff --git a/pkg/test_runner/lib/src/test_suite.dart b/pkg/test_runner/lib/src/test_suite.dart
index a2729d0..04229a6 100644
--- a/pkg/test_runner/lib/src/test_suite.dart
+++ b/pkg/test_runner/lib/src/test_suite.dart
@@ -838,6 +838,10 @@
for (var opt in vmOptions)
opt.replaceAll(r'$TEST_COMPILATION_DIR', tempDir)
];
+ for (var i = 0; i < testFile.dart2wasmOptions.length; i += 1) {
+ testFile.dart2wasmOptions[i] = testFile.dart2wasmOptions[i]
+ .replaceAll(r'$TEST_COMPILATION_DIR', tempDir);
+ }
environment['TEST_COMPILATION_DIR'] = tempDir;
compileTimeArguments = compilerConfiguration.computeCompilerArguments(
diff --git a/pkg/wasm_builder/lib/source_map.dart b/pkg/wasm_builder/lib/source_map.dart
new file mode 100644
index 0000000..3eac612
--- /dev/null
+++ b/pkg/wasm_builder/lib/source_map.dart
@@ -0,0 +1,188 @@
+// Copyright (c) 2024, 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.
+
+/// Represents a mapping from a range of generated instructions to some source
+/// code.
+class SourceMapping {
+ /// Start offset of mapped instructions.
+ final int instructionOffset;
+
+ /// Source info for the mapped instructions starting at [instructionOffset].
+ ///
+ /// When `null`, the mapping effectively makes the code unmapped. This is
+ /// useful for compiler-generated code that doesn't correstpond to any lines
+ /// in the source.
+ final SourceInfo? sourceInfo;
+
+ SourceMapping._(this.instructionOffset, this.sourceInfo);
+
+ SourceMapping(
+ this.instructionOffset, Uri fileUri, int line, int col, String? name)
+ : sourceInfo = SourceInfo(fileUri, line, col, name);
+
+ SourceMapping.unmapped(this.instructionOffset) : sourceInfo = null;
+
+ @override
+ String toString() => '$instructionOffset -> $sourceInfo';
+}
+
+class SourceInfo {
+ /// URI of the compiled code's file.
+ final Uri fileUri;
+
+ /// 0-based line number of the compiled code.
+ final int line;
+
+ /// 0-based column number of the compiled code.
+ final int col;
+
+ /// Name of the mapped code. This is usually the name of the function that
+ /// contains the code.
+ final String? name;
+
+ SourceInfo(this.fileUri, this.line, this.col, this.name);
+
+ @override
+ String toString() => '$fileUri:$line:$col ($name)';
+
+ @override
+ bool operator ==(Object other) {
+ if (identical(this, other)) {
+ return true;
+ }
+
+ if (other is! SourceInfo) {
+ return false;
+ }
+
+ return fileUri == other.fileUri &&
+ line == other.line &&
+ col == other.col &&
+ name == other.name;
+ }
+
+ @override
+ int get hashCode => Object.hash(fileUri, line, col, name);
+}
+
+class SourceMapSerializer {
+ final List<SourceMapping> mappings = [];
+
+ void addMapping(int instructionOffset, SourceInfo? sourceInfo) {
+ final mapping = SourceMapping._(instructionOffset, sourceInfo);
+ mappings.add(mapping);
+ }
+
+ void copyMappings(SourceMapSerializer other, int offset) {
+ for (final mapping in other.mappings) {
+ mappings.add(SourceMapping._(
+ mapping.instructionOffset + offset,
+ mapping.sourceInfo,
+ ));
+ }
+ }
+
+ String serialize() => _serializeSourceMap(mappings);
+}
+
+String _serializeSourceMap(List<SourceMapping> mappings) {
+ final Set<Uri> sourcesSet = {};
+ for (final mapping in mappings) {
+ if (mapping.sourceInfo?.fileUri != null) {
+ sourcesSet.add(mapping.sourceInfo!.fileUri);
+ }
+ }
+
+ final List<Uri> sourcesList = sourcesSet.toList();
+
+ // Maps sources to their indices in the 'sources' list.
+ final Map<Uri, int> sourceIndices = {};
+ for (Uri source in sourcesList) {
+ sourceIndices[source] = sourceIndices.length;
+ }
+
+ final Set<String> namesSet = {};
+ for (final mapping in mappings) {
+ if (mapping.sourceInfo?.name != null) {
+ namesSet.add(mapping.sourceInfo!.name!);
+ }
+ }
+
+ final List<String> namesList = namesSet.toList();
+
+ // Maps names to their index in the 'names' list.
+ final Map<String, int> nameIndices = {};
+ for (String name in namesList) {
+ nameIndices[name] = nameIndices.length;
+ }
+
+ // Generate the 'mappings' field.
+ final StringBuffer mappingsStr = StringBuffer();
+
+ int lastTargetColumn = 0;
+ int lastSourceIndex = 0;
+ int lastSourceLine = 0;
+ int lastSourceColumn = 0;
+ int lastNameIndex = 0;
+
+ for (int i = 0; i < mappings.length; ++i) {
+ final mapping = mappings[i];
+
+ lastTargetColumn =
+ _encodeVLQ(mappingsStr, mapping.instructionOffset, lastTargetColumn);
+
+ final sourceInfo = mapping.sourceInfo;
+ if (sourceInfo != null) {
+ final sourceIndex = sourceIndices[sourceInfo.fileUri]!;
+
+ lastSourceIndex = _encodeVLQ(mappingsStr, sourceIndex, lastSourceIndex);
+ lastSourceLine = _encodeVLQ(mappingsStr, sourceInfo.line, lastSourceLine);
+ lastSourceColumn =
+ _encodeVLQ(mappingsStr, sourceInfo.col, lastSourceColumn);
+
+ if (sourceInfo.name != null) {
+ final nameIndex = nameIndices[sourceInfo.name!]!;
+ lastNameIndex = _encodeVLQ(mappingsStr, nameIndex, lastNameIndex);
+ }
+ }
+
+ if (i != mappings.length - 1) {
+ mappingsStr.write(',');
+ }
+ }
+
+ return """{
+ "version": 3,
+ "sources": [${sourcesList.map((source) => '"$source"').join(",")}],
+ "names": [${namesList.map((name) => '"$name"').join(",")}],
+ "mappings": "$mappingsStr"
+ }""";
+}
+
+/// Writes the VLQ of delta between [value] and [offset] into [output] and
+/// return [value].
+int _encodeVLQ(StringSink output, int value, int offset) {
+ int delta = value - offset;
+ int signBit = 0;
+ if (delta < 0) {
+ signBit = 1;
+ delta = -delta;
+ }
+ delta = (delta << 1) | signBit;
+ do {
+ int digit = delta & _vlqBaseMask;
+ delta >>= _vlqBaseShift;
+ if (delta > 0) {
+ digit |= _vlqContinuationBit;
+ }
+ output.write(_base64Digits[digit]);
+ } while (delta > 0);
+ return value;
+}
+
+const int _vlqBaseShift = 5;
+const int _vlqBaseMask = (1 << 5) - 1;
+const int _vlqContinuationBit = 1 << 5;
+const String _base64Digits = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn'
+ 'opqrstuvwxyz0123456789+/';
diff --git a/pkg/wasm_builder/lib/src/builder/instructions.dart b/pkg/wasm_builder/lib/src/builder/instructions.dart
index abe326e..ba4cd00 100644
--- a/pkg/wasm_builder/lib/src/builder/instructions.dart
+++ b/pkg/wasm_builder/lib/src/builder/instructions.dart
@@ -6,6 +6,7 @@
import '../ir/ir.dart' as ir;
import 'builder.dart';
+import '../../source_map.dart';
// TODO(joshualitt): Suggested further optimizations:
// 1) Add size estimates to `_Instruction`, and then remove logic where we
@@ -119,6 +120,12 @@
/// middle of the stack are left out.
int maxStackShown = 10;
+ /// Mappings for the instructions in [_instructions] to their source code.
+ ///
+ /// Since we add mappings as we generate instructions, this will be sorted
+ /// based on [SourceMapping.instructionOffset].
+ final List<SourceMapping>? _sourceMappings;
+
int _indent = 1;
final List<String> _traceLines = [];
@@ -141,7 +148,8 @@
/// Create a new instruction sequence.
InstructionsBuilder(this.module, List<ir.ValueType> outputs)
- : _stackTraces = module.watchPoints.isNotEmpty ? {} : null {
+ : _stackTraces = module.watchPoints.isNotEmpty ? {} : null,
+ _sourceMappings = module.sourceMapUrl == null ? null : [] {
_labelStack.add(Expression(const [], outputs));
}
@@ -151,9 +159,11 @@
/// Textual trace of the instructions.
String get trace => _traceLines.join();
+ bool get recordSourceMaps => _sourceMappings != null;
+
@override
- ir.Instructions forceBuild() =>
- ir.Instructions(locals, _instructions, _stackTraces, _traceLines);
+ ir.Instructions forceBuild() => ir.Instructions(
+ locals, _instructions, _stackTraces, _traceLines, _sourceMappings);
void _add(ir.Instruction i) {
if (!_reachable) return;
@@ -344,6 +354,55 @@
indentAfter: reindent ? 1 : 0);
}
+ // Source maps
+
+ /// Start mapping added instructions to the source location given in
+ /// arguments.
+ ///
+ /// This assumes [recordSourceMaps] is `true`.
+ void startSourceMapping(Uri fileUri, int line, int col, String? name) {
+ _addSourceMapping(
+ SourceMapping(_instructions.length, fileUri, line, col, name));
+ }
+
+ /// Stop mapping added instructions to the last source location given in
+ /// [startSourceMapping].
+ ///
+ /// The instructions added after this won't have a mapping in the source map.
+ ///
+ /// This assumes [recordSourceMaps] is `true`.
+ void stopSourceMapping() {
+ _addSourceMapping(SourceMapping.unmapped(_instructions.length));
+ }
+
+ void _addSourceMapping(SourceMapping mapping) {
+ final sourceMappings = _sourceMappings!;
+
+ if (sourceMappings.isNotEmpty) {
+ final lastMapping = sourceMappings.last;
+
+ // Check if we are overriding the current source location. This can
+ // happen when we restore the source location after a compiling a
+ // sub-tree, and the next node in the AST immediately updates the source
+ // location. The restored location is then never used.
+ if (lastMapping.instructionOffset == mapping.instructionOffset) {
+ sourceMappings.removeLast();
+ sourceMappings.add(mapping);
+ return;
+ }
+
+ // Check if we the new mapping maps to the same source as the old
+ // mapping. This happens when we have e.g. an instance field get like
+ // `length`, which gets transformed by the front-end as `this.length`. In
+ // this case `this` and `length` will have the same source location.
+ if (lastMapping.sourceInfo == mapping.sourceInfo) {
+ return;
+ }
+ }
+
+ sourceMappings.add(mapping);
+ }
+
// Meta
/// Emit a comment.
diff --git a/pkg/wasm_builder/lib/src/builder/module.dart b/pkg/wasm_builder/lib/src/builder/module.dart
index ff96ace..aeffc84 100644
--- a/pkg/wasm_builder/lib/src/builder/module.dart
+++ b/pkg/wasm_builder/lib/src/builder/module.dart
@@ -8,6 +8,7 @@
// TODO(joshualitt): Get rid of cycles in the builder graph.
/// A Wasm module builder.
class ModuleBuilder with Builder<ir.Module> {
+ final Uri? sourceMapUrl;
final List<int> watchPoints;
final types = TypesBuilder();
late final functions = FunctionsBuilder(this);
@@ -24,7 +25,7 @@
/// bytes to watch. When the module is serialized, the stack traces leading
/// to the production of all watched bytes are printed. This can be used to
/// debug runtime errors happening at specific offsets within the module.
- ModuleBuilder({this.watchPoints = const []});
+ ModuleBuilder(this.sourceMapUrl, {this.watchPoints = const []});
@override
ir.Module forceBuild() {
@@ -33,6 +34,7 @@
final finalMemories = memories.build();
final finalGlobals = globals.build();
return ir.Module(
+ sourceMapUrl,
finalFunctions,
finalTables,
tags.build(),
diff --git a/pkg/wasm_builder/lib/src/ir/function.dart b/pkg/wasm_builder/lib/src/ir/function.dart
index b0fce07..e39f23a 100644
--- a/pkg/wasm_builder/lib/src/ir/function.dart
+++ b/pkg/wasm_builder/lib/src/ir/function.dart
@@ -70,6 +70,7 @@
// Bundle locals and body
localS.write(body);
s.writeUnsigned(localS.data.length);
+ s.sourceMapSerializer.copyMappings(localS.sourceMapSerializer, s.offset);
s.writeData(localS);
}
diff --git a/pkg/wasm_builder/lib/src/ir/instructions.dart b/pkg/wasm_builder/lib/src/ir/instructions.dart
index 6eec752..98658cc 100644
--- a/pkg/wasm_builder/lib/src/ir/instructions.dart
+++ b/pkg/wasm_builder/lib/src/ir/instructions.dart
@@ -2,6 +2,7 @@
// 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 '../../source_map.dart';
import '../serialize/serialize.dart';
import 'ir.dart';
@@ -21,15 +22,46 @@
/// A string trace.
late final trace = _traceLines.join();
+ /// Mappings for the instructions in [_instructions] to their source code.
+ ///
+ /// Since we add mappings as we generate instructions, this will be sorted
+ /// based on [SourceMapping.instructionOffset].
+ final List<SourceMapping>? _sourceMappings;
+
/// Create a new instruction sequence.
- Instructions(
- this.locals, this.instructions, this._stackTraces, this._traceLines);
+ Instructions(this.locals, this.instructions, this._stackTraces,
+ this._traceLines, this._sourceMappings);
@override
void serialize(Serializer s) {
- for (final i in instructions) {
+ final sourceMappings = _sourceMappings;
+ int sourceMappingIdx = 0;
+ for (int instructionIdx = 0;
+ instructionIdx < instructions.length;
+ instructionIdx += 1) {
+ final i = instructions[instructionIdx];
if (_stackTraces != null) s.debugTrace(_stackTraces![i]!);
+
+ if (sourceMappings != null) {
+ // Skip to the mapping that covers the current instruction.
+ while (sourceMappingIdx < sourceMappings.length - 1 &&
+ sourceMappings[sourceMappingIdx + 1].instructionOffset <=
+ instructionIdx) {
+ sourceMappingIdx += 1;
+ }
+
+ if (sourceMappingIdx < sourceMappings.length) {
+ final mapping = sourceMappings[sourceMappingIdx];
+ if (mapping.instructionOffset <= instructionIdx) {
+ s.sourceMapSerializer.addMapping(s.offset, mapping.sourceInfo);
+ sourceMappingIdx += 1;
+ }
+ }
+ }
+
i.serialize(s);
}
+
+ s.sourceMapSerializer.addMapping(s.offset, null);
}
}
diff --git a/pkg/wasm_builder/lib/src/ir/module.dart b/pkg/wasm_builder/lib/src/ir/module.dart
index 945a985..6ab8585 100644
--- a/pkg/wasm_builder/lib/src/ir/module.dart
+++ b/pkg/wasm_builder/lib/src/ir/module.dart
@@ -17,8 +17,10 @@
final DataSegments dataSegments;
final List<Import> imports;
final List<int> watchPoints;
+ final Uri? sourceMapUrl;
Module(
+ this.sourceMapUrl,
this.functions,
this.tables,
this.tags,
@@ -51,6 +53,10 @@
DataCountSection(dataSegments.defined, watchPoints).serialize(s);
CodeSection(functions.defined, watchPoints).serialize(s);
DataSection(dataSegments.defined, watchPoints).serialize(s);
+ if (sourceMapUrl != null) {
+ SourceMapSection(sourceMapUrl.toString()).serialize(s);
+ }
+
if (functions.namedCount > 0 ||
types.namedCount > 0 ||
globals.namedCount > 0) {
diff --git a/pkg/wasm_builder/lib/src/serialize/sections.dart b/pkg/wasm_builder/lib/src/serialize/sections.dart
index df3a9e6..4c71841 100644
--- a/pkg/wasm_builder/lib/src/serialize/sections.dart
+++ b/pkg/wasm_builder/lib/src/serialize/sections.dart
@@ -17,6 +17,8 @@
serializeContents(contents);
s.writeByte(id);
s.writeUnsigned(contents.data.length);
+ s.sourceMapSerializer
+ .copyMappings(contents.sourceMapSerializer, s.offset);
s.writeData(contents, watchPoints);
}
}
@@ -390,3 +392,18 @@
s.writeData(globalNameSubsection);
}
}
+
+class SourceMapSection extends CustomSection {
+ final String url;
+
+ SourceMapSection(this.url) : super([]);
+
+ @override
+ bool get isNotEmpty => true;
+
+ @override
+ void serializeContents(Serializer s) {
+ s.writeName("sourceMappingURL");
+ s.writeName(url);
+ }
+}
diff --git a/pkg/wasm_builder/lib/src/serialize/serialize.dart b/pkg/wasm_builder/lib/src/serialize/serialize.dart
index 5643c15..c617ff1 100644
--- a/pkg/wasm_builder/lib/src/serialize/serialize.dart
+++ b/pkg/wasm_builder/lib/src/serialize/serialize.dart
@@ -11,11 +11,12 @@
ElementSection,
ExportSection,
FunctionSection,
- ImportSection,
GlobalSection,
+ ImportSection,
MemorySection,
NameSection,
+ SourceMapSection,
StartSection,
- TagSection,
TableSection,
+ TagSection,
TypeSection;
diff --git a/pkg/wasm_builder/lib/src/serialize/serializer.dart b/pkg/wasm_builder/lib/src/serialize/serializer.dart
index ae2fc7b..9ca3f52 100644
--- a/pkg/wasm_builder/lib/src/serialize/serializer.dart
+++ b/pkg/wasm_builder/lib/src/serialize/serializer.dart
@@ -6,6 +6,8 @@
import 'dart:convert';
import 'dart:typed_data';
+import '../../source_map.dart';
+
abstract class Serializable {
void serialize(Serializer s);
}
@@ -23,6 +25,11 @@
// chunk of data produced by this serializer.
late final SplayTreeMap<int, Object> _traces = SplayTreeMap();
+ /// Get the current offset in the serialized data.
+ int get offset => _index;
+
+ final SourceMapSerializer sourceMapSerializer = SourceMapSerializer();
+
void _ensure(int size) {
// Ensure space for at least `size` additional bytes.
if (_data.length < _index + size) {
diff --git a/tests/web/wasm/source_map_simple_test.dart b/tests/web/wasm/source_map_simple_test.dart
new file mode 100644
index 0000000..2197ae6
--- /dev/null
+++ b/tests/web/wasm/source_map_simple_test.dart
@@ -0,0 +1,119 @@
+// Copyright (c) 2024, 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.
+
+// dart2wasmOptions=--extra-compiler-option=-DTEST_COMPILATION_DIR=$TEST_COMPILATION_DIR
+
+import 'dart:js_interop';
+import 'dart:typed_data';
+import 'dart:convert';
+
+import 'package:source_maps/parser.dart';
+
+void f() {
+ g();
+}
+
+void g() {
+ throw 'hi';
+}
+
+void main() {
+ // Read source map of the current program.
+ final compilationDir = const String.fromEnvironment("TEST_COMPILATION_DIR");
+ final sourceMapFileContents =
+ readfile('$compilationDir/source_map_simple_test.wasm.map');
+ final mapping = parse(utf8.decode(sourceMapFileContents)) as SingleMapping;
+
+ // Get some simple stack trace.
+ String? stackTraceString;
+ try {
+ f();
+ } catch (e, st) {
+ stackTraceString = st.toString();
+ }
+
+ // Print the stack trace to make it easy to update the test.
+ print("-----");
+ print(stackTraceString);
+ print("-----");
+
+ final stackTraceLines = stackTraceString!.split('\n');
+
+ // Stack trace should look like:
+ //
+ // at Error._throwWithCurrentStackTrace (wasm://wasm/00118b26:wasm-function[144]:0x163e0)
+ // at f (wasm://wasm/00118b26:wasm-function[996]:0x243dd)
+ // at main (wasm://wasm/00118b26:wasm-function[988]:0x241dc)
+ // at main tear-off trampoline (wasm://wasm/00118b26:wasm-function[990]:0x24340)
+ // at _invokeMain (wasm://wasm/00118b26:wasm-function[104]:0x15327)
+ // at Module.invoke (/usr/local/google/home/omersa/dart/sdk/test/test.mjs:317:26)
+ // at main (/usr/local/google/home/omersa/dart/sdk/sdk/pkg/dart2wasm/bin/run_wasm.js:421:21)
+ // at async action (/usr/local/google/home/omersa/dart/sdk/sdk/pkg/dart2wasm/bin/run_wasm.js:350:37)
+ //
+ // The first 5 frames are in Wasm, but "main tear-off trampoline" shouldn't
+ // have a source mapping as it's compiler generated.
+
+ // (function name, line, column) of the frames we check.
+ //
+ // Information we don't check are "null": we don't want to check line/column
+ // of standard library functions to avoid breaking the test with unrelated
+ // changes to the standard library.
+ const List<(String, int?, int?)?> frameDetails = [
+ ("_throwWithCurrentStackTrace", null, null),
+ ("g", 18, 3),
+ ("f", 14, 3),
+ ("main", 31, 5),
+ null, // compiler generated, not mapped
+ ("_invokeMain", null, null),
+ ];
+
+ for (int frameIdx = 0; frameIdx < frameDetails.length; frameIdx += 1) {
+ final line = stackTraceLines[frameIdx];
+ final hexOffsetMatch = stackTraceHexOffsetRegExp.firstMatch(line);
+ if (hexOffsetMatch == null) {
+ throw "Unable to parse hex offset from stack frame $frameIdx";
+ }
+ final hexOffsetStr = hexOffsetMatch.group(1)!; // includes '0x'
+ final offset = int.tryParse(hexOffsetStr);
+ if (offset == null) {
+ throw "Unable to parse hex number in frame $frameIdx: $hexOffsetStr";
+ }
+ final span = mapping.spanFor(0, offset);
+ final frameInfo = frameDetails[frameIdx];
+ if (frameInfo == null) {
+ if (span != null) {
+ throw "Stack frame $frameIdx should not have a source span, but it is mapped: $span";
+ }
+ continue;
+ }
+ if (span == null) {
+ throw "Stack frame $frameIdx does not have source mapping";
+ }
+ final funName = span.text;
+ if (frameInfo.$1 != funName) {
+ throw "Stack frame $frameIdx does not have expected name: expected ${frameInfo.$1}, found $funName";
+ }
+ if (frameInfo.$2 != null) {
+ if (span.start.line + 1 != frameInfo.$2) {
+ throw "Stack frame $frameIdx is expected to have line ${frameInfo.$2}, but it has line ${span.start.line + 1}";
+ }
+ }
+ if (frameInfo.$3 != null) {
+ if (span.start.column + 1 != frameInfo.$3) {
+ throw "Stack frame $frameIdx is expected to have column ${frameInfo.$3}, but it has column ${span.start.column + 1}";
+ }
+ }
+ }
+}
+
+/// Read the file at the given [path].
+///
+/// This relies on the `readbuffer` function provided by d8.
+@JS()
+external JSArrayBuffer readbuffer(JSString path);
+
+/// Read the file at the given [path].
+Uint8List readfile(String path) => Uint8List.view(readbuffer(path.toJS).toDart);
+
+final stackTraceHexOffsetRegExp = RegExp(r'wasm-function.*(0x[0-9a-fA-F]+)\)$');
diff --git a/tests/web/web.status b/tests/web/web.status
index f09a47a..55cc136 100644
--- a/tests/web/web.status
+++ b/tests/web/web.status
@@ -8,6 +8,9 @@
[ $compiler == dart2wasm ]
(?!wasm)*: SkipByDesign
+[ $compiler == dart2wasm && $runtime != d8 ]
+wasm/source_map_simple_test: SkipByDesign
+
[ $compiler != dart2wasm ]
wasm/*: SkipByDesign