3b750c5545
The file will contain a JS expression that evaluates to a boolean.
If it (at runtime) evalutes to
* `true` it means that all required features are supported by the JS
environment and the dart2wasm-compiled app can be used
* `false` it means some features were not present in the JS
environment and the dart2wasm-compiled app shouldn't be used,
instead a dart2js fallback may be used
We introduce this mechanism to allow users, at compilation time, to tell
dart2wasm to take advantage of new spec features and allow the runtime
to self-detect whether they are available and fallback to dart2js if
not.
The first feature we introduce (already in this PR) is
`--require-js-string-builtin` that will tell dart2wasm it can assume the
`js-string` builtin is available (and emit corresponding `*.support.js`
code to detect it).
If the flag was passed, we take advantage of the `js-string` import
mechanism for string constants that doesn't require emitting them in the
mjs file (which significantly reduces code size and improves startup
time - compared with emitting JS strings in the mjs file).
We enable `--require-js-string-builtin` on one CI configuration for
testing that if we don't use any polyfill, the imports of the builtin
functions as well as magical utf8-encoded wasm imports work.
We also use a template mechanism to generate `*.mjs` as the code
becomes more readable (e.g. to conditionally include the js string
polyfill)
Issue https://github.com/dart-lang/sdk/issues/59951
Change-Id: Ic7e7818a2d5269095935022941352beeb9fed731
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/408781
Reviewed-by: Ömer Ağacan <omersa@google.com>
Commit-Queue: Martin Kustermann <kustermann@google.com>
310 lines
11 KiB
Dart
310 lines
11 KiB
Dart
// Copyright (c) 2022, 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.
|
|
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:build_integration/file_system/multi_root.dart'
|
|
show MultiRootFileSystem;
|
|
import 'package:front_end/src/api_prototype/standard_file_system.dart'
|
|
show StandardFileSystem;
|
|
import 'package:front_end/src/api_unstable/vm.dart'
|
|
show
|
|
CompilerOptions,
|
|
CompilerResult,
|
|
DiagnosticMessage,
|
|
kernelForProgram,
|
|
NnbdMode,
|
|
Severity;
|
|
import 'package:kernel/ast.dart';
|
|
import 'package:kernel/class_hierarchy.dart';
|
|
import 'package:kernel/core_types.dart';
|
|
import 'package:kernel/kernel.dart' show writeComponentToText;
|
|
import 'package:kernel/library_index.dart';
|
|
import 'package:kernel/verifier.dart';
|
|
import 'package:vm/kernel_front_end.dart' show writeDepfile;
|
|
import 'package:vm/transformations/mixin_deduplication.dart'
|
|
as mixin_deduplication show transformComponent;
|
|
import 'package:vm/transformations/to_string_transformer.dart'
|
|
as to_string_transformer;
|
|
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 Serializer;
|
|
|
|
import 'compiler_options.dart' as compiler;
|
|
import 'constant_evaluator.dart';
|
|
import 'deferred_loading.dart' as deferred_loading;
|
|
import 'js/runtime_generator.dart' as js;
|
|
import 'record_class_generator.dart';
|
|
import 'records.dart';
|
|
import 'target.dart' as wasm show Mode;
|
|
import 'target.dart' hide Mode;
|
|
import 'translator.dart';
|
|
|
|
sealed class CompilationResult {}
|
|
|
|
class CompilationSuccess extends CompilationResult {
|
|
final Map<String, ({Uint8List moduleBytes, String? sourceMap})> wasmModules;
|
|
final String jsRuntime;
|
|
final String supportJs;
|
|
|
|
CompilationSuccess(this.wasmModules, this.jsRuntime, this.supportJs);
|
|
}
|
|
|
|
class CompilationError extends CompilationResult {}
|
|
|
|
/// The CFE has crashed with an exception.
|
|
///
|
|
/// This is a CFE bug and should be reported by users.
|
|
class CFECrashError extends CompilationError {
|
|
final Object error;
|
|
final StackTrace stackTrace;
|
|
|
|
CFECrashError(this.error, this.stackTrace);
|
|
}
|
|
|
|
/// Compiling the Dart program resulted in compile-time errors.
|
|
///
|
|
/// This is a bug in the dart program (e.g. syntax errors, static type errors,
|
|
/// ...) that's being compiled. Users have to address those errors in their
|
|
/// code for it to compile successfully.
|
|
///
|
|
/// The errors are already printed via the `handleDiagnosticMessage` callback.
|
|
/// (We print them as soon as they are reported by CFE. i.e. we stream errors
|
|
/// instead of accumulating/batching all of them and reporting at the end.)
|
|
class CFECompileTimeErrors extends CompilationError {
|
|
CFECompileTimeErrors();
|
|
}
|
|
|
|
/// 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.
|
|
///
|
|
/// When generating source maps, `sourceMapUrlGenerator` argument should be
|
|
/// provided which takes the module name and gives 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<CompilationResult> compileToModule(
|
|
compiler.WasmCompilerOptions options,
|
|
Uri Function(String moduleName)? sourceMapUrlGenerator,
|
|
void Function(DiagnosticMessage) handleDiagnosticMessage) async {
|
|
var hadCompileTimeError = false;
|
|
void diagnosticMessageHandler(DiagnosticMessage message) {
|
|
if (message.severity == Severity.error) {
|
|
hadCompileTimeError = true;
|
|
}
|
|
handleDiagnosticMessage(message);
|
|
}
|
|
|
|
final wasm.Mode mode;
|
|
if (options.translatorOptions.jsCompatibility) {
|
|
mode = wasm.Mode.jsCompatibility;
|
|
} else {
|
|
mode = wasm.Mode.regular;
|
|
}
|
|
final WasmTarget target = WasmTarget(
|
|
enableExperimentalFfi: options.translatorOptions.enableExperimentalFfi,
|
|
enableExperimentalWasmInterop:
|
|
options.translatorOptions.enableExperimentalWasmInterop,
|
|
removeAsserts: !options.translatorOptions.enableAsserts,
|
|
mode: mode);
|
|
CompilerOptions compilerOptions = CompilerOptions()
|
|
..target = target
|
|
// This is a dummy directory that always exists. This option should be
|
|
// unused as we pass platform.dill or libraries.json, though currently the
|
|
// CFE mandates this option to be there (but doesn't use it).
|
|
// => Remove this once CFE no longer mandates this (or remove option in CFE
|
|
// entirely).
|
|
..sdkRoot = Uri.file('.')
|
|
..librariesSpecificationUri = options.librariesSpecPath
|
|
..packagesFileUri = options.packagesPath
|
|
..environmentDefines = {
|
|
'dart.tool.dart2wasm': 'true',
|
|
...options.environment,
|
|
}
|
|
..explicitExperimentalFlags = options.feExperimentalFlags
|
|
..verbose = false
|
|
..onDiagnostic = diagnosticMessageHandler
|
|
..nnbdMode = NnbdMode.Strong;
|
|
if (options.multiRootScheme != null) {
|
|
compilerOptions.fileSystem = MultiRootFileSystem(
|
|
options.multiRootScheme!,
|
|
options.multiRoots.isEmpty ? [Uri.base] : options.multiRoots,
|
|
StandardFileSystem.instance);
|
|
}
|
|
|
|
if (options.platformPath != null) {
|
|
compilerOptions.sdkSummary = options.platformPath;
|
|
} else {
|
|
compilerOptions.compileSdk = true;
|
|
}
|
|
|
|
CompilerResult? compilerResult;
|
|
try {
|
|
compilerResult = await kernelForProgram(options.mainUri, compilerOptions);
|
|
} catch (e, s) {
|
|
return CFECrashError(e, s);
|
|
}
|
|
if (hadCompileTimeError) return CFECompileTimeErrors();
|
|
assert(compilerResult != null);
|
|
|
|
Component component = compilerResult!.component!;
|
|
CoreTypes coreTypes = compilerResult.coreTypes!;
|
|
ClassHierarchy classHierarchy = compilerResult.classHierarchy!;
|
|
LibraryIndex libraryIndex = LibraryIndex(component, [
|
|
"dart:_boxed_bool",
|
|
"dart:_boxed_double",
|
|
"dart:_boxed_int",
|
|
"dart:_compact_hash",
|
|
"dart:_internal",
|
|
"dart:_js_helper",
|
|
"dart:_js_types",
|
|
"dart:_list",
|
|
"dart:_string",
|
|
"dart:_wasm",
|
|
"dart:async",
|
|
"dart:collection",
|
|
"dart:core",
|
|
"dart:ffi",
|
|
"dart:typed_data",
|
|
]);
|
|
|
|
if (options.dumpKernelAfterCfe != null) {
|
|
writeComponentToText(component, path: options.dumpKernelAfterCfe!);
|
|
}
|
|
|
|
if (options.deleteToStringPackageUri.isNotEmpty) {
|
|
to_string_transformer.transformComponent(
|
|
component, options.deleteToStringPackageUri);
|
|
}
|
|
|
|
if (options.translatorOptions.enableMultiModuleStressTestMode) {
|
|
deferred_loading.transformComponentForTestMode(
|
|
component, classHierarchy, coreTypes, target);
|
|
}
|
|
|
|
ConstantEvaluator constantEvaluator = ConstantEvaluator(
|
|
options, target, component, coreTypes, classHierarchy, libraryIndex);
|
|
unreachable_code_elimination.transformComponent(target, component,
|
|
constantEvaluator, options.translatorOptions.enableAsserts);
|
|
|
|
js.RuntimeFinalizer jsRuntimeFinalizer =
|
|
js.createRuntimeFinalizer(component, coreTypes, classHierarchy);
|
|
|
|
final Map<RecordShape, Class> recordClasses =
|
|
generateRecordClasses(component, coreTypes);
|
|
target.recordClasses = recordClasses;
|
|
|
|
if (options.dumpKernelBeforeTfa != null) {
|
|
writeComponentToText(component, path: options.dumpKernelBeforeTfa!);
|
|
}
|
|
|
|
mixin_deduplication.transformComponent(component);
|
|
|
|
// Patch `dart:_internal`s `mainTearOff` getter.
|
|
final internalLib = component.libraries
|
|
.singleWhere((lib) => lib.importUri.toString() == 'dart:_internal');
|
|
final mainTearOff = internalLib.procedures
|
|
.singleWhere((procedure) => procedure.name.text == 'mainTearOff');
|
|
mainTearOff.isExternal = false;
|
|
mainTearOff.function.body = ReturnStatement(
|
|
ConstantExpression(StaticTearOffConstant(component.mainMethod!)));
|
|
|
|
// Keep the flags in-sync with
|
|
// pkg/vm/test/transformations/type_flow/transformer_test.dart
|
|
globalTypeFlow.transformComponent(target, coreTypes, component,
|
|
useRapidTypeAnalysis: false);
|
|
|
|
if (options.dumpKernelAfterTfa != null) {
|
|
writeComponentToText(component,
|
|
path: options.dumpKernelAfterTfa!, showMetadata: true);
|
|
}
|
|
|
|
assert(() {
|
|
verifyComponent(
|
|
target, VerificationStage.afterGlobalTransformations, component);
|
|
return true;
|
|
}());
|
|
|
|
final moduleOutputData = deferred_loading.modulesForComponent(
|
|
component, options, target, coreTypes);
|
|
|
|
var translator = Translator(component, coreTypes, libraryIndex, recordClasses,
|
|
moduleOutputData, options.translatorOptions);
|
|
|
|
String? depFile = options.depFile;
|
|
if (depFile != null) {
|
|
writeDepfile(compilerOptions.fileSystem, component.uriToSource.keys,
|
|
options.outputFile, depFile);
|
|
}
|
|
|
|
final generateSourceMaps = options.translatorOptions.generateSourceMaps;
|
|
final modules = translator.translate(sourceMapUrlGenerator);
|
|
final wasmModules = <String, ({Uint8List moduleBytes, String? sourceMap})>{};
|
|
modules.forEach((moduleOutput, module) {
|
|
final serializer = Serializer();
|
|
module.serialize(serializer);
|
|
final wasmModuleSerialized = serializer.data;
|
|
|
|
final sourceMap =
|
|
generateSourceMaps ? serializer.sourceMapSerializer.serialize() : null;
|
|
wasmModules[moduleOutput.moduleName] =
|
|
(moduleBytes: wasmModuleSerialized, sourceMap: sourceMap);
|
|
});
|
|
|
|
String jsRuntime = jsRuntimeFinalizer.generate(
|
|
translator.functions.translatedProcedures,
|
|
translator.internalizedStringsForJSRuntime,
|
|
translator.options.requireJsStringBuiltin,
|
|
mode);
|
|
|
|
final supportJs = _generateSupportJs(options.translatorOptions);
|
|
return CompilationSuccess(wasmModules, jsRuntime, supportJs);
|
|
}
|
|
|
|
String _generateSupportJs(TranslatorOptions options) {
|
|
// Copied from
|
|
// https://github.com/GoogleChromeLabs/wasm-feature-detect/blob/main/src/detectors/gc/index.js
|
|
//
|
|
// Uses WasmGC types and will only validate correctly if the engine supports
|
|
// WasmGC:
|
|
// ```
|
|
// (module
|
|
// (type $type0 (struct (field $field0 i8)))
|
|
// )
|
|
// ```
|
|
//
|
|
// NOTE: Once we support more feature detections we may use
|
|
// `package:wasm_builder` to create the module instead of having a fixed one
|
|
// here.
|
|
const String supportsWasmGC =
|
|
'WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,5,1,95,1,120,0]))';
|
|
|
|
// Imports a `js-string` builtin spec function *with wrong signature*. An engine
|
|
//
|
|
// * *without* knowledge about `js-string` builtin would accept such an import at
|
|
// validation time.
|
|
//
|
|
// * *with* knowledge about `js-string` would refuse it as the signature
|
|
// used to import the `cast` function is not according to `js-string` spec
|
|
//
|
|
// ```
|
|
// (module
|
|
// (func $wasm:js-string.cast (;0;) (import "wasm:js-string" "cast"))
|
|
// )
|
|
// ```
|
|
const String supportsJsStringBuiltins =
|
|
'!WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,2,23,1,14,119,97,115,109,58,106,115,45,115,116,114,105,110,103,4,99,97,115,116,0,0]),{"builtins":["js-string"]})';
|
|
|
|
final requiredFeatures = [
|
|
supportsWasmGC,
|
|
if (options.requireJsStringBuiltin) supportsJsStringBuiltins
|
|
];
|
|
return '(${requiredFeatures.join('&&')})';
|
|
}
|