Files
sdk/pkg/dart2wasm/lib/compile.dart
T
Martin Kustermann e3cf529f87 [dart2wasm] More compact encoding of deferred load lists
Measured on size of e main module (baseline is we don't embed
it in application code):

* embedding before: +16.5% uncompressed / +9.1% compressed
* embedding with this CL: +4% uncompressed / +4.3% compressed

When embeddeding deferred load list information into the app
(as opposed to a separate json file) we now use a more compact
encoding.

Specifically: Instead of encoding it as an array of an array of
strings (which are module names), we encode it as an array of an
array of module ids and construct the module name from the id.

To make the array of module ids more compact we utilize the fact
that we can sort them and encode in delta encoding (i.e. instead
of absolute module ids, encode the diff between previous module
id in the list).

We put the encoded module id lists in a data section and create
`WasmArray<WasmI8>`s from them at startup. When we trigger a load
we then decode them into the list of module names.

There's more opportunity to optimize it, but it's good to do
this as a first step.

Change-Id: I293fb8879d992fc370786f6c9b258ccd27e1559b
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/508980
Reviewed-by: Srujan Gaddam <srujzs@google.com>
Commit-Queue: Martin Kustermann <kustermann@google.com>
2026-06-05 00:50:46 -07:00

884 lines
26 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:convert';
import 'package:front_end/src/api_prototype/file_system.dart' show FileSystem;
import 'package:front_end/src/api_unstable/vm.dart'
show
CompilerOptions,
CompilerResult,
CfeDiagnosticMessage,
kernelForProgram,
CfeSeverity;
import 'package:kernel/ast.dart';
import 'package:kernel/class_hierarchy.dart';
import 'package:kernel/core_types.dart';
import 'package:kernel/library_index.dart';
import 'package:kernel/type_environment.dart';
import 'package:kernel/verifier.dart';
import 'package:path/path.dart' as path;
import 'package:pool/pool.dart' as pool;
import 'package:record_use/record_use.dart' as record_use;
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/record_use/record_use.dart' as record_use;
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/type_flow/utils.dart' as tfa_utils;
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 'generate_wasm.dart';
import 'io_util.dart';
import 'js/runtime_generator.dart' as js;
import 'modules.dart';
import 'record_class_generator.dart';
import 'records.dart';
import 'source_map_utils.dart';
import 'target.dart' as wasm show Mode;
import 'target.dart' hide Mode;
import 'translator.dart';
import 'util.dart';
sealed class CompilationResult {}
abstract class CompilationDryRunResult extends CompilationResult {}
class CompilationDryRunError extends CompilationDryRunResult {}
class CompilationDryRunSuccess extends CompilationDryRunResult {}
sealed class CompilationSuccess extends CompilationResult {}
class CfeResult extends CompilationSuccess {
final Component component;
final CoreTypes coreTypes;
CfeResult(this.component, this.coreTypes);
}
class TfaResult extends CompilationSuccess {
final Component component;
final CoreTypes coreTypes;
final LibraryIndex libraryIndex;
final ModuleStrategy moduleStrategy;
final Map<RecordShape, Class> recordClasses;
TfaResult(
this.component,
this.coreTypes,
this.libraryIndex,
this.moduleStrategy,
this.recordClasses,
);
}
class CodegenResult extends CompilationSuccess {
/// The main wasm file of the compiled application.
final String mainWasmFile;
/// The ids of all emitted wasm modules, including the special `0` id
/// for the main module.
final Set<int> moduleIds;
CodegenResult(this.mainWasmFile, this.moduleIds) {
assert(moduleIds.contains(compiler.WasmCompilerOptions.mainModuleId));
}
}
class OptResult extends CompilationSuccess {
final String mainWasmFile;
final int numModules;
OptResult(this.mainWasmFile, this.numModules);
}
abstract 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);
@override
String toString() => 'CFECrashError($error):\n$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);
}
List<String> librariesToIndex(wasm.Mode mode) {
return [
"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 (mode == .standalone) "dart:_embedder",
];
}
const List<String> _binaryenFlags = [
'--enable-gc',
'--enable-reference-types',
'--enable-multivalue',
'--enable-exception-handling',
'--enable-nontrapping-float-to-int',
'--enable-sign-ext',
'--enable-bulk-memory',
'--enable-threads',
'--enable-simd',
'--no-inline=*<noInline>*',
'--closed-world',
'--traps-never-happen',
'--type-unfinalizing',
'-Os',
'--type-ssa',
'--gufa',
'-Os',
'--type-merging',
'-Os',
'--type-finalizing',
'--minimize-rec-groups',
];
const List<String> _binaryenFlagsMultiModule = [
'--enable-gc',
'--enable-reference-types',
'--enable-multivalue',
'--enable-exception-handling',
'--enable-nontrapping-float-to-int',
'--enable-sign-ext',
'--enable-bulk-memory',
'--enable-threads',
'--enable-simd',
'--no-inline=*<noInline>*',
'--traps-never-happen',
'-Os',
'-Os',
];
/// 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> compile(
compiler.WasmCompilerOptions options,
CompilerPhaseInputOutputManager ioManager,
void Function(CfeDiagnosticMessage) handleDiagnosticMessage,
) async {
final wasm.Mode mode;
if (options.translatorOptions.jsCompatibility) {
mode = wasm.Mode.jsCompatibility;
} else if (options.translatorOptions.standalone) {
mode = wasm.Mode.standalone;
} else {
mode = wasm.Mode.regular;
}
final WasmTarget target = WasmTarget(
enableExperimentalFfi: options.translatorOptions.enableExperimentalFfi,
enableExperimentalWasmInterop:
options.translatorOptions.enableExperimentalWasmInterop,
removeAsserts: !options.translatorOptions.enableAsserts,
mode: mode,
);
CfeResult? cfeResult;
TfaResult? tfaResult;
CodegenResult? codegenResult;
CompilationResult? lastResult;
for (final phase in options.phases) {
switch (phase) {
case compiler.CompilerPhase.cfe:
lastResult = await _runCfePhase(
options,
target,
ioManager.fileSystem,
ioManager,
handleDiagnosticMessage,
);
if (lastResult is! CfeResult) return lastResult;
cfeResult = lastResult;
case compiler.CompilerPhase.tfa:
lastResult = await _runTfaPhase(
cfeResult ?? await _loadCfeResult(options, ioManager),
options,
target,
ioManager,
);
if (lastResult is! TfaResult) return lastResult;
tfaResult = lastResult;
case compiler.CompilerPhase.codegen:
lastResult = await _runCodegenPhase(
tfaResult ?? await _loadTfaResult(options, target, ioManager),
options,
ioManager,
);
if (lastResult is! CodegenResult) return lastResult;
codegenResult = lastResult;
case compiler.CompilerPhase.opt:
lastResult = await _runOptPhase(
codegenResult ?? await _loadCodegenResult(options, ioManager),
options,
ioManager,
);
if (lastResult is! OptResult) return lastResult;
}
}
return lastResult!;
}
Future<CfeResult> _loadCfeResult(
compiler.WasmCompilerOptions options,
CompilerPhaseInputOutputManager ioManager,
) async {
final component = Component();
await ioManager.readComponent(options.mainUri, component);
final coreTypes = CoreTypes(component);
return CfeResult(component, coreTypes);
}
Future<CompilationResult> _runCfePhase(
compiler.WasmCompilerOptions options,
WasmTarget target,
FileSystem fileSystem,
CompilerPhaseInputOutputManager ioManager,
void Function(CfeDiagnosticMessage) handleDiagnosticMessage,
) async {
var hadCompileTimeError = false;
void diagnosticMessageHandler(CfeDiagnosticMessage message) {
if (message.severity == CfeSeverity.error) {
hadCompileTimeError = true;
}
handleDiagnosticMessage(message);
}
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
..embedSourceText = options.translatorOptions.enableAsserts
..onDiagnostic = diagnosticMessageHandler
..fileSystem = fileSystem;
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 (options.dryRun) {
final component = compilerResult?.component;
if (component == null) {
return CompilationDryRunError();
}
final summarizer = DryRunSummarizer(
component,
enableExperimentalFfi: options.translatorOptions.enableExperimentalFfi,
);
final hasErrors = await summarizer.summarize();
return hasErrors ? CompilationDryRunError() : CompilationDryRunSuccess();
}
if (hadCompileTimeError) {
return CFECompileTimeErrors(compilerResult?.component);
}
final component = compilerResult!.component!;
if (options.dumpKernelAfterCfe != null) {
ioManager.writeComponentAsText(component, options.dumpKernelAfterCfe!);
}
if (options.emitCfe) {
await ioManager.writeComponent(component, options.outputFile);
}
return CfeResult(component, compilerResult.coreTypes!);
}
Future<TfaResult> _loadTfaResult(
compiler.WasmCompilerOptions options,
WasmTarget target,
CompilerPhaseInputOutputManager ioManager,
) async {
final component = createEmptyComponent();
final recordClassesRepository = _RecordClassesRepository();
component.addMetadataRepository(recordClassesRepository);
await ioManager.readComponent(options.mainUri, component);
final coreTypes = CoreTypes(component);
final libraryIndex = LibraryIndex(component, librariesToIndex(target.mode));
final classHierarchy = ClassHierarchy(component, coreTypes);
final moduleStrategy = await _createModuleStrategy(
options,
ioManager,
component,
coreTypes,
target,
classHierarchy,
);
final recordClasses = <RecordShape, Class>{};
recordClassesRepository.mapping.forEach((cls, shape) {
recordClasses[shape] = cls;
});
return TfaResult(
component,
coreTypes,
libraryIndex,
moduleStrategy,
recordClasses,
);
}
Future<CompilationResult> _runTfaPhase(
CfeResult cfeResult,
compiler.WasmCompilerOptions options,
WasmTarget target,
CompilerPhaseInputOutputManager ioManager,
) async {
var CfeResult(:component, :coreTypes) = cfeResult;
ClosedWorldClassHierarchy classHierarchy =
ClassHierarchy(component, coreTypes) as ClosedWorldClassHierarchy;
LibraryIndex libraryIndex = LibraryIndex(
component,
librariesToIndex(target.mode),
);
if (options.deleteToStringPackageUri.isNotEmpty) {
to_string_transformer.transformComponent(
component,
options.deleteToStringPackageUri,
);
}
if (!options.translatorOptions.standalone) {
js.performJSInteropTransformations(
component.libraries,
coreTypes,
classHierarchy,
);
}
final librariesToTransform = 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,
);
target.recordClasses = recordClasses;
if (options.dumpKernelBeforeTfa != null) {
ioManager.writeComponentAsText(component, options.dumpKernelBeforeTfa!);
}
final moduleStrategy = await _createModuleStrategy(
options,
ioManager,
component,
coreTypes,
target,
classHierarchy,
);
// Ensure we annotate AST nodes as entry points prior to other transformations
// looking at pragmas (such as mixin_deduplication and TFA).
moduleStrategy.addEntryPoints();
mixin_deduplication.transformLibraries(
librariesToTransform,
coreTypes,
target,
// This puts each canonical mixin application in its own library so that
// the import graph does not need to add edges to a single library
// containing all mixin applications.
useUniqueDeduplicationLibrary:
options.translatorOptions.enableDeferredLoading,
);
// Ensure this happens after mixin deduplication so that all libraries and
// classes are present.
moduleStrategy.prepareComponent();
final hasDeferredImports = component.libraries.any(
(lib) => lib.dependencies.any((d) => d.isDeferred),
);
if (hasDeferredImports) {
DeferredLoadingLowering.markRuntimeFunctionsAsEntrypoints(coreTypes);
}
_patchMainTearOffs(coreTypes, component);
// We initialize the [printStats] to `false` to prevent it's field
// initializer to run (which only works on VM -- but we want our compiler
// to also run if compiled via dart2js/dart2wasm)
tfa_utils.printStats = false;
// Keep the flags in-sync with
// pkg/vm/test/transformations/type_flow/transformer_test.dart
globalTypeFlow.transformComponent(
target,
coreTypes,
component,
useRapidTypeAnalysis: true,
treeShakeProtobufs:
options.translatorOptions.enableProtobufTreeShaker ||
options.translatorOptions.enableProtobufMixinTreeShaker,
treeShakeProtobufMixins:
options.translatorOptions.enableProtobufMixinTreeShaker,
);
// TFA may have tree shaken members that are in the library index cache.
// To avoid having dangling references in the index, we create a new one.
libraryIndex = LibraryIndex(component, librariesToIndex(target.mode));
if (options.emitTfa) {
// Store metadata needed for codegen so that it can be serialized.
final recordClassesRepo = _RecordClassesRepository();
recordClasses.forEach((shape, cls) {
recordClassesRepo.mapping[cls] = shape;
});
component.addMetadataRepository(recordClassesRepo);
}
assert(() {
verifyComponent(
target,
VerificationStage.afterGlobalTransformations,
component,
);
return true;
}());
if (options.dumpKernelAfterTfa != null) {
ioManager.writeComponentAsText(component, options.dumpKernelAfterTfa!);
}
if (options.emitTfa) {
await ioManager.writeComponent(component, options.outputFile);
}
return TfaResult(
component,
coreTypes,
libraryIndex,
moduleStrategy,
recordClasses,
);
}
Future<CodegenResult> _loadCodegenResult(
compiler.WasmCompilerOptions options,
CompilerPhaseInputOutputManager ioManager,
) async {
final mainUri = (await ioManager.resolveUri(options.mainUri))!.toFilePath();
return CodegenResult(mainUri, await ioManager.getModuleIds(mainUri));
}
Future<CompilationResult> _runCodegenPhase(
TfaResult tfaSuccess,
compiler.WasmCompilerOptions options,
CompilerPhaseInputOutputManager ioManager,
) async {
final TfaResult(
:component,
:coreTypes,
:moduleStrategy,
:libraryIndex,
:recordClasses,
) = tfaSuccess;
final loadingMap = DeferredModuleLoadingMap.fromComponent(component);
component.accept(DeferredLoadingLowering(coreTypes, loadingMap));
// May populate [loadingMap] by creating various [ModuleOutputData].
await moduleStrategy.processComponentAfterTfa(loadingMap);
final moduleOutputData = moduleStrategy.buildModuleOutputData();
final translator = Translator(
component,
coreTypes,
libraryIndex,
recordClasses,
loadingMap,
moduleOutputData,
options.translatorOptions,
);
String? depFile = options.depFile;
if (depFile != null) {
writeDepfile(
ioManager.fileSystem,
component.uriToSource.keys,
options.outputFile,
depFile,
);
}
final generateSourceMaps = options.translatorOptions.generateSourceMaps;
final modules = translator.translate(ioManager.sourceMapUrlGenerator);
final writeFutures = <Future<void>>[];
List<String?>? classNames;
if (generateSourceMaps && options.translatorOptions.minify) {
classNames = [];
for (var classId = 0; classId < translator.classes.length; classId += 1) {
classNames.add(translator.classes[classId].cls?.name);
}
}
modules.forEach((moduleMetadata, module) {
if (moduleMetadata.skipEmit) return;
final serializer = Serializer();
module.serialize(serializer);
writeFutures.add(
ioManager.writeWasmModule(serializer.data, moduleMetadata.moduleName),
);
if (generateSourceMaps) {
final sourceMapJson = serializer.sourceMapSerializer.serializeAsJson();
if (moduleMetadata.isMain && classNames != null) {
addMinifiedClassNames(sourceMapJson, classNames);
}
writeFutures.add(
ioManager.writeWasmSourceMap(
jsonEncode(sourceMapJson),
moduleMetadata.moduleName,
),
);
}
});
await Future.wait(writeFutures);
if (!options.translatorOptions.standalone) {
final jsRuntimeFinalizer = js.RuntimeFinalizer(
coreTypes,
translator.interopMemberNamer,
);
final jsRuntime = jsRuntimeFinalizer.generate(
moduleOutputData.mainModule.moduleImportName,
translator.functions.translatedProcedures,
translator.internalizedStringsForJSRuntime,
translator.options.requireJsStringBuiltin,
translator.options.enableDeferredLoading ||
translator.options.enableMultiModuleStressTestMode,
);
final supportJs = _generateSupportJs(options.translatorOptions);
final deferredMapFile = options.deferredMapUri;
if (deferredMapFile != null) {
await writeDeferredMapFile(component, coreTypes, options, loadingMap);
}
await ioManager.writeJsRuntime(jsRuntime);
await ioManager.writeSupportJs(supportJs);
}
final wasmOutputFilename = path.basename(options.outputFile);
final moduleIds = modules.keys
.map<int>(
(moduleMetadata) => WasmCompilerOptions.idForModuleName(
wasmOutputFilename,
moduleMetadata.moduleName,
)!,
)
.toSet();
if (options.recordedUsesFile != null) {
record_use.LoadingUnit loadingUnitForNode(TreeNode node) {
while (node is! NamedNode) {
node = node.parent!;
}
assert(node is Member || node is Class);
final moduleOutput = moduleOutputData.moduleForReference(node.reference);
if (moduleOutput == moduleOutputData.defaultModule &&
moduleOutputData.modules.length > 1) {
// This is an unassigned reference such as a constant class only
// used for annotations. Assign it to the main module as a placeholder.
return record_use.LoadingUnit(
moduleOutputData.mainModule.moduleImportName,
);
}
return record_use.LoadingUnit(moduleOutput.moduleImportName);
}
record_use.transformComponent(
component,
options.recordedUsesFile!,
loadingUnitLookup: loadingUnitForNode,
);
}
return CodegenResult(options.outputFile, moduleIds);
}
Future<CompilationResult> _runOptPhase(
CodegenResult codegenResult,
compiler.WasmCompilerOptions options,
CompilerPhaseInputOutputManager ioManager,
) async {
final moduleIdsToOptimize = options.moduleIdsToOptimize.isEmpty
? codegenResult.moduleIds
: options.moduleIdsToOptimize;
final numModules = moduleIdsToOptimize.length;
final optPool = pool.Pool(
options.maxActiveWasmOptProcesses == -1
? numModules
: options.maxActiveWasmOptProcesses,
);
await Future.wait([
for (final moduleId in moduleIdsToOptimize)
optPool.withResource(() async {
await ioManager.runWasmOpt(
codegenResult.mainWasmFile,
moduleId,
options.useMultiModuleOpt
? _binaryenFlagsMultiModule
: _binaryenFlags,
);
}),
]);
await optPool.close();
return OptResult(options.outputFile, numModules);
}
Future<ModuleStrategy> _createModuleStrategy(
compiler.WasmCompilerOptions options,
CompilerPhaseInputOutputManager ioManager,
Component component,
CoreTypes coreTypes,
WasmTarget target,
ClassHierarchy classHierarchy,
) async {
if (options.translatorOptions.enableDeferredLoading) {
return DeferredLoadingModuleStrategy(
component,
options,
target,
coreTypes,
ioManager,
);
} else if (options.translatorOptions.enableMultiModuleStressTestMode) {
return StressTestModuleStrategy(
component,
coreTypes,
options,
target,
classHierarchy,
);
}
return DefaultModuleStrategy(coreTypes, component, options);
}
// Patches `dart:_internal`s `mainTearOff{0,1,2}` getters.
void _patchMainTearOffs(CoreTypes coreTypes, Component component) {
final mainMethod = component.mainMethod!;
final mainMethodType = mainMethod.computeSignatureOrFunctionType();
Procedure lookup(String name) {
return coreTypes.index.getTopLevelProcedure('dart:_internal', name);
}
(Procedure, DartType) lookupWithType(String name) {
final p = lookup(name);
return (p, p.function.positionalParameters[1].type);
}
void patchInvokeMainInternal(Procedure p) {
final invoke = lookup('_invokeMainInternal');
invoke.isExternal = false;
invoke.function.body = ReturnStatement(
StaticInvocation(
p,
Arguments([
VariableGet(invoke.function.positionalParameters.single),
ConstantExpression(StaticTearOffConstant(mainMethod)),
]),
),
)..parent = invoke.function;
}
final typeEnv = TypeEnvironment(
coreTypes,
ClassHierarchy(component, coreTypes),
);
bool mainHasType(DartType type) => typeEnv.isSubtypeOf(mainMethodType, type);
final (mainInvoke0, mainArg0Type) = lookupWithType('_invokeMainArg0');
final (mainInvoke1, mainArg1Type) = lookupWithType('_invokeMainArg1');
final (mainInvoke2, mainArg2Type) = lookupWithType('_invokeMainArg2');
if (mainHasType(mainArg2Type)) {
return patchInvokeMainInternal(mainInvoke2);
}
if (mainHasType(mainArg1Type)) {
return patchInvokeMainInternal(mainInvoke1);
}
if (mainHasType(mainArg0Type)) {
return patchInvokeMainInternal(mainInvoke0);
}
throw 'Main method has unexpected type: $mainMethodType';
}
class _RecordClassesRepository extends MetadataRepository<RecordShape> {
static const String _tag = 'dart2wasm.recordClasses';
@override
final Map<Class, RecordShape> mapping = {};
@override
RecordShape readFromBinary(Node node, BinarySource source) {
final positionals = source.readUInt30();
final namesLength = source.readUInt30();
final names = namesLength == 0 ? const <String>[] : <String>[];
for (int i = 0; i < namesLength; i++) {
names.add(source.readStringReference());
}
return RecordShape(positionals, names);
}
@override
String get tag => _tag;
@override
void writeToBinary(RecordShape metadata, Node node, BinarySink sink) {
sink.writeUInt30(metadata.positionals);
sink.writeUInt30(metadata.names.length);
for (final name in metadata.names) {
sink.writeStringReference(name);
}
}
}
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('&&')})';
}