Skip to content

Commit addbdcb

Browse files
natebiggsCommit Queue
authored andcommitted
[dart2wasm] Add --dry-run flag to dart2wasm.
This new flag will run the CFE to create a kernel and then run a series of checks over the resulting kernel to look for errors that could block a wasm migration. The compiler will then exit before actually starting the wasm compilation process. This means no output file will be emitted so callers must be aware of this. This first CL is not meant to cover every check we could add here. It adds some initial checks and we can expand on this to include more in follow-up changes. One of the checks implemented here is also provided by a lint. While ideally we would share code between lints and these checks, the delta in the CFE vs analyzer model makes that infeasible today. Sample output: ``` Found incompatibilities with WebAssembly. package:dryrun/test.dart 5:15 - Cannot test a JS value against String (3) package:dryrun/test.dart 6:7 - JS interop class 'B' cannot extend Dart class 'A'. (2) ``` Bug: #60050 Change-Id: Ib2c8e3501cc42d57b86ebaa749359ce6c5dba974 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/437960 Commit-Queue: Nate Biggs <[email protected]> Reviewed-by: Martin Kustermann <[email protected]>
1 parent 1af124c commit addbdcb

File tree

16 files changed

+663
-3
lines changed

16 files changed

+663
-3
lines changed

pkg/dart2wasm/lib/compile.dart

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import 'package:wasm_builder/wasm_builder.dart' show Serializer;
3737
import 'compiler_options.dart' as compiler;
3838
import 'constant_evaluator.dart';
3939
import 'deferred_loading.dart';
40+
import 'dry_run.dart';
4041
import 'dynamic_module_kernel_metadata.dart';
4142
import 'dynamic_modules.dart';
4243
import 'js/runtime_generator.dart' as js;
@@ -49,6 +50,12 @@ import 'translator.dart';
4950

5051
sealed class CompilationResult {}
5152

53+
abstract class CompilationDryRunResult extends CompilationResult {}
54+
55+
class CompilationDryRunError extends CompilationDryRunResult {}
56+
57+
class CompilationDryRunSuccess extends CompilationDryRunResult {}
58+
5259
class CompilationSuccess extends CompilationResult {
5360
final Map<String, ({Uint8List moduleBytes, String? sourceMap})> wasmModules;
5461
final String jsRuntime;
@@ -79,7 +86,9 @@ class CFECrashError extends CompilationError {
7986
/// (We print them as soon as they are reported by CFE. i.e. we stream errors
8087
/// instead of accumulating/batching all of them and reporting at the end.)
8188
class CFECompileTimeErrors extends CompilationError {
82-
CFECompileTimeErrors();
89+
final Component? component;
90+
91+
CFECompileTimeErrors(this.component);
8392
}
8493

8594
const List<String> _librariesToIndex = [
@@ -199,7 +208,18 @@ Future<CompilationResult> compileToModule(
199208
} catch (e, s) {
200209
return CFECrashError(e, s);
201210
}
202-
if (hadCompileTimeError) return CFECompileTimeErrors();
211+
if (options.dryRun) {
212+
final component = compilerResult?.component;
213+
if (component == null) {
214+
return CompilationDryRunError();
215+
}
216+
final summarizer = DryRunSummarizer(component);
217+
final hasErrors = summarizer.summarize();
218+
return hasErrors ? CompilationDryRunError() : CompilationDryRunSuccess();
219+
}
220+
if (hadCompileTimeError) {
221+
return CFECompileTimeErrors(compilerResult?.component);
222+
}
203223
assert(compilerResult != null);
204224

205225
Component component = compilerResult!.component!;

pkg/dart2wasm/lib/compiler_options.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class WasmCompilerOptions {
2929
String? dumpKernelAfterCfe;
3030
String? dumpKernelBeforeTfa;
3131
String? dumpKernelAfterTfa;
32+
bool dryRun = false;
3233

3334
factory WasmCompilerOptions.defaultOptions() =>
3435
WasmCompilerOptions(mainUri: Uri(), outputFile: '');

pkg/dart2wasm/lib/dart2wasm.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ final List<Option> options = [
2626
defaultsTo: _d.translatorOptions.inlining),
2727
Flag("minify", (o, value) => o.translatorOptions.minify = value,
2828
defaultsTo: _d.translatorOptions.minify),
29+
Flag("dry-run", (o, value) => o.dryRun = value, defaultsTo: _d.dryRun),
2930
Flag("polymorphic-specialization",
3031
(o, value) => o.translatorOptions.polymorphicSpecialization = value,
3132
defaultsTo: _d.translatorOptions.polymorphicSpecialization),

pkg/dart2wasm/lib/dry_run.dart

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:_js_interop_checks/js_interop_checks.dart';
6+
import 'package:collection/collection.dart';
7+
import 'package:front_end/src/api_prototype/codes.dart'
8+
show Message, LocatedMessage;
9+
import 'package:kernel/ast.dart';
10+
import 'package:kernel/class_hierarchy.dart';
11+
import 'package:kernel/core_types.dart';
12+
import 'package:kernel/target/targets.dart' show DiagnosticReporter;
13+
import 'package:kernel/type_environment.dart';
14+
15+
enum _DryRunErrorCode {
16+
noDartHtml(0),
17+
noDartJs(1),
18+
interopChecksError(2),
19+
isTestValueError(3),
20+
isTestTypeError(4),
21+
isTestGenericTypeError(5),
22+
;
23+
24+
const _DryRunErrorCode(this.code);
25+
26+
final int code;
27+
}
28+
29+
class _DryRunError {
30+
final _DryRunErrorCode code;
31+
final String problemMessage;
32+
final Uri? errorSourceUri;
33+
final Location? errorLocation;
34+
35+
_DryRunError(this.code, this.problemMessage,
36+
{this.errorSourceUri, this.errorLocation});
37+
38+
static String _locationToString(Uri? sourceUri, Location? location) {
39+
if (sourceUri == null && location == null) return 'unknown location';
40+
final uri = sourceUri ?? location?.file;
41+
final lineCol =
42+
location != null ? ' ${location.line}:${location.column}' : '';
43+
return '$uri$lineCol';
44+
}
45+
46+
@override
47+
String toString() => '${_locationToString(errorSourceUri, errorLocation)} '
48+
'- $problemMessage (${code.code})';
49+
}
50+
51+
/// Runs several passes over the provided kernel to find any code in the
52+
/// sources that could block a migration to WASM from JS.
53+
///
54+
/// At the end of execution, it emits a summary to stdout that looks like:
55+
/// ```
56+
/// Found incompatibilities with WebAssembly.
57+
///
58+
/// package:dryrun/test.dart 5:15 - Cannot test a JS value against String (3)
59+
/// package:dryrun/test.dart 6:7 - JS interop class 'B' cannot extend Dart class 'A'. (2)
60+
/// ````
61+
class DryRunSummarizer {
62+
final Component component;
63+
late final CoreTypes coreTypes;
64+
late final ClassHierarchy classHierarchy;
65+
66+
DryRunSummarizer(this.component) {
67+
coreTypes = CoreTypes(component);
68+
classHierarchy = ClassHierarchy(component, coreTypes);
69+
}
70+
71+
static const Map<String, _DryRunErrorCode> _disallowedDartUris = {
72+
'dart:html': _DryRunErrorCode.noDartHtml,
73+
'dart:js': _DryRunErrorCode.noDartJs,
74+
// 'dart:ffi' is handled by interop checks.
75+
};
76+
77+
List<_DryRunError> _analyzeImports() {
78+
final errors = <_DryRunError>[];
79+
80+
for (final library in component.libraries) {
81+
if (library.importUri.scheme == 'dart') continue;
82+
83+
for (final dep in library.dependencies) {
84+
final depLib = dep.importedLibraryReference.asLibrary;
85+
final code = _disallowedDartUris[depLib.importUri.toString()];
86+
if (code != null) {
87+
errors.add(_DryRunError(code, '${depLib.importUri} unsupported',
88+
errorSourceUri: library.importUri, errorLocation: dep.location));
89+
}
90+
}
91+
}
92+
93+
return errors;
94+
}
95+
96+
List<_DryRunError> _interopChecks() {
97+
final collector = _CollectingDiagnosticReporter(component);
98+
final reporter = JsInteropDiagnosticReporter(collector);
99+
// These checks will already have been done by the CFE but the message
100+
// format the CFE provides for those errors makes it hard to identify them
101+
// as interop-specific errors. Instead we rerun and collect any errors here.
102+
component.accept(JsInteropChecks(coreTypes, classHierarchy, reporter,
103+
JsInteropChecks.getNativeClasses(component),
104+
isDart2Wasm: true));
105+
return collector.errors;
106+
}
107+
108+
List<_DryRunError> _analyzeComponent() {
109+
final analyzer = _AnalysisVisitor(coreTypes, classHierarchy);
110+
component.accept(analyzer);
111+
return analyzer.errors;
112+
}
113+
114+
bool summarize() {
115+
final errors = [
116+
..._analyzeImports(),
117+
..._interopChecks(),
118+
..._analyzeComponent(),
119+
];
120+
121+
if (errors.isNotEmpty) {
122+
print('Found incompatibilities with WebAssembly.\n');
123+
print(errors.join('\n'));
124+
return true;
125+
}
126+
return false;
127+
}
128+
}
129+
130+
class _CollectingDiagnosticReporter
131+
extends DiagnosticReporter<Message, LocatedMessage> {
132+
final Component component;
133+
final List<_DryRunError> errors = <_DryRunError>[];
134+
135+
_CollectingDiagnosticReporter(this.component);
136+
137+
@override
138+
void report(Message message, int charOffset, int length, Uri? fileUri,
139+
{List<LocatedMessage>? context}) {
140+
final libraryUri = fileUri != null
141+
? component.libraries
142+
.firstWhereOrNull((e) => e.fileUri == fileUri)
143+
?.importUri
144+
: null;
145+
final location =
146+
fileUri != null ? component.getLocation(fileUri, charOffset) : null;
147+
errors.add(_DryRunError(
148+
_DryRunErrorCode.interopChecksError, message.problemMessage,
149+
errorSourceUri: libraryUri ?? fileUri, errorLocation: location));
150+
}
151+
}
152+
153+
class _AnalysisVisitor extends RecursiveVisitor {
154+
Library? _enclosingLibrary;
155+
late StaticTypeContext _context;
156+
final TypeEnvironment _typeEnvironment;
157+
final DartType _jsAnyType;
158+
final List<_DryRunError> errors = [];
159+
160+
_AnalysisVisitor(CoreTypes coreTypes, ClassHierarchy hierarchy)
161+
: _typeEnvironment = TypeEnvironment(coreTypes, hierarchy),
162+
_jsAnyType = ExtensionType(
163+
coreTypes.index.getExtensionType('dart:js_interop', 'JSAny'),
164+
Nullability.nullable);
165+
166+
@override
167+
void visitLibrary(Library node) {
168+
if (node.importUri.scheme == 'dart') return;
169+
_enclosingLibrary = node;
170+
_context = StaticTypeContext.forAnnotations(node, _typeEnvironment);
171+
super.visitLibrary(node);
172+
_enclosingLibrary = null;
173+
}
174+
175+
@override
176+
void visitProcedure(Procedure node) {
177+
_context = StaticTypeContext(node, _typeEnvironment);
178+
super.visitProcedure(node);
179+
}
180+
181+
@override
182+
void visitIsExpression(IsExpression node) {
183+
final operandStaticType = node.operand.getStaticType(_context);
184+
if (_typeEnvironment.isSubtypeOf(operandStaticType, _jsAnyType)) {
185+
errors.add(_DryRunError(
186+
_DryRunErrorCode.isTestValueError,
187+
'Should not perform an `is` test on a JS value. Use `isA` with a JS '
188+
'value type instead.',
189+
errorSourceUri: _enclosingLibrary?.importUri,
190+
errorLocation: node.location));
191+
}
192+
if (_typeEnvironment.isSubtypeOf(node.type, _jsAnyType)) {
193+
errors.add(_DryRunError(
194+
_DryRunErrorCode.isTestTypeError,
195+
'Should not perform an `is` test against a JS value type. '
196+
'Use `isA` instead.',
197+
errorSourceUri: _enclosingLibrary?.importUri,
198+
errorLocation: node.location));
199+
} else if (_hasJsTypeArguments(node.type)) {
200+
errors.add(_DryRunError(
201+
_DryRunErrorCode.isTestGenericTypeError,
202+
'Should not perform an `is` test against a generic DartType with JS '
203+
'type arguments.',
204+
errorSourceUri: _enclosingLibrary?.importUri,
205+
errorLocation: node.location));
206+
}
207+
super.visitIsExpression(node);
208+
}
209+
210+
bool _hasJsTypeArguments(DartType type) {
211+
// Check InterfaceType and ExtensionType
212+
if (type is TypeDeclarationType) {
213+
final arguments = type.typeArguments;
214+
if (arguments.any((e) => _typeEnvironment.isSubtypeOf(e, _jsAnyType))) {
215+
return true;
216+
}
217+
return arguments.any(_hasJsTypeArguments);
218+
} else if (type is RecordType) {
219+
final fields = type.positional.followedBy(type.named.map((t) => t.type));
220+
if (fields.any((e) => _typeEnvironment.isSubtypeOf(e, _jsAnyType))) {
221+
return true;
222+
}
223+
return fields.any(_hasJsTypeArguments);
224+
}
225+
return false;
226+
}
227+
}

pkg/dart2wasm/lib/generate_wasm.dart

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,17 @@ Future<int> generateWasm(WasmCompilerOptions options,
7878

7979
CompilationResult result =
8080
await compileToModule(options, relativeSourceMapUrlMapper, (message) {
81-
printDiagnosticMessage(message, errorPrinter);
81+
if (!options.dryRun) printDiagnosticMessage(message, errorPrinter);
8282
});
8383

84+
if (result is CompilationDryRunResult) {
85+
assert(options.dryRun);
86+
if (result is CompilationDryRunError) {
87+
return 254;
88+
}
89+
return 0;
90+
}
91+
8492
// If the compilation to wasm failed we use appropriate exit codes recognized
8593
// by our test infrastructure. We use the same exit codes as the VM does. See:
8694
// runtime/bin/error_exit.h:kDartFrontendErrorExitCode

pkg/dart2wasm/pubspec.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,6 @@ dependencies:
2121

2222
# Use 'any' constraints here; we get our versions from the DEPS file.
2323
dev_dependencies:
24+
expect: any
25+
js: any
2426
lints: any

0 commit comments

Comments
 (0)