blob: c835c7949ca951e3886b4b5b0131d1e0b062d74d [file] [log] [blame] [edit]
// 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:io' show File;
import 'dart:typed_data';
import 'package:build_integration/file_system/multi_root.dart'
show MultiRootFileSystem, MultiRootFileSystemEntity;
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,
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/type_environment.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 transformLibraries;
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';
import 'dry_run.dart';
import 'dynamic_module_kernel_metadata.dart';
import 'dynamic_modules.dart';
import 'js/runtime_generator.dart' as js;
import 'modules.dart';
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 {}
abstract class CompilationDryRunResult extends CompilationResult {}
class CompilationDryRunError extends CompilationDryRunResult {}
class CompilationDryRunSuccess extends CompilationDryRunResult {}
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 {
final Component? component;
CFECompileTimeErrors(this.component);
}
const List<String> _librariesToIndex = [
"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",
];
/// 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',
'dart.tool.dart2wasm.minify': '${options.translatorOptions.minify}',
...options.environment,
}
..explicitExperimentalFlags = options.feExperimentalFlags
..verbose = false
..onDiagnostic = diagnosticMessageHandler;
if (options.multiRootScheme != null) {
compilerOptions.fileSystem = MultiRootFileSystem(
options.multiRootScheme!,
options.multiRoots.isEmpty ? [Uri.base] : options.multiRoots,
StandardFileSystem.instance);
}
Future<Uri?> resolveUri(Uri? uri) async {
if (uri == null) return null;
var fileSystemEntity = compilerOptions.fileSystem.entityForUri(uri);
if (fileSystemEntity is MultiRootFileSystemEntity) {
fileSystemEntity = await fileSystemEntity.delegate;
}
return fileSystemEntity.uri;
}
if (options.platformPath != null) {
compilerOptions.sdkSummary = options.platformPath;
} else {
compilerOptions.compileSdk = true;
}
final dynamicMainModuleUri = await resolveUri(options.dynamicMainModuleUri);
final dynamicInterfaceUri = await resolveUri(options.dynamicInterfaceUri);
final isDynamicMainModule =
options.dynamicModuleType == DynamicModuleType.main;
final isDynamicSubmodule =
options.dynamicModuleType == DynamicModuleType.submodule;
if (isDynamicSubmodule) {
compilerOptions.additionalDills.add(dynamicMainModuleUri!);
if (options.validateDynamicModules) {
// We must pass the unresolved URI here to be compatible with the CFE
// dynamic interface validator.
compilerOptions.dynamicInterfaceSpecificationUri =
options.dynamicInterfaceUri;
}
}
CompilerResult? compilerResult;
try {
compilerResult = await kernelForProgram(options.mainUri, compilerOptions,
requireMain: !isDynamicSubmodule);
} catch (e, s) {
return CFECrashError(e, s);
}
if (options.dryRun) {
final component = compilerResult?.component;
if (component == null) {
return CompilationDryRunError();
}
final summarizer = DryRunSummarizer(component);
final hasErrors = summarizer.summarize();
return hasErrors ? CompilationDryRunError() : CompilationDryRunSuccess();
}
if (hadCompileTimeError) {
return CFECompileTimeErrors(compilerResult?.component);
}
assert(compilerResult != null);
Component component = compilerResult!.component!;
CoreTypes coreTypes = compilerResult.coreTypes!;
ClosedWorldClassHierarchy classHierarchy =
ClassHierarchy(component, coreTypes) as ClosedWorldClassHierarchy;
LibraryIndex libraryIndex = LibraryIndex(component, _librariesToIndex);
if (options.dumpKernelAfterCfe != null) {
writeComponentToText(component, path: options.dumpKernelAfterCfe!);
}
if (options.deleteToStringPackageUri.isNotEmpty) {
to_string_transformer.transformComponent(
component, options.deleteToStringPackageUri);
}
var jsInteropMethods = js.performJSInteropTransformations(
component.getDynamicSubmoduleLibraries(coreTypes),
coreTypes,
classHierarchy);
if (isDynamicSubmodule) {
// Join the submodule libraries with the TFAed component from the main
// module compilation. JS interop transformer must be run before this since
// some methods it uses may have been tree-shaken from the TFAed component.
(component, jsInteropMethods) = await generateDynamicSubmoduleComponent(
component, coreTypes, dynamicMainModuleUri!, jsInteropMethods);
coreTypes = CoreTypes(component);
classHierarchy =
ClassHierarchy(component, coreTypes) as ClosedWorldClassHierarchy;
libraryIndex = LibraryIndex(component, _librariesToIndex);
}
final librariesToTransform = isDynamicSubmodule
? component.getDynamicSubmoduleLibraries(coreTypes)
: component.libraries;
final constantEvaluator = ConstantEvaluator(
options, target, component, coreTypes, classHierarchy, libraryIndex);
unreachable_code_elimination.transformLibraries(target, librariesToTransform,
constantEvaluator, options.translatorOptions.enableAsserts);
final Map<RecordShape, Class> recordClasses = generateRecordClasses(
component, coreTypes,
isDynamicMainModule: isDynamicMainModule,
isDynamicSubmodule: isDynamicSubmodule);
target.recordClasses = recordClasses;
if (options.dumpKernelBeforeTfa != null) {
writeComponentToText(component, path: options.dumpKernelBeforeTfa!);
}
mixin_deduplication.transformLibraries(librariesToTransform);
ModuleStrategy moduleStrategy;
if (options.translatorOptions.enableDeferredLoading) {
moduleStrategy =
DeferredLoadingModuleStrategy(component, options, target, coreTypes);
} else if (options.translatorOptions.enableMultiModuleStressTestMode) {
moduleStrategy =
StressTestModuleStrategy(component, coreTypes, target, classHierarchy);
} else if (isDynamicMainModule) {
moduleStrategy = DynamicMainModuleStrategy(
component,
coreTypes,
File.fromUri(dynamicInterfaceUri!).readAsStringSync(),
options.dynamicInterfaceUri!);
} else if (isDynamicSubmodule) {
moduleStrategy = DynamicSubmoduleStrategy(
component, options, target, coreTypes, dynamicMainModuleUri!);
} else {
moduleStrategy = DefaultModuleStrategy(component);
}
moduleStrategy.prepareComponent();
MainModuleMetadata mainModuleMetadata =
MainModuleMetadata.empty(options.translatorOptions, options.environment);
if (isDynamicSubmodule) {
mainModuleMetadata =
await deserializeMainModuleMetadata(component, options);
mainModuleMetadata.verifyDynamicSubmoduleOptions(options);
} else if (isDynamicMainModule) {
MainModuleMetadata.verifyMainModuleOptions(options);
await serializeMainModuleComponent(component, dynamicMainModuleUri!,
optimized: false);
}
if (!isDynamicSubmodule) {
_patchMainTearOffs(coreTypes, component);
// 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 = moduleStrategy.buildModuleOutputData();
var translator = Translator(component, coreTypes, libraryIndex, recordClasses,
moduleOutputData, options.translatorOptions,
mainModuleMetadata: mainModuleMetadata,
enableDynamicModules: options.enableDynamicModules);
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) {
if (moduleOutput.skipEmit) return;
final serializer = Serializer();
module.serialize(serializer);
final wasmModuleSerialized = serializer.data;
final sourceMap =
generateSourceMaps ? serializer.sourceMapSerializer.serialize() : null;
wasmModules[moduleOutput.moduleName] =
(moduleBytes: wasmModuleSerialized, sourceMap: sourceMap);
});
final jsRuntimeFinalizer = js.RuntimeFinalizer(jsInteropMethods);
final jsRuntime = isDynamicSubmodule
? jsRuntimeFinalizer.generateDynamicSubmodule(
translator.functions.translatedProcedures,
translator.options.requireJsStringBuiltin,
translator.internalizedStringsForJSRuntime)
: jsRuntimeFinalizer.generate(
translator.functions.translatedProcedures,
translator.internalizedStringsForJSRuntime,
translator.options.requireJsStringBuiltin,
translator.options.enableDeferredLoading ||
translator.options.enableMultiModuleStressTestMode ||
translator.dynamicModuleSupportEnabled);
final supportJs = _generateSupportJs(options.translatorOptions);
if (isDynamicMainModule) {
await serializeMainModuleMetadata(component, translator, options);
await serializeMainModuleComponent(component, dynamicMainModuleUri!,
optimized: true);
}
return CompilationSuccess(wasmModules, jsRuntime, supportJs);
}
// Patches `dart:_internal`s `mainTearOff{0,1,2}` getters.
void _patchMainTearOffs(CoreTypes coreTypes, Component component) {
final mainMethod = component.mainMethod!;
final mainMethodType = mainMethod.computeSignatureOrFunctionType();
void patchToReturnMainTearOff(Procedure p) {
p.function.body =
ReturnStatement(ConstantExpression(StaticTearOffConstant(mainMethod)))
..parent = p.function;
}
final typeEnv =
TypeEnvironment(coreTypes, ClassHierarchy(component, coreTypes));
bool mainHasType(DartType type) => typeEnv.isSubtypeOf(mainMethodType, type);
final internalLib = coreTypes.index.getLibrary('dart:_internal');
(Procedure, DartType) lookupAndInitialize(String name) {
final p = internalLib.procedures
.singleWhere((procedure) => procedure.name.text == name);
p.isExternal = false;
p.function.body = ReturnStatement(NullLiteral())..parent = p.function;
return (p, p.function.returnType.toNonNull());
}
final (mainTearOff0, mainArg0Type) = lookupAndInitialize('mainTearOffArg0');
final (mainTearOff1, mainArg1Type) = lookupAndInitialize('mainTearOffArg1');
final (mainTearOff2, mainArg2Type) = lookupAndInitialize('mainTearOffArg2');
if (mainHasType(mainArg2Type)) return patchToReturnMainTearOff(mainTearOff2);
if (mainHasType(mainArg1Type)) return patchToReturnMainTearOff(mainTearOff1);
if (mainHasType(mainArg0Type)) return patchToReturnMainTearOff(mainTearOff0);
}
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('&&')})';
}