Skip to content

Commit

Permalink
[dart2wasm] Generate source maps
Browse files Browse the repository at this point in the history
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.

## Implementation

- `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

Change-Id: Ic8f723f7a154402c0d34710689db57d640b83b86
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/370500
Reviewed-by: Martin Kustermann <kustermann@google.com>
Commit-Queue: Ömer Ağacan <omersa@google.com>
  • Loading branch information
osa1 authored and Commit Queue committed Jul 4, 2024
1 parent 0ac19a1 commit 10742d9
Show file tree
Hide file tree
Showing 20 changed files with 601 additions and 31 deletions.
17 changes: 14 additions & 3 deletions pkg/dart2wasm/lib/closures.dart
Original file line number Diff line number Diff line change
Expand Up @@ -947,8 +947,9 @@ class ClosureRepresentationCluster {
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.
Expand Down Expand Up @@ -1137,12 +1138,22 @@ class CaptureFinder extends RecursiveVisitor {

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].
Expand Down Expand Up @@ -1275,7 +1286,7 @@ class CaptureFinder extends RecursiveVisitor {
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);
Expand Down
89 changes: 89 additions & 0 deletions pkg/dart2wasm/lib/code_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,67 @@ class CodeGenerator extends ExpressionVisitor1<w.ValueType, w.ValueType>
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;
Expand Down Expand Up @@ -208,6 +269,9 @@ class CodeGenerator extends ExpressionVisitor1<w.ValueType, w.ValueType>
return;
}

final source = member.enclosingComponent!.uriToSource[member.fileUri]!;
setSourceMapSourceAndFileOffset(source, member.fileOffset);

if (member is Constructor) {
translator.membersBeingGenerated.add(member);
if (reference.isConstructorBodyReference) {
Expand Down Expand Up @@ -484,12 +548,18 @@ class CodeGenerator extends ExpressionVisitor1<w.ValueType, w.ValueType>

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);
}
}
}
Expand Down Expand Up @@ -717,6 +787,8 @@ class CodeGenerator extends ExpressionVisitor1<w.ValueType, w.ValueType>
// Initialize closure information from enclosing member.
this.closures = closures;

setSourceMapSource(lambda.functionNodeSource);

assert(lambda.functionNode.asyncMarker != AsyncMarker.Async);

setupLambdaParametersAndContexts(lambda);
Expand Down Expand Up @@ -877,22 +949,39 @@ class CodeGenerator extends ExpressionVisitor1<w.ValueType, w.ValueType>
/// 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);
return expectedType;
} 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);
}
}

Expand Down
37 changes: 23 additions & 14 deletions pkg/dart2wasm/lib/compile.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import 'package:vm/transformations/type_flow/transformer.dart' as globalTypeFlow
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';
Expand All @@ -45,26 +45,26 @@ import 'target.dart' hide Mode;
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.
///
/// 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) {
Expand Down Expand Up @@ -205,10 +205,19 @@ Future<CompilerOutput?> compileToModule(compiler.WasmCompilerOptions options,
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);
}
4 changes: 4 additions & 0 deletions pkg/dart2wasm/lib/dart2wasm.dart
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ final List<Option> options = [
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(
Expand Down
18 changes: 15 additions & 3 deletions pkg/dart2wasm/lib/generate_wasm.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,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';
Expand All @@ -22,10 +23,16 @@ Future<int> generateWasm(WasmCompilerOptions options,
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;
Expand All @@ -36,8 +43,13 @@ Future<int> generateWasm(WasmCompilerOptions options,
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;
}
4 changes: 4 additions & 0 deletions pkg/dart2wasm/lib/state_machine.dart
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,9 @@ abstract class StateMachineCodeGenerator extends CodeGenerator {

@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!);
Expand All @@ -614,6 +617,7 @@ abstract class StateMachineCodeGenerator extends CodeGenerator {
@override
w.BaseFunction generateLambda(Lambda lambda, Closures closures) {
this.closures = closures;
setSourceMapSource(lambda.functionNodeSource);
setupLambdaParametersAndContexts(lambda);
_generateBodies(lambda.functionNode);
return function;
Expand Down
5 changes: 3 additions & 2 deletions pkg/dart2wasm/lib/translator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class TranslatorOptions {
bool verbose = false;
bool enableExperimentalFfi = false;
bool enableExperimentalWasmInterop = false;
bool generateSourceMaps = true;
int inliningLimit = 0;
int? sharedMemoryMaxPages;
List<int> watchPoints = [];
Expand Down Expand Up @@ -292,8 +293,8 @@ class Translator with KernelNodes {
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
Expand Down
1 change: 1 addition & 0 deletions pkg/dart2wasm/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ dependencies:
kernel: any
vm: any
wasm_builder: any
path: any

# Use 'any' constraints here; we get our versions from the DEPS file.
dev_dependencies:
Expand Down
4 changes: 4 additions & 0 deletions pkg/test_runner/lib/src/test_suite.dart
Original file line number Diff line number Diff line change
Expand Up @@ -838,6 +838,10 @@ class StandardTestSuite extends TestSuite {
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(
Expand Down
Loading

0 comments on commit 10742d9

Please sign in to comment.