blob: e2158e5e2c16269e4339f79a2db52eda0fe1e103 [file] [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: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 '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,
);
}
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);
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);
}
final wasmOutputFilename = path.basename(options.outputFile);
final moduleIds = modules.keys
.map<int>(
(moduleMetadata) => options.idForModuleName(
wasmOutputFilename,
moduleMetadata.moduleName,
)!,
)
.toSet();
await ioManager.writeJsRuntime(jsRuntime);
await ioManager.writeSupportJs(supportJs);
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('&&')})';
}