From 6faa5f3bd00ad8cbc640b3fc80cf7466c002a7df Mon Sep 17 00:00:00 2001 From: Aske Simon Christensen Date: Wed, 16 Feb 2022 11:11:14 +0000 Subject: [PATCH] [dart2wasm] Initial commit for the Dart-to-WasmGC compiler. This is work in progress. Several language features are still unimplemented or only partially implemented. Instructions for running the compiler and its output can be found in pkg/dart2wasm/dart2wasm.md. These procedures are preliminary and expected to change. The best version of d8 to use for this version of dart2wasm is 10.0.40, as explained here: https://dart-review.googlesource.com/c/sdk/+/232097 This commit also adds a dart2wasm-hostasserts-linux-x64-d8 testing configuration to run the compiler over the test suite. The history of the prototype that this is based on can be seen here: https://github.com/askeksa-google/sdk/tree/wasm_prototype Issue: https://github.com/dart-lang/sdk/issues/32894 Change-Id: I910b6ff239ef9c5f66863e4ca97b39b8202cce85 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/175728 Reviewed-by: Joshua Litt Commit-Queue: Aske Simon Christensen --- .dart_tool/package_config.json | 12 + .packages | 2 + pkg/dart2wasm/bin/dart2wasm.dart | 99 + pkg/dart2wasm/bin/run_wasm.js | 63 + pkg/dart2wasm/dart2wasm.md | 65 + pkg/dart2wasm/lib/class_info.dart | 377 +++ pkg/dart2wasm/lib/closures.dart | 318 +++ pkg/dart2wasm/lib/code_generator.dart | 1940 ++++++++++++++ pkg/dart2wasm/lib/compile.dart | 69 + pkg/dart2wasm/lib/constants.dart | 637 +++++ pkg/dart2wasm/lib/constants_backend.dart | 110 + pkg/dart2wasm/lib/dispatch_table.dart | 308 +++ pkg/dart2wasm/lib/functions.dart | 216 ++ pkg/dart2wasm/lib/globals.dart | 198 ++ pkg/dart2wasm/lib/intrinsics.dart | 931 +++++++ pkg/dart2wasm/lib/param_info.dart | 93 + pkg/dart2wasm/lib/reference_extensions.dart | 62 + pkg/dart2wasm/lib/target.dart | 191 ++ pkg/dart2wasm/lib/transformers.dart | 108 + pkg/dart2wasm/lib/translator.dart | 819 ++++++ pkg/dart2wasm/pubspec.yaml | 26 + pkg/smith/lib/configuration.dart | 11 + pkg/test_runner/lib/src/command.dart | 3 + pkg/test_runner/lib/src/command_output.dart | 39 + .../lib/src/compiler_configuration.dart | 80 + pkg/test_runner/lib/src/configuration.dart | 1 + .../lib/src/runtime_configuration.dart | 2 +- pkg/test_runner/lib/src/test_suite.dart | 2 + pkg/wasm_builder/LICENSE | 26 + pkg/wasm_builder/lib/src/instructions.dart | 2283 +++++++++++++++++ pkg/wasm_builder/lib/src/module.dart | 820 ++++++ pkg/wasm_builder/lib/src/serialize.dart | 137 + pkg/wasm_builder/lib/src/types.dart | 657 +++++ pkg/wasm_builder/lib/wasm_builder.dart | 34 + pkg/wasm_builder/pubspec.yaml | 9 + sdk/bin/dart2wasm | 48 + sdk/bin/dart2wasm_developer | 6 + sdk/lib/_internal/vm/lib/compact_hash.dart | 3 + .../_internal/vm/lib/typed_data_patch.dart | 23 + sdk/lib/_internal/wasm/lib/bool.dart | 21 + sdk/lib/_internal/wasm/lib/class_id.dart | 20 + sdk/lib/_internal/wasm/lib/core_patch.dart | 51 + sdk/lib/_internal/wasm/lib/date_patch.dart | 542 ++++ sdk/lib/_internal/wasm/lib/developer.dart | 61 + sdk/lib/_internal/wasm/lib/double.dart | 290 +++ sdk/lib/_internal/wasm/lib/expando_patch.dart | 13 + sdk/lib/_internal/wasm/lib/function.dart | 12 + sdk/lib/_internal/wasm/lib/growable_list.dart | 305 +++ .../_internal/wasm/lib/hash_factories.dart | 51 + .../_internal/wasm/lib/identical_patch.dart | 12 + sdk/lib/_internal/wasm/lib/immutable_map.dart | 221 ++ sdk/lib/_internal/wasm/lib/int.dart | 610 +++++ .../_internal/wasm/lib/internal_patch.dart | 128 + sdk/lib/_internal/wasm/lib/list.dart | 223 ++ sdk/lib/_internal/wasm/lib/math_patch.dart | 262 ++ sdk/lib/_internal/wasm/lib/object_patch.dart | 49 + sdk/lib/_internal/wasm/lib/patch.dart | 9 + sdk/lib/_internal/wasm/lib/print_patch.dart | 9 + .../wasm/lib/string_buffer_patch.dart | 212 ++ sdk/lib/_internal/wasm/lib/string_patch.dart | 1391 ++++++++++ sdk/lib/_internal/wasm/lib/timer_patch.dart | 82 + sdk/lib/_internal/wasm/lib/type.dart | 47 + sdk/lib/libraries.json | 75 + sdk/lib/libraries.yaml | 60 + sdk/lib/wasm/wasm_types.dart | 90 + tests/language/language_dart2wasm.status | 10 + tools/bots/test_matrix.json | 69 + 67 files changed, 15752 insertions(+), 1 deletion(-) create mode 100644 pkg/dart2wasm/bin/dart2wasm.dart create mode 100644 pkg/dart2wasm/bin/run_wasm.js create mode 100644 pkg/dart2wasm/dart2wasm.md create mode 100644 pkg/dart2wasm/lib/class_info.dart create mode 100644 pkg/dart2wasm/lib/closures.dart create mode 100644 pkg/dart2wasm/lib/code_generator.dart create mode 100644 pkg/dart2wasm/lib/compile.dart create mode 100644 pkg/dart2wasm/lib/constants.dart create mode 100644 pkg/dart2wasm/lib/constants_backend.dart create mode 100644 pkg/dart2wasm/lib/dispatch_table.dart create mode 100644 pkg/dart2wasm/lib/functions.dart create mode 100644 pkg/dart2wasm/lib/globals.dart create mode 100644 pkg/dart2wasm/lib/intrinsics.dart create mode 100644 pkg/dart2wasm/lib/param_info.dart create mode 100644 pkg/dart2wasm/lib/reference_extensions.dart create mode 100644 pkg/dart2wasm/lib/target.dart create mode 100644 pkg/dart2wasm/lib/transformers.dart create mode 100644 pkg/dart2wasm/lib/translator.dart create mode 100644 pkg/dart2wasm/pubspec.yaml create mode 100644 pkg/wasm_builder/LICENSE create mode 100644 pkg/wasm_builder/lib/src/instructions.dart create mode 100644 pkg/wasm_builder/lib/src/module.dart create mode 100644 pkg/wasm_builder/lib/src/serialize.dart create mode 100644 pkg/wasm_builder/lib/src/types.dart create mode 100644 pkg/wasm_builder/lib/wasm_builder.dart create mode 100644 pkg/wasm_builder/pubspec.yaml create mode 100755 sdk/bin/dart2wasm create mode 100755 sdk/bin/dart2wasm_developer create mode 100644 sdk/lib/_internal/wasm/lib/bool.dart create mode 100644 sdk/lib/_internal/wasm/lib/class_id.dart create mode 100644 sdk/lib/_internal/wasm/lib/core_patch.dart create mode 100644 sdk/lib/_internal/wasm/lib/date_patch.dart create mode 100644 sdk/lib/_internal/wasm/lib/developer.dart create mode 100644 sdk/lib/_internal/wasm/lib/double.dart create mode 100644 sdk/lib/_internal/wasm/lib/expando_patch.dart create mode 100644 sdk/lib/_internal/wasm/lib/function.dart create mode 100644 sdk/lib/_internal/wasm/lib/growable_list.dart create mode 100644 sdk/lib/_internal/wasm/lib/hash_factories.dart create mode 100644 sdk/lib/_internal/wasm/lib/identical_patch.dart create mode 100644 sdk/lib/_internal/wasm/lib/immutable_map.dart create mode 100644 sdk/lib/_internal/wasm/lib/int.dart create mode 100644 sdk/lib/_internal/wasm/lib/internal_patch.dart create mode 100644 sdk/lib/_internal/wasm/lib/list.dart create mode 100644 sdk/lib/_internal/wasm/lib/math_patch.dart create mode 100644 sdk/lib/_internal/wasm/lib/object_patch.dart create mode 100644 sdk/lib/_internal/wasm/lib/patch.dart create mode 100644 sdk/lib/_internal/wasm/lib/print_patch.dart create mode 100644 sdk/lib/_internal/wasm/lib/string_buffer_patch.dart create mode 100644 sdk/lib/_internal/wasm/lib/string_patch.dart create mode 100644 sdk/lib/_internal/wasm/lib/timer_patch.dart create mode 100644 sdk/lib/_internal/wasm/lib/type.dart create mode 100644 sdk/lib/wasm/wasm_types.dart create mode 100644 tests/language/language_dart2wasm.status diff --git a/.dart_tool/package_config.json b/.dart_tool/package_config.json index 76aed36953b..66d9ee65f21 100644 --- a/.dart_tool/package_config.json +++ b/.dart_tool/package_config.json @@ -228,6 +228,12 @@ "packageUri": "lib/", "languageVersion": "2.12" }, + { + "name": "dart2wasm", + "rootUri": "../pkg/dart2wasm", + "packageUri": "lib/", + "languageVersion": "2.12" + }, { "name": "dart_internal", "rootUri": "../pkg/dart_internal", @@ -780,6 +786,12 @@ "packageUri": "lib/", "languageVersion": "2.12" }, + { + "name": "wasm_builder", + "rootUri": "../pkg/wasm_builder", + "packageUri": "lib/", + "languageVersion": "2.12" + }, { "name": "watcher", "rootUri": "../third_party/pkg/watcher", diff --git a/.packages b/.packages index e8855d6415a..38ab4a9bd64 100644 --- a/.packages +++ b/.packages @@ -33,6 +33,7 @@ dart2js_info:pkg/dart2js_info/lib dart2js_runtime_metrics:pkg/dart2js_runtime_metrics/lib dart2js_tools:pkg/dart2js_tools/lib dart2native:pkg/dart2native/lib +dart2wasm:pkg/dart2wasm/lib dart_internal:pkg/dart_internal/lib dart_style:third_party/pkg_tested/dart_style/lib dartdev:pkg/dartdev/lib @@ -117,6 +118,7 @@ vector_math:third_party/pkg/vector_math/lib vm:pkg/vm/lib vm_service:pkg/vm_service/lib vm_snapshot_analysis:pkg/vm_snapshot_analysis/lib +wasm_builder:pkg/wasm_builder/lib watcher:third_party/pkg/watcher/lib webdriver:third_party/pkg/webdriver/lib webkit_inspection_protocol:third_party/pkg/webkit_inspection_protocol/lib diff --git a/pkg/dart2wasm/bin/dart2wasm.dart b/pkg/dart2wasm/bin/dart2wasm.dart new file mode 100644 index 00000000000..10f175084f8 --- /dev/null +++ b/pkg/dart2wasm/bin/dart2wasm.dart @@ -0,0 +1,99 @@ +// 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'; +import 'dart:typed_data'; + +import 'package:front_end/src/api_unstable/vm.dart' + show printDiagnosticMessage, resolveInputUri; + +import 'package:dart2wasm/compile.dart'; +import 'package:dart2wasm/translator.dart'; + +final Map boolOptionMap = { + "export-all": (o, value) => o.exportAll = value, + "inlining": (o, value) => o.inlining = value, + "lazy-constants": (o, value) => o.lazyConstants = value, + "local-nullability": (o, value) => o.localNullability = value, + "name-section": (o, value) => o.nameSection = value, + "nominal-types": (o, value) => o.nominalTypes = value, + "parameter-nullability": (o, value) => o.parameterNullability = value, + "polymorphic-specialization": (o, value) => + o.polymorphicSpecialization = value, + "print-kernel": (o, value) => o.printKernel = value, + "print-wasm": (o, value) => o.printWasm = value, + "runtime-types": (o, value) => o.runtimeTypes = value, + "string-data-segments": (o, value) => o.stringDataSegments = value, +}; +final Map intOptionMap = { + "watch": (o, value) => (o.watchPoints ??= []).add(value), +}; + +Never usage(String message) { + print("Usage: dart2wasm [] "); + print(""); + print("Options:"); + print(" --dart-sdk="); + print(""); + for (String option in boolOptionMap.keys) { + print(" --[no-]$option"); + } + print(""); + for (String option in intOptionMap.keys) { + print(" --$option "); + } + print(""); + + throw message; +} + +Future main(List args) async { + Uri sdkPath = Platform.script.resolve("../../../sdk"); + TranslatorOptions options = TranslatorOptions(); + List nonOptions = []; + void Function(TranslatorOptions, int)? intOptionFun = null; + for (String arg in args) { + if (intOptionFun != null) { + intOptionFun(options, int.parse(arg)); + intOptionFun = null; + } else if (arg.startsWith("--dart-sdk=")) { + String path = arg.substring("--dart-sdk=".length); + sdkPath = Uri.file(Directory(path).absolute.path); + } else if (arg.startsWith("--no-")) { + var optionFun = boolOptionMap[arg.substring(5)]; + if (optionFun == null) usage("Unknown option $arg"); + optionFun(options, false); + } else if (arg.startsWith("--")) { + var optionFun = boolOptionMap[arg.substring(2)]; + if (optionFun != null) { + optionFun(options, true); + } else { + intOptionFun = intOptionMap[arg.substring(2)]; + if (intOptionFun == null) usage("Unknown option $arg"); + } + } else { + nonOptions.add(arg); + } + } + if (intOptionFun != null) { + usage("Missing argument to ${args.last}"); + } + + if (nonOptions.length != 2) usage("Requires two file arguments"); + String input = nonOptions[0]; + String output = nonOptions[1]; + Uri mainUri = resolveInputUri(input); + + Uint8List? module = await compileToModule(mainUri, sdkPath, options, + (message) => printDiagnosticMessage(message, print)); + + if (module == null) { + exitCode = 1; + return exitCode; + } + + await File(output).writeAsBytes(module); + + return 0; +} diff --git a/pkg/dart2wasm/bin/run_wasm.js b/pkg/dart2wasm/bin/run_wasm.js new file mode 100644 index 00000000000..08a6d92f0d6 --- /dev/null +++ b/pkg/dart2wasm/bin/run_wasm.js @@ -0,0 +1,63 @@ +// 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. +// +// Runner V8 script for testing dart2wasm, takes ".wasm" file as argument. +// Run as follows: +// +// $> d8 --experimental-wasm-gc --wasm-gc-js-interop run_wasm.js -- .wasm + +function stringFromDartString(string) { + var length = inst.exports.$stringLength(string); + var array = new Array(length); + for (var i = 0; i < length; i++) { + array[i] = inst.exports.$stringRead(string, i); + } + return String.fromCharCode(...array); +} + +function stringToDartString(string) { + var length = string.length; + var range = 0; + for (var i = 0; i < length; i++) { + range |= string.codePointAt(i); + } + if (range < 256) { + var dartString = inst.exports.$stringAllocate1(length); + for (var i = 0; i < length; i++) { + inst.exports.$stringWrite1(dartString, i, string.codePointAt(i)); + } + return dartString; + } else { + var dartString = inst.exports.$stringAllocate2(length); + for (var i = 0; i < length; i++) { + inst.exports.$stringWrite2(dartString, i, string.codePointAt(i)); + } + return dartString; + } +} + +// Imports for printing and event loop +var dart2wasm = { + printToConsole: function(string) { + console.log(stringFromDartString(string)) + }, + scheduleCallback: function(milliseconds, closure) { + setTimeout(function() { + inst.exports.$call0(closure); + }, milliseconds); + } +}; + +// Create a Wasm module from the binary wasm file. +var bytes = readbuffer(arguments[0]); +var module = new WebAssembly.Module(bytes); + +// Instantiate the Wasm module, importing from the global scope. +var importObject = (typeof window !== 'undefined') + ? window + : Realm.global(Realm.current()); +var inst = new WebAssembly.Instance(module, importObject); + +var result = inst.exports.main(); +if (result) console.log(result); diff --git a/pkg/dart2wasm/dart2wasm.md b/pkg/dart2wasm/dart2wasm.md new file mode 100644 index 00000000000..8dc3203c294 --- /dev/null +++ b/pkg/dart2wasm/dart2wasm.md @@ -0,0 +1,65 @@ +## Running dart2wasm + +You don't need to build the Dart SDK to run dart2wasm, as long as you have a Dart SDK installed. + +To compile a Dart file to Wasm, run: + +`dart --enable-asserts pkg/dart2wasm/bin/dart2wasm.dart` *options* *infile*`.dart` *outfile*`.wasm` + +where *options* include: + +| Option | Default | Description | +| --------------------------------------- | ------- | ----------- | +| `--dart-sdk=`*path* | relative to script | The location of the `sdk` directory inside the Dart SDK, containing the core library sources. +| `--`[`no-`]`export-all` | no | Export all functions; otherwise, just export `main`. +| `--`[`no-`]`inlining` | no | Inline small functions. +| `--`[`no-`]`lazy-constants` | no | Instantiate constants lazily. +| `--`[`no-`]`local-nullability` | no | Use non-nullable types for non-nullable locals and temporaries. +| `--`[`no-`]`name-section` | yes | Emit Name Section with function names. +| `--`[`no-`]`nominal-types` | no | Emit experimental nominal types. +| `--`[`no-`]`parameter-nullability` | yes | Use non-nullable types for non-nullable parameters and return values. +| `--`[`no-`]`polymorphic-specialization` | no | Do virtual calls by switching on the class ID instead of using `call_indirect`. +| `--`[`no-`]`print-kernel` | no | Print IR for each function before compiling it. +| `--`[`no-`]`print-wasm` | no | Print Wasm instructions of each compiled function. +| `--`[`no-`]`runtime-types` | yes | Use RTTs for allocations and casts. +| `--`[`no-`]`string-data-segments` | no | Use experimental array init from data segment for string constants. +| `--watch` *offset* | | Print stack trace leading to the byte at offset *offset* in the `.wasm` output file. Can be specified multiple times. + +The resulting `.wasm` file can be run with: + +`d8 --experimental-wasm-gc --wasm-gc-js-interop pkg/dart2wasm/bin/run_wasm.js -- `*outfile*`.wasm` + +## Imports and exports + +To import a function, declare it as a global, external function and mark it with a `wasm:import` pragma indicating the imported name (which must be two identifiers separated by a dot): +```dart +@pragma("wasm:import", "foo.bar") +external void fooBar(Object object); +``` +which will call `foo.bar` on the host side: +```javascript +var foo = { + bar: function(object) { /* implementation here */ } +}; +``` +To export a function, mark it with a `wasm:export` pragma: +```dart +@pragma("wasm:export") +void foo(double x) { /* implementation here */ } + +@pragma("wasm:export", "baz") +void bar(double x) { /* implementation here */ } +``` +With the Wasm module instance in `inst`, these can be called as: +```javascript +inst.exports.foo(1); +inst.exports.baz(2); +``` + +### Types to use for interop + +In the signatures of imported and exported functions, use the following types: + +- For numbers, use `double`. +- For Dart objects, use the corresponding Dart type. The fields of the underlying representation can be accessed on the JS side as `.$field0`, `.$field1` etc., but there is currently no defined way of finding the field index of a particular Dart field, so this mechanism is mainly useful for special objects with known layout. +- For JS objects, use the `WasmAnyRef` type (or `WasmAnyRef?` as applicable) from the `dart:wasm` package. These can be passed around and stored as opaque values on the Dart side. diff --git a/pkg/dart2wasm/lib/class_info.dart b/pkg/dart2wasm/lib/class_info.dart new file mode 100644 index 00000000000..54037e02f33 --- /dev/null +++ b/pkg/dart2wasm/lib/class_info.dart @@ -0,0 +1,377 @@ +// 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:math'; + +import 'package:dart2wasm/translator.dart'; + +import 'package:kernel/ast.dart'; + +import 'package:wasm_builder/wasm_builder.dart' as w; + +/// Wasm struct field indices for fields that are accessed explicitly from Wasm +/// code, e.g. in intrinsics. +/// +/// The values are validated by asserts, typically either through +/// [ClassInfo.addField] (for manually added fields) or by a line in +/// [FieldIndex.validate] (for fields declared in Dart code). +class FieldIndex { + static const classId = 0; + static const boxValue = 1; + static const identityHash = 1; + static const stringArray = 2; + static const closureContext = 2; + static const closureFunction = 3; + static const typedListBaseLength = 2; + static const typedListArray = 3; + static const typedListViewTypedData = 3; + static const typedListViewOffsetInBytes = 4; + static const byteDataViewLength = 2; + static const byteDataViewTypedData = 3; + static const byteDataViewOffsetInBytes = 4; + + static void validate(Translator translator) { + void check(Class cls, String name, int expectedIndex) { + assert( + translator.fieldIndex[ + cls.fields.firstWhere((f) => f.name.text == name)] == + expectedIndex, + "Unexpected field index for ${cls.name}.$name"); + } + + check(translator.boxedBoolClass, "value", FieldIndex.boxValue); + check(translator.boxedIntClass, "value", FieldIndex.boxValue); + check(translator.boxedDoubleClass, "value", FieldIndex.boxValue); + check(translator.oneByteStringClass, "_array", FieldIndex.stringArray); + check(translator.twoByteStringClass, "_array", FieldIndex.stringArray); + check(translator.functionClass, "context", FieldIndex.closureContext); + } +} + +const int initialIdentityHash = 0; + +/// Information about the Wasm representation for a class. +class ClassInfo { + /// The Dart class that this info corresponds to. The top type does not have + /// an associated Dart class. + final Class? cls; + + /// The Class ID of this class, stored in every instance of the class. + final int classId; + + /// Depth of this class in the Wasm type hierarchy. + final int depth; + + /// The Wasm struct used to represent instances of this class. A class will + /// sometimes use the same struct as its superclass. + final w.StructType struct; + + /// Wasm global containing the RTT for this class. + late final w.DefinedGlobal rtt; + + /// The superclass for this class. This will usually be the Dart superclass, + /// but there are a few exceptions, where the Wasm type hierarchy does not + /// follow the Dart class hierarchy. + final ClassInfo? superInfo; + + /// For every type parameter which is directly mapped to a type parameter in + /// the superclass, this contains the corresponding superclass type + /// parameter. These will reuse the corresponding type parameter field of + /// the superclass. + final Map typeParameterMatch; + + /// The class whose struct is used as the type for variables of this type. + /// This is a type which is a superclass of all subtypes of this type. + late ClassInfo repr; + + /// All classes which implement this class. This is used to compute `repr`. + final List implementedBy = []; + + late final w.RefType nullableType = w.RefType.def(struct, nullable: true); + late final w.RefType nonNullableType = w.RefType.def(struct, nullable: false); + + w.RefType typeWithNullability(bool nullable) => + nullable ? nullableType : nonNullableType; + + ClassInfo(this.cls, this.classId, this.depth, this.struct, this.superInfo, + ClassInfoCollector collector, + {this.typeParameterMatch = const {}}) { + if (collector.options.useRttGlobals) { + rtt = collector.makeRtt(struct, superInfo); + } + implementedBy.add(this); + } + + void addField(w.FieldType fieldType, [int? expectedIndex]) { + assert(expectedIndex == null || expectedIndex == struct.fields.length); + struct.fields.add(fieldType); + } +} + +ClassInfo upperBound(Iterable classes) { + while (classes.length > 1) { + Set newClasses = {}; + int minDepth = 999999999; + int maxDepth = 0; + for (ClassInfo info in classes) { + minDepth = min(minDepth, info.depth); + maxDepth = max(maxDepth, info.depth); + } + int targetDepth = minDepth == maxDepth ? minDepth - 1 : minDepth; + for (ClassInfo info in classes) { + while (info.depth > targetDepth) { + info = info.superInfo!; + } + newClasses.add(info); + } + classes = newClasses; + } + return classes.single; +} + +/// Constructs the Wasm type hierarchy. +class ClassInfoCollector { + final Translator translator; + int nextClassId = 0; + late final ClassInfo topInfo; + + late final w.FieldType typeType = + w.FieldType(translator.classInfo[translator.typeClass]!.nullableType); + + ClassInfoCollector(this.translator); + + w.Module get m => translator.m; + + TranslatorOptions get options => translator.options; + + w.DefinedGlobal makeRtt(w.StructType struct, ClassInfo? superInfo) { + assert(options.useRttGlobals); + int depth = superInfo != null ? superInfo.depth + 1 : 0; + final w.DefinedGlobal rtt = + m.addGlobal(w.GlobalType(w.Rtt(struct, depth), mutable: false)); + final w.Instructions b = rtt.initializer; + if (superInfo != null) { + b.global_get(superInfo.rtt); + b.rtt_sub(struct); + } else { + b.rtt_canon(struct); + } + b.end(); + return rtt; + } + + void initializeTop() { + final w.StructType struct = translator.structType("#Top"); + topInfo = ClassInfo(null, nextClassId++, 0, struct, null, this); + translator.classes.add(topInfo); + translator.classForHeapType[struct] = topInfo; + } + + void initialize(Class cls) { + ClassInfo? info = translator.classInfo[cls]; + if (info == null) { + Class? superclass = cls.superclass; + if (superclass == null) { + ClassInfo superInfo = topInfo; + final w.StructType struct = + translator.structType(cls.name, superType: superInfo.struct); + info = ClassInfo( + cls, nextClassId++, superInfo.depth + 1, struct, superInfo, this); + // Mark Top type as implementing Object to force the representation + // type of Object to be Top. + info.implementedBy.add(topInfo); + } else { + // Recursively initialize all supertypes before initializing this class. + initialize(superclass); + for (Supertype interface in cls.implementedTypes) { + initialize(interface.classNode); + } + + // In the Wasm type hierarchy, Object, bool and num sit directly below + // the Top type. The implementation classes (_StringBase, _Type and the + // box classes) sit directly below the public classes they implement. + // All other classes sit below their superclass. + ClassInfo superInfo = cls == translator.coreTypes.boolClass || + cls == translator.coreTypes.numClass + ? topInfo + : cls == translator.stringBaseClass || + cls == translator.typeClass || + translator.boxedClasses.values.contains(cls) + ? translator.classInfo[cls.implementedTypes.single.classNode]! + : translator.classInfo[superclass]!; + + // Figure out which type parameters can reuse a type parameter field of + // the superclass. + Map typeParameterMatch = {}; + if (cls.typeParameters.isNotEmpty) { + Supertype supertype = cls.superclass == superInfo.cls + ? cls.supertype! + : cls.implementedTypes.single; + for (TypeParameter parameter in cls.typeParameters) { + for (int i = 0; i < supertype.typeArguments.length; i++) { + DartType arg = supertype.typeArguments[i]; + if (arg is TypeParameterType && arg.parameter == parameter) { + typeParameterMatch[parameter] = + superInfo.cls!.typeParameters[i]; + break; + } + } + } + } + + // A class can reuse the Wasm struct of the superclass if it doesn't + // declare any Wasm fields of its own. This is the case when three + // conditions are met: + // 1. All type parameters can reuse a type parameter field of the + // superclass. + // 2. The class declares no Dart fields of its own. + // 3. The class is not a special class that contains hidden fields. + bool canReuseSuperStruct = + typeParameterMatch.length == cls.typeParameters.length && + cls.fields.where((f) => f.isInstanceMember).isEmpty && + cls != translator.typedListBaseClass && + cls != translator.typedListClass && + cls != translator.typedListViewClass && + cls != translator.byteDataViewClass; + w.StructType struct = canReuseSuperStruct + ? superInfo.struct + : translator.structType(cls.name, superType: superInfo.struct); + info = ClassInfo( + cls, nextClassId++, superInfo.depth + 1, struct, superInfo, this, + typeParameterMatch: typeParameterMatch); + + // Mark all interfaces as being implemented by this class. This is + // needed to calculate representation types. + for (Supertype interface in cls.implementedTypes) { + ClassInfo? interfaceInfo = translator.classInfo[interface.classNode]; + while (interfaceInfo != null) { + interfaceInfo.implementedBy.add(info); + interfaceInfo = interfaceInfo.superInfo; + } + } + } + translator.classes.add(info); + translator.classInfo[cls] = info; + translator.classForHeapType.putIfAbsent(info.struct, () => info!); + } + } + + void computeRepresentation(ClassInfo info) { + info.repr = upperBound(info.implementedBy); + } + + void generateFields(ClassInfo info) { + ClassInfo? superInfo = info.superInfo; + if (superInfo == null) { + // Top - add class id field + info.addField(w.FieldType(w.NumType.i32), FieldIndex.classId); + } else if (info.struct != superInfo.struct) { + // Copy fields from superclass + for (w.FieldType fieldType in superInfo.struct.fields) { + info.addField(fieldType); + } + if (info.cls!.superclass == null) { + // Object - add identity hash code field + info.addField(w.FieldType(w.NumType.i32), FieldIndex.identityHash); + } + // Add fields for type variables + for (TypeParameter parameter in info.cls!.typeParameters) { + TypeParameter? match = info.typeParameterMatch[parameter]; + if (match != null) { + // Reuse supertype type variable + translator.typeParameterIndex[parameter] = + translator.typeParameterIndex[match]!; + } else { + translator.typeParameterIndex[parameter] = info.struct.fields.length; + info.addField(typeType); + } + } + // Add fields for Dart instance fields + for (Field field in info.cls!.fields) { + if (field.isInstanceMember) { + w.ValueType wasmType = translator.translateType(field.type); + // TODO(askesc): Generalize this check for finer nullability control + if (wasmType != w.RefType.data()) { + wasmType = wasmType.withNullability(true); + } + translator.fieldIndex[field] = info.struct.fields.length; + info.addField(w.FieldType(wasmType)); + } + } + } else { + for (TypeParameter parameter in info.cls!.typeParameters) { + // Reuse supertype type variable + translator.typeParameterIndex[parameter] = + translator.typeParameterIndex[info.typeParameterMatch[parameter]]!; + } + } + } + + void collect() { + // Create class info and Wasm structs for all classes. + initializeTop(); + for (Library library in translator.component.libraries) { + for (Class cls in library.classes) { + initialize(cls); + } + } + + // For each class, compute which Wasm struct should be used for the type of + // variables bearing that class as their Dart type. This is the struct + // corresponding to the least common supertype of all Dart classes + // implementing this class. + for (ClassInfo info in translator.classes) { + computeRepresentation(info); + } + + // Now that the representation types for all classes have been computed, + // fill in the types of the fields in the generated Wasm structs. + for (ClassInfo info in translator.classes) { + generateFields(info); + } + + // Add hidden fields of typed_data classes. + addTypedDataFields(); + + // Validate that all internally used fields have the expected indices. + FieldIndex.validate(translator); + } + + void addTypedDataFields() { + ClassInfo typedListBaseInfo = + translator.classInfo[translator.typedListBaseClass]!; + typedListBaseInfo.addField(w.FieldType(w.NumType.i32, mutable: false), + FieldIndex.typedListBaseLength); + + ClassInfo typedListInfo = translator.classInfo[translator.typedListClass]!; + typedListInfo.addField(w.FieldType(w.NumType.i32, mutable: false), + FieldIndex.typedListBaseLength); + w.RefType bytesArrayType = w.RefType.def( + translator.wasmArrayType(w.PackedType.i8, "i8"), + nullable: false); + typedListInfo.addField( + w.FieldType(bytesArrayType, mutable: false), FieldIndex.typedListArray); + + w.RefType typedListType = + w.RefType.def(typedListInfo.struct, nullable: false); + + ClassInfo typedListViewInfo = + translator.classInfo[translator.typedListViewClass]!; + typedListViewInfo.addField(w.FieldType(w.NumType.i32, mutable: false), + FieldIndex.typedListBaseLength); + typedListViewInfo.addField(w.FieldType(typedListType, mutable: false), + FieldIndex.typedListViewTypedData); + typedListViewInfo.addField(w.FieldType(w.NumType.i32, mutable: false), + FieldIndex.typedListViewOffsetInBytes); + + ClassInfo byteDataViewInfo = + translator.classInfo[translator.byteDataViewClass]!; + byteDataViewInfo.addField(w.FieldType(w.NumType.i32, mutable: false), + FieldIndex.byteDataViewLength); + byteDataViewInfo.addField(w.FieldType(typedListType, mutable: false), + FieldIndex.byteDataViewTypedData); + byteDataViewInfo.addField(w.FieldType(w.NumType.i32, mutable: false), + FieldIndex.byteDataViewOffsetInBytes); + } +} diff --git a/pkg/dart2wasm/lib/closures.dart b/pkg/dart2wasm/lib/closures.dart new file mode 100644 index 00000000000..cde1b4cd1c4 --- /dev/null +++ b/pkg/dart2wasm/lib/closures.dart @@ -0,0 +1,318 @@ +// 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 'package:dart2wasm/code_generator.dart'; +import 'package:dart2wasm/translator.dart'; + +import 'package:kernel/ast.dart'; + +import 'package:wasm_builder/wasm_builder.dart' as w; + +/// A local function or function expression. +class Lambda { + final FunctionNode functionNode; + final w.DefinedFunction function; + + Lambda(this.functionNode, this.function); +} + +/// The context for one or more closures, containing their captured variables. +/// +/// Contexts can be nested, corresponding to the scopes covered by the contexts. +/// Each local function, function expression or loop (`while`, `do`/`while` or +/// `for`) gives rise to its own context nested inside the context of its +/// surrounding scope. At runtime, each context has a reference to its parent +/// context. +/// +/// Closures corresponding to local functions or function expressions in the +/// same scope share the same context. Thus, a closure can potentially keep more +/// values alive than the ones captured by the closure itself. +/// +/// A context may be empty (containing no captured variables), in which case it +/// is skipped in the context parent chain and never allocated. A context can +/// also be skipped if it only contains variables that are not in scope for the +/// child context (and its descendants). +class Context { + /// The node containing the scope covered by the context. This is either a + /// [FunctionNode] (for members, local functions and function expressions), + /// a [ForStatement], a [DoStatement] or a [WhileStatement]. + final TreeNode owner; + + /// The parent of this context, corresponding to the lexically enclosing + /// owner. This is null if the context is a member context, or if all contexts + /// in the parent chain are skipped. + final Context? parent; + + /// The variables captured by this context. + final List variables = []; + + /// Whether this context contains a captured `this`. Only member contexts can. + bool containsThis = false; + + /// The Wasm struct representing this context at runtime. + late final w.StructType struct; + + /// The local variable currently pointing to this context. Used during code + /// generation. + late w.Local currentLocal; + + bool get isEmpty => variables.isEmpty && !containsThis; + + int get parentFieldIndex { + assert(parent != null); + return 0; + } + + int get thisFieldIndex { + assert(containsThis); + return 0; + } + + Context(this.owner, this.parent); +} + +/// A captured variable. +class Capture { + final VariableDeclaration variable; + late final Context context; + late final int fieldIndex; + bool written = false; + + Capture(this.variable); + + w.ValueType get type => context.struct.fields[fieldIndex].type.unpacked; +} + +/// Compiler passes to find all captured variables and construct the context +/// tree for a member. +class Closures { + final CodeGenerator codeGen; + final Map captures = {}; + bool isThisCaptured = false; + final Map lambdas = {}; + final Map contexts = {}; + final Set closurizedFunctions = {}; + + Closures(this.codeGen); + + Translator get translator => codeGen.translator; + + void findCaptures(Member member) { + var find = CaptureFinder(this, member); + if (member is Constructor) { + Class cls = member.enclosingClass; + for (Field field in cls.fields) { + if (field.isInstanceMember && field.initializer != null) { + field.initializer!.accept(find); + } + } + } + member.accept(find); + } + + void collectContexts(TreeNode node, {TreeNode? container}) { + if (captures.isNotEmpty || isThisCaptured) { + node.accept(ContextCollector(this, container)); + } + } + + void buildContexts() { + // Make struct definitions + for (Context context in contexts.values) { + if (!context.isEmpty) { + context.struct = translator.structType(""); + } + } + + // Build object layouts + for (Context context in contexts.values) { + if (!context.isEmpty) { + w.StructType struct = context.struct; + if (context.parent != null) { + assert(!context.containsThis); + struct.fields.add(w.FieldType( + w.RefType.def(context.parent!.struct, nullable: true))); + } + if (context.containsThis) { + struct.fields.add(w.FieldType( + codeGen.preciseThisLocal!.type.withNullability(true))); + } + for (VariableDeclaration variable in context.variables) { + int index = struct.fields.length; + struct.fields.add(w.FieldType( + translator.translateType(variable.type).withNullability(true))); + captures[variable]!.fieldIndex = index; + } + } + } + } +} + +class CaptureFinder extends RecursiveVisitor { + final Closures closures; + final Member member; + final Map variableDepth = {}; + int depth = 0; + + CaptureFinder(this.closures, this.member); + + Translator get translator => closures.translator; + + @override + void visitAssertStatement(AssertStatement node) {} + + @override + void visitVariableDeclaration(VariableDeclaration node) { + if (depth > 0) { + variableDepth[node] = depth; + } + super.visitVariableDeclaration(node); + } + + void _visitVariableUse(VariableDeclaration variable) { + int declDepth = variableDepth[variable] ?? 0; + assert(declDepth <= depth); + if (declDepth < depth) { + closures.captures[variable] = Capture(variable); + } else if (variable.parent is FunctionDeclaration) { + closures.closurizedFunctions.add(variable.parent as FunctionDeclaration); + } + } + + @override + void visitVariableGet(VariableGet node) { + _visitVariableUse(node.variable); + super.visitVariableGet(node); + } + + @override + void visitVariableSet(VariableSet node) { + _visitVariableUse(node.variable); + super.visitVariableSet(node); + } + + void _visitThis() { + if (depth > 0) { + closures.isThisCaptured = true; + } + } + + @override + void visitThisExpression(ThisExpression node) { + _visitThis(); + } + + @override + void visitSuperMethodInvocation(SuperMethodInvocation node) { + _visitThis(); + super.visitSuperMethodInvocation(node); + } + + @override + void visitTypeParameterType(TypeParameterType node) { + if (node.parameter.parent == member.enclosingClass) { + _visitThis(); + } + } + + void _visitLambda(FunctionNode node) { + if (node.positionalParameters.length != node.requiredParameterCount || + node.namedParameters.isNotEmpty) { + throw "Not supported: Optional parameters for " + "function expression or local function at ${node.location}"; + } + int parameterCount = node.requiredParameterCount; + w.FunctionType type = translator.closureFunctionType(parameterCount); + w.DefinedFunction function = + translator.m.addFunction(type, "$member (closure)"); + closures.lambdas[node] = Lambda(node, function); + + depth++; + node.visitChildren(this); + depth--; + } + + @override + void visitFunctionExpression(FunctionExpression node) { + _visitLambda(node.function); + } + + @override + void visitFunctionDeclaration(FunctionDeclaration node) { + // Variable is in outer scope + node.variable.accept(this); + _visitLambda(node.function); + } +} + +class ContextCollector extends RecursiveVisitor { + final Closures closures; + Context? currentContext; + + ContextCollector(this.closures, TreeNode? container) { + if (container != null) { + currentContext = closures.contexts[container]!; + } + } + + @override + void visitAssertStatement(AssertStatement node) {} + + void _newContext(TreeNode node) { + bool outerMost = currentContext == null; + Context? oldContext = currentContext; + Context? parent = currentContext; + while (parent != null && parent.isEmpty) parent = parent.parent; + currentContext = Context(node, parent); + if (closures.isThisCaptured && outerMost) { + currentContext!.containsThis = true; + } + closures.contexts[node] = currentContext!; + node.visitChildren(this); + currentContext = oldContext; + } + + @override + void visitConstructor(Constructor node) { + node.function.accept(this); + currentContext = closures.contexts[node.function]!; + visitList(node.initializers, this); + } + + @override + void visitFunctionNode(FunctionNode node) { + _newContext(node); + } + + @override + void visitWhileStatement(WhileStatement node) { + _newContext(node); + } + + @override + void visitDoStatement(DoStatement node) { + _newContext(node); + } + + @override + void visitForStatement(ForStatement node) { + _newContext(node); + } + + @override + void visitVariableDeclaration(VariableDeclaration node) { + Capture? capture = closures.captures[node]; + if (capture != null) { + currentContext!.variables.add(node); + capture.context = currentContext!; + } + super.visitVariableDeclaration(node); + } + + @override + void visitVariableSet(VariableSet node) { + closures.captures[node.variable]?.written = true; + super.visitVariableSet(node); + } +} diff --git a/pkg/dart2wasm/lib/code_generator.dart b/pkg/dart2wasm/lib/code_generator.dart new file mode 100644 index 00000000000..d4ef423da7f --- /dev/null +++ b/pkg/dart2wasm/lib/code_generator.dart @@ -0,0 +1,1940 @@ +// 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 'package:dart2wasm/class_info.dart'; +import 'package:dart2wasm/closures.dart'; +import 'package:dart2wasm/dispatch_table.dart'; +import 'package:dart2wasm/intrinsics.dart'; +import 'package:dart2wasm/param_info.dart'; +import 'package:dart2wasm/reference_extensions.dart'; +import 'package:dart2wasm/translator.dart'; + +import 'package:kernel/ast.dart'; +import 'package:kernel/type_environment.dart'; + +import 'package:wasm_builder/wasm_builder.dart' as w; + +/// Main code generator for member bodies. +/// +/// The [generate] method first collects all local functions and function +/// expressions in the body and then generates code for the body. Code for the +/// local functions and function expressions must be generated separately by +/// calling the [generateLambda] method on all lambdas in [closures]. +/// +/// A new [CodeGenerator] object must be created for each new member or lambda. +/// +/// Every visitor method for an expression takes in the Wasm type that it is +/// expected to leave on the stack (or the special [voidMarker] to indicate that +/// it should leave nothing). It returns what it actually left on the stack. The +/// code generation for every expression or subexpression is done via the [wrap] +/// method, which emits appropriate conversion code if the produced type is not +/// a subtype of the expected type. +class CodeGenerator extends ExpressionVisitor1 + implements InitializerVisitor, StatementVisitor { + final Translator translator; + final w.DefinedFunction function; + final Reference reference; + late final List paramLocals; + final w.Label? returnLabel; + + late final Intrinsifier intrinsifier; + late final StaticTypeContext typeContext; + late final w.Instructions b; + + late final Closures closures; + + final Map locals = {}; + w.Local? thisLocal; + w.Local? preciseThisLocal; + final Map typeLocals = {}; + final List finalizers = []; + final Map labels = {}; + final Map switchLabels = {}; + + /// Create a code generator for a member or one of its lambdas. + /// + /// The [paramLocals] and [returnLabel] parameters can be used to generate + /// code for an inlined function by specifying the locals containing the + /// parameters (instead of the function inputs) and the label to jump to on + /// return (instead of emitting a `return` instruction). + CodeGenerator(this.translator, this.function, this.reference, + {List? paramLocals, this.returnLabel}) { + this.paramLocals = paramLocals ?? function.locals; + intrinsifier = Intrinsifier(this); + typeContext = StaticTypeContext(member, translator.typeEnvironment); + b = function.body; + } + + Member get member => reference.asMember; + + w.ValueType get returnType => translator + .outputOrVoid(returnLabel?.targetTypes ?? function.type.outputs); + + TranslatorOptions get options => translator.options; + + w.ValueType get voidMarker => translator.voidMarker; + + w.ValueType translateType(DartType type) => translator.translateType(type); + + w.Local addLocal(w.ValueType type) { + return function.addLocal(translator.typeForLocal(type)); + } + + DartType dartTypeOf(Expression exp) { + return exp.getStaticType(typeContext); + } + + void _unimplemented( + TreeNode node, Object message, List expectedTypes) { + final text = "Not implemented: $message at ${node.location}"; + print(text); + b.comment(text); + b.block(const [], expectedTypes); + b.unreachable(); + b.end(); + } + + @override + void defaultInitializer(Initializer node) { + _unimplemented(node, node.runtimeType, const []); + } + + @override + w.ValueType defaultExpression(Expression node, w.ValueType expectedType) { + _unimplemented(node, node.runtimeType, [expectedType]); + return expectedType; + } + + @override + void defaultStatement(Statement node) { + _unimplemented(node, node.runtimeType, const []); + } + + /// Generate code for the body of the member. + void generate() { + closures = Closures(this); + + Member member = this.member; + + if (reference.isTearOffReference) { + // Tear-off getter + w.DefinedFunction closureFunction = + translator.getTearOffFunction(member as Procedure); + + int parameterCount = member.function.requiredParameterCount; + w.DefinedGlobal global = translator.makeFunctionRef(closureFunction); + + ClassInfo info = translator.classInfo[translator.functionClass]!; + translator.functions.allocateClass(info.classId); + + b.i32_const(info.classId); + b.i32_const(initialIdentityHash); + b.local_get(paramLocals[0]); + b.global_get(global); + translator.struct_new(b, parameterCount); + b.end(); + return; + } + + if (intrinsifier.generateMemberIntrinsic( + reference, function, paramLocals, returnLabel)) { + b.end(); + return; + } + + if (member.isExternal) { + final text = + "Unimplemented external member $member at ${member.location}"; + print(text); + b.comment(text); + b.unreachable(); + b.end(); + return; + } + + if (member is Field) { + if (member.isStatic) { + // Static field initializer function + assert(reference == member.fieldReference); + closures.findCaptures(member); + closures.collectContexts(member); + closures.buildContexts(); + + w.Global global = translator.globals.getGlobal(member); + w.Global? flag = translator.globals.getGlobalInitializedFlag(member); + wrap(member.initializer!, global.type.type); + b.global_set(global); + if (flag != null) { + b.i32_const(1); + b.global_set(flag); + } + b.global_get(global); + translator.convertType( + function, global.type.type, function.type.outputs.single); + b.end(); + return; + } + + // Implicit getter or setter + w.StructType struct = + translator.classInfo[member.enclosingClass!]!.struct; + int fieldIndex = translator.fieldIndex[member]!; + w.ValueType fieldType = struct.fields[fieldIndex].type.unpacked; + + void getThis() { + w.Local thisLocal = paramLocals[0]; + w.RefType structType = w.RefType.def(struct, nullable: true); + b.local_get(thisLocal); + translator.convertType(function, thisLocal.type, structType); + } + + if (reference.isImplicitGetter) { + // Implicit getter + getThis(); + b.struct_get(struct, fieldIndex); + translator.convertType(function, fieldType, returnType); + } else { + // Implicit setter + w.Local valueLocal = paramLocals[1]; + getThis(); + b.local_get(valueLocal); + translator.convertType(function, valueLocal.type, fieldType); + b.struct_set(struct, fieldIndex); + } + b.end(); + return; + } + + ParameterInfo paramInfo = translator.paramInfoFor(reference); + bool hasThis = member.isInstanceMember || member is Constructor; + int typeParameterOffset = hasThis ? 1 : 0; + int implicitParams = typeParameterOffset + paramInfo.typeParamCount; + List positional = + member.function!.positionalParameters; + for (int i = 0; i < positional.length; i++) { + locals[positional[i]] = paramLocals[implicitParams + i]; + } + List named = member.function!.namedParameters; + for (var param in named) { + locals[param] = + paramLocals[implicitParams + paramInfo.nameIndex[param.name]!]; + } + List typeParameters = member is Constructor + ? member.enclosingClass.typeParameters + : member.function!.typeParameters; + for (int i = 0; i < typeParameters.length; i++) { + typeLocals[typeParameters[i]] = paramLocals[typeParameterOffset + i]; + } + + closures.findCaptures(member); + + if (hasThis) { + Class cls = member.enclosingClass!; + ClassInfo info = translator.classInfo[cls]!; + thisLocal = paramLocals[0]; + w.RefType thisType = info.nonNullableType; + if (translator.needsConversion(paramLocals[0].type, thisType)) { + preciseThisLocal = addLocal(thisType); + b.local_get(paramLocals[0]); + translator.ref_cast(b, info); + b.local_set(preciseThisLocal!); + } else { + preciseThisLocal = paramLocals[0]; + } + } + + closures.collectContexts(member); + if (member is Constructor) { + for (Field field in member.enclosingClass.fields) { + if (field.isInstanceMember && field.initializer != null) { + closures.collectContexts(field.initializer!, + container: member.function); + } + } + } + closures.buildContexts(); + + allocateContext(member.function!); + captureParameters(); + + if (member is Constructor) { + Class cls = member.enclosingClass; + ClassInfo info = translator.classInfo[cls]!; + for (TypeParameter typeParam in cls.typeParameters) { + b.local_get(thisLocal!); + b.local_get(typeLocals[typeParam]!); + b.struct_set(info.struct, translator.typeParameterIndex[typeParam]!); + } + for (Field field in cls.fields) { + if (field.isInstanceMember && field.initializer != null) { + int fieldIndex = translator.fieldIndex[field]!; + b.local_get(thisLocal!); + wrap( + field.initializer!, info.struct.fields[fieldIndex].type.unpacked); + b.struct_set(info.struct, fieldIndex); + } + } + for (Initializer initializer in member.initializers) { + initializer.accept(this); + } + } + + member.function!.body?.accept(this); + _implicitReturn(); + b.end(); + } + + /// Generate code for the body of a lambda. + void generateLambda(Lambda lambda, Closures closures) { + this.closures = closures; + + final int implicitParams = 1; + List positional = + lambda.functionNode.positionalParameters; + for (int i = 0; i < positional.length; i++) { + locals[positional[i]] = paramLocals[implicitParams + i]; + } + + Context? context = closures.contexts[lambda.functionNode]?.parent; + if (context != null) { + b.local_get(paramLocals[0]); + translator.ref_cast(b, context.struct); + while (true) { + w.Local contextLocal = + addLocal(w.RefType.def(context!.struct, nullable: false)); + context.currentLocal = contextLocal; + if (context.parent != null || context.containsThis) { + b.local_tee(contextLocal); + } else { + b.local_set(contextLocal); + } + if (context.parent == null) break; + + b.struct_get(context.struct, context.parentFieldIndex); + if (options.localNullability) { + b.ref_as_non_null(); + } + context = context.parent!; + } + if (context.containsThis) { + thisLocal = addLocal( + context.struct.fields[context.thisFieldIndex].type.unpacked); + preciseThisLocal = thisLocal; + b.struct_get(context.struct, context.thisFieldIndex); + b.local_set(thisLocal!); + } + } + allocateContext(lambda.functionNode); + captureParameters(); + + lambda.functionNode.body!.accept(this); + _implicitReturn(); + b.end(); + } + + void _implicitReturn() { + if (function.type.outputs.length > 0) { + w.ValueType returnType = function.type.outputs[0]; + if (returnType is w.RefType && returnType.nullable) { + // Dart body may have an implicit return null. + b.ref_null(returnType.heapType); + } else { + // This point is unreachable, but the Wasm validator still expects the + // stack to contain a value matching the Wasm function return type. + b.block(const [], function.type.outputs); + b.comment("Unreachable implicit return"); + b.unreachable(); + b.end(); + } + } + } + + void allocateContext(TreeNode node) { + Context? context = closures.contexts[node]; + if (context != null && !context.isEmpty) { + w.Local contextLocal = + addLocal(w.RefType.def(context.struct, nullable: false)); + context.currentLocal = contextLocal; + translator.struct_new_default(b, context.struct); + b.local_set(contextLocal); + if (context.containsThis) { + b.local_get(contextLocal); + b.local_get(preciseThisLocal!); + b.struct_set(context.struct, context.thisFieldIndex); + } + if (context.parent != null) { + w.Local parentLocal = context.parent!.currentLocal; + b.local_get(contextLocal); + b.local_get(parentLocal); + b.struct_set(context.struct, context.parentFieldIndex); + } + } + } + + void captureParameters() { + locals.forEach((variable, local) { + Capture? capture = closures.captures[variable]; + if (capture != null) { + b.local_get(capture.context.currentLocal); + b.local_get(local); + translator.convertType(function, local.type, capture.type); + b.struct_set(capture.context.struct, capture.fieldIndex); + } + }); + } + + /// Generates code for an expression plus conversion code to convert the + /// result to the expected type if needed. All expression code generation goes + /// through this method. + w.ValueType wrap(Expression node, w.ValueType expectedType) { + w.ValueType resultType = node.accept1(this, expectedType); + translator.convertType(function, resultType, expectedType); + return expectedType; + } + + w.ValueType _call(Reference target) { + w.BaseFunction targetFunction = translator.functions.getFunction(target); + if (translator.shouldInline(target)) { + List inlinedLocals = + targetFunction.type.inputs.map((t) => addLocal(t)).toList(); + for (w.Local local in inlinedLocals.reversed) { + b.local_set(local); + } + w.Label block = b.block(const [], targetFunction.type.outputs); + b.comment("Inlined ${target.asMember}"); + CodeGenerator(translator, function, target, + paramLocals: inlinedLocals, returnLabel: block) + .generate(); + } else { + String access = + target.isGetter ? "get" : (target.isSetter ? "set" : "call"); + b.comment("Direct $access of '${target.asMember}'"); + b.call(targetFunction); + } + return translator.outputOrVoid(targetFunction.type.outputs); + } + + @override + void visitInvalidInitializer(InvalidInitializer node) {} + + @override + void visitAssertInitializer(AssertInitializer node) {} + + @override + void visitLocalInitializer(LocalInitializer node) { + node.variable.accept(this); + } + + @override + void visitFieldInitializer(FieldInitializer node) { + Class cls = (node.parent as Constructor).enclosingClass; + w.StructType struct = translator.classInfo[cls]!.struct; + int fieldIndex = translator.fieldIndex[node.field]!; + + b.local_get(thisLocal!); + wrap(node.value, struct.fields[fieldIndex].type.unpacked); + b.struct_set(struct, fieldIndex); + } + + @override + void visitRedirectingInitializer(RedirectingInitializer node) { + Class cls = (node.parent as Constructor).enclosingClass; + b.local_get(thisLocal!); + if (options.parameterNullability && thisLocal!.type.nullable) { + b.ref_as_non_null(); + } + for (TypeParameter typeParam in cls.typeParameters) { + _makeType(TypeParameterType(typeParam, Nullability.nonNullable), node); + } + _visitArguments(node.arguments, node.targetReference, 1); + _call(node.targetReference); + } + + @override + void visitSuperInitializer(SuperInitializer node) { + Supertype? supertype = + (node.parent as Constructor).enclosingClass.supertype; + if (supertype?.classNode.superclass == null) { + return; + } + b.local_get(thisLocal!); + if (options.parameterNullability && thisLocal!.type.nullable) { + b.ref_as_non_null(); + } + for (DartType typeArg in supertype!.typeArguments) { + _makeType(typeArg, node); + } + _visitArguments(node.arguments, node.targetReference, + 1 + supertype.typeArguments.length); + _call(node.targetReference); + } + + @override + void visitBlock(Block node) { + for (Statement statement in node.statements) { + statement.accept(this); + } + } + + @override + void visitLabeledStatement(LabeledStatement node) { + w.Label label = b.block(); + labels[node] = label; + node.body.accept(this); + labels.remove(node); + b.end(); + } + + @override + void visitBreakStatement(BreakStatement node) { + b.br(labels[node.target]!); + } + + @override + void visitVariableDeclaration(VariableDeclaration node) { + if (node.type is VoidType) { + if (node.initializer != null) { + wrap(node.initializer!, voidMarker); + } + return; + } + w.ValueType type = translateType(node.type); + w.Local? local; + Capture? capture = closures.captures[node]; + if (capture == null || !capture.written) { + local = addLocal(type); + locals[node] = local; + } + if (node.initializer != null) { + if (capture != null) { + w.ValueType expectedType = capture.written ? capture.type : local!.type; + b.local_get(capture.context.currentLocal); + wrap(node.initializer!, expectedType); + if (!capture.written) { + b.local_tee(local!); + } + b.struct_set(capture.context.struct, capture.fieldIndex); + } else { + wrap(node.initializer!, local!.type); + b.local_set(local); + } + } else if (local != null && !local.type.defaultable) { + // Uninitialized variable + translator.globals.instantiateDummyValue(b, local.type); + b.local_set(local); + } + } + + @override + void visitEmptyStatement(EmptyStatement node) {} + + @override + void visitAssertStatement(AssertStatement node) {} + + @override + void visitAssertBlock(AssertBlock node) {} + + @override + void visitTryCatch(TryCatch node) { + // TODO(joshualitt): Include catches + node.body.accept(this); + } + + @override + void visitTryFinally(TryFinally node) { + finalizers.add(node.finalizer); + node.body.accept(this); + finalizers.removeLast().accept(this); + } + + @override + void visitExpressionStatement(ExpressionStatement node) { + wrap(node.expression, voidMarker); + } + + bool _hasLogicalOperator(Expression condition) { + while (condition is Not) condition = condition.operand; + return condition is LogicalExpression; + } + + void _branchIf(Expression? condition, w.Label target, + {required bool negated}) { + if (condition == null) { + if (!negated) b.br(target); + return; + } + while (condition is Not) { + negated = !negated; + condition = condition.operand; + } + if (condition is LogicalExpression) { + bool isConjunctive = + (condition.operatorEnum == LogicalExpressionOperator.AND) ^ negated; + if (isConjunctive) { + w.Label conditionBlock = b.block(); + _branchIf(condition.left, conditionBlock, negated: !negated); + _branchIf(condition.right, target, negated: negated); + b.end(); + } else { + _branchIf(condition.left, target, negated: negated); + _branchIf(condition.right, target, negated: negated); + } + } else { + wrap(condition!, w.NumType.i32); + if (negated) { + b.i32_eqz(); + } + b.br_if(target); + } + } + + void _conditional(Expression condition, void then(), void otherwise()?, + List result) { + if (!_hasLogicalOperator(condition)) { + // Simple condition + wrap(condition, w.NumType.i32); + b.if_(const [], result); + then(); + if (otherwise != null) { + b.else_(); + otherwise(); + } + b.end(); + } else { + // Complex condition + w.Label ifBlock = b.block(const [], result); + if (otherwise != null) { + w.Label elseBlock = b.block(); + _branchIf(condition, elseBlock, negated: true); + then(); + b.br(ifBlock); + b.end(); + otherwise(); + } else { + _branchIf(condition, ifBlock, negated: true); + then(); + } + b.end(); + } + } + + @override + void visitIfStatement(IfStatement node) { + _conditional( + node.condition, + () => node.then.accept(this), + node.otherwise != null ? () => node.otherwise!.accept(this) : null, + const []); + } + + @override + void visitDoStatement(DoStatement node) { + w.Label loop = b.loop(); + allocateContext(node); + node.body.accept(this); + _branchIf(node.condition, loop, negated: false); + b.end(); + } + + @override + void visitWhileStatement(WhileStatement node) { + w.Label block = b.block(); + w.Label loop = b.loop(); + _branchIf(node.condition, block, negated: true); + allocateContext(node); + node.body.accept(this); + b.br(loop); + b.end(); + b.end(); + } + + @override + void visitForStatement(ForStatement node) { + Context? context = closures.contexts[node]; + allocateContext(node); + for (VariableDeclaration variable in node.variables) { + variable.accept(this); + } + w.Label block = b.block(); + w.Label loop = b.loop(); + _branchIf(node.condition, block, negated: true); + node.body.accept(this); + if (node.variables.any((v) => closures.captures.containsKey(v))) { + w.Local oldContext = context!.currentLocal; + allocateContext(node); + w.Local newContext = context.currentLocal; + for (VariableDeclaration variable in node.variables) { + Capture? capture = closures.captures[variable]; + if (capture != null) { + b.local_get(oldContext); + b.struct_get(context.struct, capture.fieldIndex); + b.local_get(newContext); + b.struct_set(context.struct, capture.fieldIndex); + } + } + } else { + allocateContext(node); + } + for (Expression update in node.updates) { + wrap(update, voidMarker); + } + b.br(loop); + b.end(); + b.end(); + } + + @override + void visitForInStatement(ForInStatement node) { + throw "ForInStatement should have been desugared: $node"; + } + + @override + void visitReturnStatement(ReturnStatement node) { + Expression? expression = node.expression; + if (expression != null) { + wrap(expression, returnType); + } else { + translator.convertType(function, voidMarker, returnType); + } + for (Statement finalizer in finalizers.reversed) { + finalizer.accept(this); + } + if (returnLabel != null) { + b.br(returnLabel!); + } else { + b.return_(); + } + } + + @override + void visitSwitchStatement(SwitchStatement node) { + bool check() => + node.cases.expand((c) => c.expressions).every((e) => + e is L || + e is NullLiteral || + e is ConstantExpression && + (e.constant is C || e.constant is NullConstant)); + + // Identify kind of switch + w.ValueType valueType; + w.ValueType nullableType; + void Function() compare; + if (check()) { + // bool switch + valueType = w.NumType.i32; + nullableType = + translator.classInfo[translator.boxedBoolClass]!.nullableType; + compare = () => b.i32_eq(); + } else if (check()) { + // int switch + valueType = w.NumType.i64; + nullableType = + translator.classInfo[translator.boxedIntClass]!.nullableType; + compare = () => b.i64_eq(); + } else if (check()) { + // String switch + valueType = + translator.classInfo[translator.stringBaseClass]!.nonNullableType; + nullableType = valueType.withNullability(true); + compare = () => _call(translator.stringEquals.reference); + } else { + // Object switch + assert(check()); + valueType = w.RefType.eq(nullable: false); + nullableType = w.RefType.eq(nullable: true); + compare = () => b.ref_eq(); + } + w.Local valueLocal = addLocal(valueType); + + // Special cases + SwitchCase? defaultCase = node.cases + .cast() + .firstWhere((c) => c!.isDefault, orElse: () => null); + SwitchCase? nullCase = node.cases.cast().firstWhere( + (c) => c!.expressions.any((e) => + e is NullLiteral || + e is ConstantExpression && e.constant is NullConstant), + orElse: () => null); + + // Set up blocks, in reverse order of cases so they end in forward order + w.Label doneLabel = b.block(); + for (SwitchCase c in node.cases.reversed) { + switchLabels[c] = b.block(); + } + + // Compute value and handle null + bool isNullable = dartTypeOf(node.expression).isPotentiallyNullable; + if (isNullable) { + w.Label nullLabel = nullCase != null + ? switchLabels[nullCase]! + : defaultCase != null + ? switchLabels[defaultCase]! + : doneLabel; + wrap(node.expression, nullableType); + b.br_on_null(nullLabel); + translator.convertType( + function, nullableType.withNullability(false), valueType); + } else { + assert(nullCase == null); + wrap(node.expression, valueType); + } + b.local_set(valueLocal); + + // Compare against all case values + for (SwitchCase c in node.cases) { + for (Expression exp in c.expressions) { + if (exp is NullLiteral || + exp is ConstantExpression && exp.constant is NullConstant) { + // Null already checked, skip + } else { + wrap(exp, valueType); + b.local_get(valueLocal); + translator.convertType(function, valueLocal.type, valueType); + compare(); + b.br_if(switchLabels[c]!); + } + } + } + w.Label defaultLabel = + defaultCase != null ? switchLabels[defaultCase]! : doneLabel; + b.br(defaultLabel); + + // Emit case bodies + for (SwitchCase c in node.cases) { + switchLabels.remove(c); + b.end(); + c.body.accept(this); + b.br(doneLabel); + } + b.end(); + } + + @override + void visitContinueSwitchStatement(ContinueSwitchStatement node) { + w.Label? label = switchLabels[node.target]; + if (label != null) { + b.br(label); + } else { + throw "Not supported: Backward jump to switch case at ${node.location}"; + } + } + + @override + void visitYieldStatement(YieldStatement node) => defaultStatement(node); + + @override + w.ValueType visitBlockExpression( + BlockExpression node, w.ValueType expectedType) { + node.body.accept(this); + return wrap(node.value, expectedType); + } + + @override + w.ValueType visitLet(Let node, w.ValueType expectedType) { + node.variable.accept(this); + return wrap(node.body, expectedType); + } + + @override + w.ValueType visitThisExpression( + ThisExpression node, w.ValueType expectedType) { + return _visitThis(expectedType); + } + + w.ValueType _visitThis(w.ValueType expectedType) { + w.ValueType thisType = thisLocal!.type.withNullability(false); + w.ValueType preciseThisType = preciseThisLocal!.type.withNullability(false); + if (!thisType.isSubtypeOf(expectedType) && + preciseThisType.isSubtypeOf(expectedType)) { + b.local_get(preciseThisLocal!); + return preciseThisLocal!.type; + } else { + b.local_get(thisLocal!); + return thisLocal!.type; + } + } + + @override + w.ValueType visitConstructorInvocation( + ConstructorInvocation node, w.ValueType expectedType) { + ClassInfo info = translator.classInfo[node.target.enclosingClass]!; + translator.functions.allocateClass(info.classId); + w.Local temp = addLocal(info.nonNullableType); + translator.struct_new_default(b, info); + b.local_tee(temp); + b.local_get(temp); + b.i32_const(info.classId); + b.struct_set(info.struct, FieldIndex.classId); + if (options.parameterNullability && temp.type.nullable) { + b.ref_as_non_null(); + } + _visitArguments(node.arguments, node.targetReference, 1); + _call(node.targetReference); + if (expectedType != voidMarker) { + b.local_get(temp); + return temp.type; + } else { + return voidMarker; + } + } + + @override + w.ValueType visitStaticInvocation( + StaticInvocation node, w.ValueType expectedType) { + w.ValueType? intrinsicResult = intrinsifier.generateStaticIntrinsic(node); + if (intrinsicResult != null) return intrinsicResult; + _visitArguments(node.arguments, node.targetReference, 0); + return _call(node.targetReference); + } + + Member _lookupSuperTarget(Member interfaceTarget, {required bool setter}) { + return translator.hierarchy.getDispatchTarget( + member.enclosingClass!.superclass!, interfaceTarget.name, + setter: setter)!; + } + + @override + w.ValueType visitSuperMethodInvocation( + SuperMethodInvocation node, w.ValueType expectedType) { + Reference target = + _lookupSuperTarget(node.interfaceTarget!, setter: false).reference; + w.BaseFunction targetFunction = translator.functions.getFunction(target); + w.ValueType receiverType = targetFunction.type.inputs.first; + w.ValueType thisType = _visitThis(receiverType); + translator.convertType(function, thisType, receiverType); + _visitArguments(node.arguments, target, 1); + return _call(target); + } + + @override + w.ValueType visitInstanceInvocation( + InstanceInvocation node, w.ValueType expectedType) { + w.ValueType? intrinsicResult = intrinsifier.generateInstanceIntrinsic(node); + if (intrinsicResult != null) return intrinsicResult; + Procedure target = node.interfaceTarget; + if (node.kind == InstanceAccessKind.Object) { + switch (target.name.text) { + case "toString": + late w.Label done; + w.ValueType resultType = _virtualCall(node, target, (signature) { + done = b.block(const [], signature.outputs); + w.Label nullString = b.block(); + wrap(node.receiver, translator.topInfo.nullableType); + b.br_on_null(nullString); + }, (_) { + _visitArguments(node.arguments, node.interfaceTargetReference, 1); + }, getter: false, setter: false); + b.br(done); + b.end(); + wrap(StringLiteral("null"), resultType); + b.end(); + return resultType; + default: + _unimplemented(node, "Nullable invocation of ${target.name.text}", + [if (expectedType != voidMarker) expectedType]); + return expectedType; + } + } + Member? singleTarget = translator.singleTarget(node); + if (singleTarget != null) { + w.BaseFunction targetFunction = + translator.functions.getFunction(singleTarget.reference); + wrap(node.receiver, targetFunction.type.inputs.first); + _visitArguments(node.arguments, node.interfaceTargetReference, 1); + return _call(singleTarget.reference); + } + return _virtualCall(node, target, + (signature) => wrap(node.receiver, signature.inputs.first), (_) { + _visitArguments(node.arguments, node.interfaceTargetReference, 1); + }, getter: false, setter: false); + } + + @override + w.ValueType visitDynamicInvocation( + DynamicInvocation node, w.ValueType expectedType) { + if (node.name.text != "call") { + _unimplemented(node, "Dynamic invocation of ${node.name.text}", + [if (expectedType != voidMarker) expectedType]); + return expectedType; + } + return _functionCall( + node.arguments.positional.length, node.receiver, node.arguments); + } + + @override + w.ValueType visitEqualsCall(EqualsCall node, w.ValueType expectedType) { + w.ValueType? intrinsicResult = intrinsifier.generateEqualsIntrinsic(node); + if (intrinsicResult != null) return intrinsicResult; + Member? singleTarget = translator.singleTarget(node); + if (singleTarget == translator.coreTypes.objectEquals) { + // Plain reference comparison + wrap(node.left, w.RefType.eq(nullable: true)); + wrap(node.right, w.RefType.eq(nullable: true)); + b.ref_eq(); + } else { + // Check operands for null, then call implementation + bool leftNullable = dartTypeOf(node.left).isPotentiallyNullable; + bool rightNullable = dartTypeOf(node.right).isPotentiallyNullable; + w.RefType leftType = translator.topInfo.typeWithNullability(leftNullable); + w.RefType rightType = + translator.topInfo.typeWithNullability(rightNullable); + w.Local leftLocal = addLocal(leftType); + w.Local rightLocal = addLocal(rightType); + w.Label? operandNull; + w.Label? done; + if (leftNullable || rightNullable) { + done = b.block(const [], const [w.NumType.i32]); + operandNull = b.block(); + } + wrap(node.left, leftLocal.type); + b.local_set(leftLocal); + wrap(node.right, rightLocal.type); + if (rightNullable) { + b.local_tee(rightLocal); + b.br_on_null(operandNull!); + b.drop(); + } else { + b.local_set(rightLocal); + } + + void left([_]) { + b.local_get(leftLocal); + if (leftNullable) { + b.br_on_null(operandNull!); + } else if (leftLocal.type.nullable) { + b.ref_as_non_null(); + } + } + + void right([_]) { + b.local_get(rightLocal); + if (rightLocal.type.nullable) { + b.ref_as_non_null(); + } + } + + if (singleTarget != null) { + left(); + right(); + _call(singleTarget.reference); + } else { + _virtualCall(node, node.interfaceTarget, left, right, + getter: false, setter: false); + } + if (leftNullable || rightNullable) { + b.br(done!); + b.end(); // operandNull + if (leftNullable && rightNullable) { + // Both sides nullable - compare references + b.local_get(leftLocal); + b.local_get(rightLocal); + b.ref_eq(); + } else { + // Only one side nullable - not equal if one is null + b.i32_const(0); + } + b.end(); // done + } + } + return w.NumType.i32; + } + + @override + w.ValueType visitEqualsNull(EqualsNull node, w.ValueType expectedType) { + wrap(node.expression, translator.topInfo.nullableType); + b.ref_is_null(); + return w.NumType.i32; + } + + w.ValueType _virtualCall( + TreeNode node, + Member interfaceTarget, + void pushReceiver(w.FunctionType signature), + void pushArguments(w.FunctionType signature), + {required bool getter, + required bool setter}) { + SelectorInfo selector = translator.dispatchTable.selectorForTarget( + interfaceTarget.referenceAs(getter: getter, setter: setter)); + assert(selector.name == interfaceTarget.name.text); + + pushReceiver(selector.signature); + + int? offset = selector.offset; + if (offset == null) { + // Singular target or unreachable call + assert(selector.targetCount <= 1); + if (selector.targetCount == 1) { + pushArguments(selector.signature); + return _call(selector.singularTarget!); + } else { + b.comment("Virtual call of ${selector.name} with no targets" + " at ${node.location}"); + b.drop(); + b.block(const [], selector.signature.outputs); + b.unreachable(); + b.end(); + return translator.outputOrVoid(selector.signature.outputs); + } + } + + // Receiver is already on stack. + w.Local receiverVar = addLocal(selector.signature.inputs.first); + b.local_tee(receiverVar); + if (options.parameterNullability && receiverVar.type.nullable) { + b.ref_as_non_null(); + } + pushArguments(selector.signature); + + if (options.polymorphicSpecialization) { + _polymorphicSpecialization(selector, receiverVar); + } else { + String access = getter ? "get" : (setter ? "set" : "call"); + b.comment("Instance $access of '${selector.name}'"); + b.local_get(receiverVar); + b.struct_get(translator.topInfo.struct, FieldIndex.classId); + if (offset != 0) { + b.i32_const(offset); + b.i32_add(); + } + b.call_indirect(selector.signature); + + translator.functions.activateSelector(selector); + } + + return translator.outputOrVoid(selector.signature.outputs); + } + + void _polymorphicSpecialization(SelectorInfo selector, w.Local receiver) { + Map implementations = Map.from(selector.targets); + implementations.removeWhere((id, target) => target.asMember.isAbstract); + + w.Local idVar = addLocal(w.NumType.i32); + b.local_get(receiver); + b.struct_get(translator.topInfo.struct, FieldIndex.classId); + b.local_set(idVar); + + w.Label block = + b.block(selector.signature.inputs, selector.signature.outputs); + calls: + while (Set.from(implementations.values).length > 1) { + for (int id in implementations.keys) { + Reference target = implementations[id]!; + if (implementations.values.where((t) => t == target).length == 1) { + // Single class id implements method. + b.local_get(idVar); + b.i32_const(id); + b.i32_eq(); + b.if_(selector.signature.inputs, selector.signature.inputs); + _call(target); + b.br(block); + b.end(); + implementations.remove(id); + continue calls; + } + } + // Find class id that separates remaining classes in two. + List sorted = implementations.keys.toList()..sort(); + int pivotId = sorted.firstWhere( + (id) => implementations[id] != implementations[sorted.first]); + // Fail compilation if no such id exists. + assert(sorted.lastWhere( + (id) => implementations[id] != implementations[pivotId]) == + pivotId - 1); + Reference target = implementations[sorted.first]!; + b.local_get(idVar); + b.i32_const(pivotId); + b.i32_lt_u(); + b.if_(selector.signature.inputs, selector.signature.inputs); + _call(target); + b.br(block); + b.end(); + for (int id in sorted) { + if (id == pivotId) break; + implementations.remove(id); + } + continue calls; + } + // Call remaining implementation. + Reference target = implementations.values.first; + _call(target); + b.end(); + } + + @override + w.ValueType visitVariableGet(VariableGet node, w.ValueType expectedType) { + w.Local? local = locals[node.variable]; + Capture? capture = closures.captures[node.variable]; + if (capture != null) { + if (!capture.written && local != null) { + b.local_get(local); + return local.type; + } else { + b.local_get(capture.context.currentLocal); + b.struct_get(capture.context.struct, capture.fieldIndex); + return capture.type; + } + } else { + if (local == null) { + throw "Read of undefined variable ${node.variable}"; + } + b.local_get(local); + return local.type; + } + } + + @override + w.ValueType visitVariableSet(VariableSet node, w.ValueType expectedType) { + w.Local? local = locals[node.variable]; + Capture? capture = closures.captures[node.variable]; + bool preserved = expectedType != voidMarker; + if (capture != null) { + assert(capture.written); + b.local_get(capture.context.currentLocal); + wrap(node.value, capture.type); + if (preserved) { + w.Local temp = addLocal(translateType(node.variable.type)); + b.local_tee(temp); + b.struct_set(capture.context.struct, capture.fieldIndex); + b.local_get(temp); + return temp.type; + } else { + b.struct_set(capture.context.struct, capture.fieldIndex); + return voidMarker; + } + } else { + if (local == null) { + throw "Write of undefined variable ${node.variable}"; + } + wrap(node.value, local.type); + if (preserved) { + b.local_tee(local); + return local.type; + } else { + b.local_set(local); + return voidMarker; + } + } + } + + @override + w.ValueType visitStaticGet(StaticGet node, w.ValueType expectedType) { + w.ValueType? intrinsicResult = + intrinsifier.generateStaticGetterIntrinsic(node); + if (intrinsicResult != null) return intrinsicResult; + Member target = node.target; + if (target is Field) { + return translator.globals.readGlobal(b, target); + } else { + return _call(target.reference); + } + } + + @override + w.ValueType visitStaticTearOff(StaticTearOff node, w.ValueType expectedType) { + translator.constants.instantiateConstant( + function, b, StaticTearOffConstant(node.target), expectedType); + return expectedType; + } + + @override + w.ValueType visitStaticSet(StaticSet node, w.ValueType expectedType) { + bool preserved = expectedType != voidMarker; + Member target = node.target; + if (target is Field) { + w.Global global = translator.globals.getGlobal(target); + wrap(node.value, global.type.type); + b.global_set(global); + if (preserved) { + b.global_get(global); + return global.type.type; + } else { + return voidMarker; + } + } else { + w.BaseFunction targetFunction = + translator.functions.getFunction(target.reference); + wrap(node.value, targetFunction.type.inputs.single); + w.Local? temp; + if (preserved) { + temp = addLocal(translateType(dartTypeOf(node.value))); + b.local_tee(temp); + } + _call(target.reference); + if (preserved) { + b.local_get(temp!); + return temp.type; + } else { + return voidMarker; + } + } + } + + @override + w.ValueType visitSuperPropertyGet( + SuperPropertyGet node, w.ValueType expectedType) { + Member target = _lookupSuperTarget(node.interfaceTarget!, setter: false); + if (target is Procedure && !target.isGetter) { + throw "Not supported: Super tear-off at ${node.location}"; + } + return _directGet(target, ThisExpression(), () => null); + } + + @override + w.ValueType visitSuperPropertySet( + SuperPropertySet node, w.ValueType expectedType) { + Member target = _lookupSuperTarget(node.interfaceTarget!, setter: true); + return _directSet(target, ThisExpression(), node.value, + preserved: expectedType != voidMarker); + } + + @override + w.ValueType visitInstanceGet(InstanceGet node, w.ValueType expectedType) { + Member target = node.interfaceTarget; + if (node.kind == InstanceAccessKind.Object) { + late w.Label doneLabel; + w.ValueType resultType = _virtualCall(node, target, (signature) { + doneLabel = b.block(const [], signature.outputs); + w.Label nullLabel = b.block(); + wrap(node.receiver, translator.topInfo.nullableType); + b.br_on_null(nullLabel); + }, (_) {}, getter: true, setter: false); + b.br(doneLabel); + b.end(); // nullLabel + switch (target.name.text) { + case "hashCode": + b.i64_const(2011); + break; + case "runtimeType": + wrap(ConstantExpression(TypeLiteralConstant(NullType())), resultType); + break; + default: + _unimplemented( + node, "Nullable get of ${target.name.text}", [resultType]); + break; + } + b.end(); // doneLabel + return resultType; + } + Member? singleTarget = translator.singleTarget(node); + if (singleTarget != null) { + return _directGet(singleTarget, node.receiver, + () => intrinsifier.generateInstanceGetterIntrinsic(node)); + } else { + return _virtualCall(node, target, + (signature) => wrap(node.receiver, signature.inputs.first), (_) {}, + getter: true, setter: false); + } + } + + @override + w.ValueType visitDynamicGet(DynamicGet node, w.ValueType expectedType) { + // Provisional implementation of dynamic get which assumes the getter + // is present (otherwise it traps or calls something random) and + // does not support tearoffs. This is sufficient to handle the + // dynamic .length calls in the core libraries. + + SelectorInfo selector = + translator.dispatchTable.selectorForDynamicName(node.name.text); + + // Evaluate receiver + wrap(node.receiver, selector.signature.inputs.first); + w.Local receiverVar = addLocal(selector.signature.inputs.first); + b.local_tee(receiverVar); + if (options.parameterNullability && receiverVar.type.nullable) { + b.ref_as_non_null(); + } + + // Dispatch table call + b.comment("Dynamic get of '${selector.name}'"); + int offset = selector.offset!; + b.local_get(receiverVar); + b.struct_get(translator.topInfo.struct, FieldIndex.classId); + if (offset != 0) { + b.i32_const(offset); + b.i32_add(); + } + b.call_indirect(selector.signature); + + translator.functions.activateSelector(selector); + + return translator.outputOrVoid(selector.signature.outputs); + } + + w.ValueType _directGet( + Member target, Expression receiver, w.ValueType? Function() intrinsify) { + if (target is Field) { + ClassInfo info = translator.classInfo[target.enclosingClass]!; + int fieldIndex = translator.fieldIndex[target]!; + w.ValueType receiverType = info.nullableType; + w.ValueType fieldType = info.struct.fields[fieldIndex].type.unpacked; + wrap(receiver, receiverType); + b.struct_get(info.struct, fieldIndex); + return fieldType; + } else { + // Instance call of getter + assert(target is Procedure && target.isGetter); + w.ValueType? intrinsicResult = intrinsify(); + if (intrinsicResult != null) return intrinsicResult; + w.BaseFunction targetFunction = + translator.functions.getFunction(target.reference); + wrap(receiver, targetFunction.type.inputs.single); + return _call(target.reference); + } + } + + @override + w.ValueType visitInstanceTearOff( + InstanceTearOff node, w.ValueType expectedType) { + return _virtualCall(node, node.interfaceTarget, + (signature) => wrap(node.receiver, signature.inputs.first), (_) {}, + getter: true, setter: false); + } + + @override + w.ValueType visitInstanceSet(InstanceSet node, w.ValueType expectedType) { + bool preserved = expectedType != voidMarker; + w.Local? temp; + Member? singleTarget = translator.singleTarget(node); + if (singleTarget != null) { + return _directSet(singleTarget, node.receiver, node.value, + preserved: preserved); + } else { + _virtualCall(node, node.interfaceTarget, + (signature) => wrap(node.receiver, signature.inputs.first), + (signature) { + w.ValueType paramType = signature.inputs.last; + wrap(node.value, paramType); + if (preserved) { + temp = addLocal(paramType); + b.local_tee(temp!); + } + }, getter: false, setter: true); + if (preserved) { + b.local_get(temp!); + return temp!.type; + } else { + return voidMarker; + } + } + } + + w.ValueType _directSet(Member target, Expression receiver, Expression value, + {required bool preserved}) { + w.Local? temp; + if (target is Field) { + ClassInfo info = translator.classInfo[target.enclosingClass]!; + int fieldIndex = translator.fieldIndex[target]!; + w.ValueType receiverType = info.nullableType; + w.ValueType fieldType = info.struct.fields[fieldIndex].type.unpacked; + wrap(receiver, receiverType); + wrap(value, fieldType); + if (preserved) { + temp = addLocal(fieldType); + b.local_tee(temp); + } + b.struct_set(info.struct, fieldIndex); + } else { + w.BaseFunction targetFunction = + translator.functions.getFunction(target.reference); + w.ValueType paramType = targetFunction.type.inputs.last; + wrap(receiver, targetFunction.type.inputs.first); + wrap(value, paramType); + if (preserved) { + temp = addLocal(paramType); + b.local_tee(temp); + translator.convertType(function, temp.type, paramType); + } + _call(target.reference); + } + if (preserved) { + b.local_get(temp!); + return temp.type; + } else { + return voidMarker; + } + } + + @override + void visitFunctionDeclaration(FunctionDeclaration node) { + Capture? capture = closures.captures[node.variable]; + bool locallyClosurized = closures.closurizedFunctions.contains(node); + if (capture != null || locallyClosurized) { + if (capture != null) { + b.local_get(capture.context.currentLocal); + } + w.StructType struct = _instantiateClosure(node.function); + if (locallyClosurized) { + w.Local local = addLocal(w.RefType.def(struct, nullable: false)); + locals[node.variable] = local; + if (capture != null) { + b.local_tee(local); + } else { + b.local_set(local); + } + } + if (capture != null) { + b.struct_set(capture.context.struct, capture.fieldIndex); + } + } + } + + @override + w.ValueType visitFunctionExpression( + FunctionExpression node, w.ValueType expectedType) { + w.StructType struct = _instantiateClosure(node.function); + return w.RefType.def(struct, nullable: false); + } + + w.StructType _instantiateClosure(FunctionNode functionNode) { + int parameterCount = functionNode.requiredParameterCount; + Lambda lambda = closures.lambdas[functionNode]!; + w.DefinedGlobal global = translator.makeFunctionRef(lambda.function); + + ClassInfo info = translator.classInfo[translator.functionClass]!; + translator.functions.allocateClass(info.classId); + w.StructType struct = translator.closureStructType(parameterCount); + + b.i32_const(info.classId); + b.i32_const(initialIdentityHash); + _pushContext(functionNode); + b.global_get(global); + translator.struct_new(b, parameterCount); + + return struct; + } + + void _pushContext(FunctionNode functionNode) { + Context? context = closures.contexts[functionNode]?.parent; + if (context != null) { + b.local_get(context.currentLocal); + if (context.currentLocal.type.nullable) { + b.ref_as_non_null(); + } + } else { + b.global_get(translator.globals.dummyGlobal); // Dummy context + } + } + + @override + w.ValueType visitFunctionInvocation( + FunctionInvocation node, w.ValueType expectedType) { + FunctionType functionType = node.functionType!; + int parameterCount = functionType.requiredParameterCount; + return _functionCall(parameterCount, node.receiver, node.arguments); + } + + w.ValueType _functionCall( + int parameterCount, Expression receiver, Arguments arguments) { + w.StructType struct = translator.closureStructType(parameterCount); + w.Local temp = addLocal(w.RefType.def(struct, nullable: false)); + wrap(receiver, temp.type); + b.local_tee(temp); + b.struct_get(struct, FieldIndex.closureContext); + for (Expression arg in arguments.positional) { + wrap(arg, translator.topInfo.nullableType); + } + b.local_get(temp); + b.struct_get(struct, FieldIndex.closureFunction); + b.call_ref(); + return translator.topInfo.nullableType; + } + + @override + w.ValueType visitLocalFunctionInvocation( + LocalFunctionInvocation node, w.ValueType expectedType) { + var decl = node.variable.parent as FunctionDeclaration; + _pushContext(decl.function); + for (Expression arg in node.arguments.positional) { + wrap(arg, translator.topInfo.nullableType); + } + Lambda lambda = closures.lambdas[decl.function]!; + b.comment("Local call of ${decl.variable.name}"); + b.call(lambda.function); + return translator.topInfo.nullableType; + } + + @override + w.ValueType visitLogicalExpression( + LogicalExpression node, w.ValueType expectedType) { + _conditional(node, () => b.i32_const(1), () => b.i32_const(0), + const [w.NumType.i32]); + return w.NumType.i32; + } + + @override + w.ValueType visitNot(Not node, w.ValueType expectedType) { + wrap(node.operand, w.NumType.i32); + b.i32_eqz(); + return w.NumType.i32; + } + + @override + w.ValueType visitConditionalExpression( + ConditionalExpression node, w.ValueType expectedType) { + _conditional( + node.condition, + () => wrap(node.then, expectedType), + () => wrap(node.otherwise, expectedType), + [if (expectedType != voidMarker) expectedType]); + return expectedType; + } + + @override + w.ValueType visitNullCheck(NullCheck node, w.ValueType expectedType) { + // TODO(joshualitt): Check and throw exception + return wrap(node.operand, expectedType); + } + + void _visitArguments(Arguments node, Reference target, int signatureOffset) { + final w.FunctionType signature = translator.signatureFor(target); + final ParameterInfo paramInfo = translator.paramInfoFor(target); + for (int i = 0; i < node.types.length; i++) { + _makeType(node.types[i], node); + } + signatureOffset += node.types.length; + for (int i = 0; i < node.positional.length; i++) { + wrap(node.positional[i], signature.inputs[signatureOffset + i]); + } + // Default values for positional parameters + for (int i = node.positional.length; i < paramInfo.positional.length; i++) { + final w.ValueType type = signature.inputs[signatureOffset + i]; + translator.constants + .instantiateConstant(function, b, paramInfo.positional[i]!, type); + } + // Named arguments + final Map namedLocals = {}; + for (var namedArg in node.named) { + final w.ValueType type = signature + .inputs[signatureOffset + paramInfo.nameIndex[namedArg.name]!]; + final w.Local namedLocal = addLocal(type); + namedLocals[namedArg.name] = namedLocal; + wrap(namedArg.value, namedLocal.type); + b.local_set(namedLocal); + } + for (String name in paramInfo.names) { + w.Local? namedLocal = namedLocals[name]; + final w.ValueType type = + signature.inputs[signatureOffset + paramInfo.nameIndex[name]!]; + if (namedLocal != null) { + b.local_get(namedLocal); + translator.convertType(function, namedLocal.type, type); + } else { + translator.constants + .instantiateConstant(function, b, paramInfo.named[name]!, type); + } + } + } + + @override + w.ValueType visitStringConcatenation( + StringConcatenation node, w.ValueType expectedType) { + _makeList( + node.expressions, + translator.fixedLengthListClass, + InterfaceType(translator.stringBaseClass, Nullability.nonNullable), + node); + return _call(translator.stringInterpolate.reference); + } + + @override + w.ValueType visitThrow(Throw node, w.ValueType expectedType) { + wrap(node.expression, translator.topInfo.nullableType); + // TODO(joshualitt): Throw exception + b.comment(node.toStringInternal()); + b.drop(); + b.block(const [], [if (expectedType != voidMarker) expectedType]); + b.unreachable(); + b.end(); + return expectedType; + } + + @override + w.ValueType visitInstantiation(Instantiation node, w.ValueType expectedType) { + throw "Not supported: Generic function instantiation at ${node.location}"; + } + + @override + w.ValueType visitConstantExpression( + ConstantExpression node, w.ValueType expectedType) { + translator.constants + .instantiateConstant(function, b, node.constant, expectedType); + return expectedType; + } + + @override + w.ValueType visitNullLiteral(NullLiteral node, w.ValueType expectedType) { + translator.constants + .instantiateConstant(function, b, NullConstant(), expectedType); + return expectedType; + } + + @override + w.ValueType visitStringLiteral(StringLiteral node, w.ValueType expectedType) { + translator.constants.instantiateConstant( + function, b, StringConstant(node.value), expectedType); + return expectedType; + } + + @override + w.ValueType visitBoolLiteral(BoolLiteral node, w.ValueType expectedType) { + b.i32_const(node.value ? 1 : 0); + return w.NumType.i32; + } + + @override + w.ValueType visitIntLiteral(IntLiteral node, w.ValueType expectedType) { + b.i64_const(node.value); + return w.NumType.i64; + } + + @override + w.ValueType visitDoubleLiteral(DoubleLiteral node, w.ValueType expectedType) { + b.f64_const(node.value); + return w.NumType.f64; + } + + @override + w.ValueType visitListLiteral(ListLiteral node, w.ValueType expectedType) { + return _makeList(node.expressions, translator.growableListClass, + node.typeArgument, node); + } + + w.ValueType _makeList(List expressions, Class cls, + DartType typeArg, TreeNode node) { + ClassInfo info = translator.classInfo[cls]!; + translator.functions.allocateClass(info.classId); + w.RefType refType = info.struct.fields.last.type.unpacked as w.RefType; + w.ArrayType arrayType = refType.heapType as w.ArrayType; + w.ValueType elementType = arrayType.elementType.type.unpacked; + int length = expressions.length; + + b.i32_const(info.classId); + b.i32_const(initialIdentityHash); + _makeType(typeArg, node); + b.i64_const(length); + if (options.lazyConstants) { + // Avoid array.init instruction in lazy constants mode + b.i32_const(length); + translator.array_new_default(b, arrayType); + if (length > 0) { + w.Local arrayLocal = addLocal(refType.withNullability(false)); + b.local_set(arrayLocal); + for (int i = 0; i < length; i++) { + b.local_get(arrayLocal); + b.i32_const(i); + wrap(expressions[i], elementType); + b.array_set(arrayType); + } + b.local_get(arrayLocal); + if (arrayLocal.type.nullable) { + b.ref_as_non_null(); + } + } + } else { + for (Expression expression in expressions) { + wrap(expression, elementType); + } + translator.array_init(b, arrayType, length); + } + translator.struct_new(b, info); + + return info.nonNullableType; + } + + @override + w.ValueType visitMapLiteral(MapLiteral node, w.ValueType expectedType) { + w.BaseFunction mapFactory = + translator.functions.getFunction(translator.mapFactory.reference); + w.ValueType factoryReturnType = mapFactory.type.outputs.single; + _makeType(node.keyType, node); + _makeType(node.valueType, node); + b.call(mapFactory); + if (node.entries.isEmpty) { + return factoryReturnType; + } + w.BaseFunction mapPut = + translator.functions.getFunction(translator.mapPut.reference); + w.ValueType putReceiverType = mapPut.type.inputs[0]; + w.ValueType putKeyType = mapPut.type.inputs[1]; + w.ValueType putValueType = mapPut.type.inputs[2]; + w.Local mapLocal = addLocal(putReceiverType); + translator.convertType(function, factoryReturnType, mapLocal.type); + b.local_set(mapLocal); + for (MapLiteralEntry entry in node.entries) { + b.local_get(mapLocal); + translator.convertType(function, mapLocal.type, putReceiverType); + wrap(entry.key, putKeyType); + wrap(entry.value, putValueType); + b.call(mapPut); + } + b.local_get(mapLocal); + return mapLocal.type; + } + + @override + w.ValueType visitTypeLiteral(TypeLiteral node, w.ValueType expectedType) { + return _makeType(node.type, node); + } + + w.ValueType _makeType(DartType type, TreeNode node) { + w.ValueType typeType = + translator.classInfo[translator.typeClass]!.nullableType; + if (_isTypeConstant(type)) { + return wrap(ConstantExpression(TypeLiteralConstant(type)), typeType); + } + if (type is TypeParameterType) { + if (type.parameter.parent is FunctionNode) { + // Type argument to function + w.Local? local = typeLocals[type.parameter]; + if (local != null) { + b.local_get(local); + return local.type; + } else { + _unimplemented( + node, "Type parameter access inside lambda", [typeType]); + return typeType; + } + } + // Type argument of class + Class cls = type.parameter.parent as Class; + ClassInfo info = translator.classInfo[cls]!; + int fieldIndex = translator.typeParameterIndex[type.parameter]!; + w.ValueType thisType = _visitThis(info.nullableType); + translator.convertType(function, thisType, info.nullableType); + b.struct_get(info.struct, fieldIndex); + return typeType; + } + ClassInfo info = translator.classInfo[translator.typeClass]!; + translator.functions.allocateClass(info.classId); + if (type is FutureOrType) { + // TODO(askesc): Have an actual representation of FutureOr types + b.ref_null(info.nullableType.heapType); + return info.nullableType; + } + if (type is! InterfaceType) { + _unimplemented(node, type, [info.nullableType]); + return info.nullableType; + } + ClassInfo typeInfo = translator.classInfo[type.classNode]!; + w.ValueType typeListExpectedType = info.struct.fields[3].type.unpacked; + b.i32_const(info.classId); + b.i32_const(initialIdentityHash); + b.i64_const(typeInfo.classId); + if (type.typeArguments.isEmpty) { + b.global_get(translator.constants.emptyTypeList); + translator.convertType(function, + translator.constants.emptyTypeList.type.type, typeListExpectedType); + } else if (type.typeArguments.every(_isTypeConstant)) { + ListConstant typeArgs = ListConstant( + InterfaceType(translator.typeClass, Nullability.nonNullable), + type.typeArguments.map((t) => TypeLiteralConstant(t)).toList()); + translator.constants + .instantiateConstant(function, b, typeArgs, typeListExpectedType); + } else { + w.ValueType listType = _makeList( + type.typeArguments.map((t) => TypeLiteral(t)).toList(), + translator.fixedLengthListClass, + InterfaceType(translator.typeClass, Nullability.nonNullable), + node); + translator.convertType(function, listType, typeListExpectedType); + } + translator.struct_new(b, info); + return info.nullableType; + } + + bool _isTypeConstant(DartType type) { + return type is DynamicType || + type is VoidType || + type is NeverType || + type is NullType || + type is FunctionType || + type is InterfaceType && type.typeArguments.every(_isTypeConstant); + } + + @override + w.ValueType visitIsExpression(IsExpression node, w.ValueType expectedType) { + wrap(node.operand, translator.topInfo.nullableType); + emitTypeTest(node.type, dartTypeOf(node.operand), node); + return w.NumType.i32; + } + + /// Test value against a Dart type. Expects the value on the stack as a + /// (ref null #Top) and leaves the result on the stack as an i32. + void emitTypeTest(DartType type, DartType operandType, TreeNode node) { + if (type is! InterfaceType) { + // TODO(askesc): Implement type test for remaining types + print("Not implemented: Type test with non-interface type $type" + " at ${node.location}"); + b.drop(); + b.i32_const(1); + return; + } + bool isNullable = operandType.isPotentiallyNullable; + w.Label? resultLabel; + if (isNullable) { + // Store operand in a temporary variable, since Binaryen does not support + // block inputs. + w.Local operand = addLocal(translator.topInfo.nullableType); + b.local_set(operand); + resultLabel = b.block(const [], const [w.NumType.i32]); + w.Label nullLabel = b.block(const [], const []); + b.local_get(operand); + b.br_on_null(nullLabel); + } + if (type.typeArguments.any((t) => t is! DynamicType)) { + // If the tested-against type as an instance of the static operand type + // has the same type arguments as the static operand type, it is not + // necessary to test the type arguments. + Class cls = translator.classForType(operandType); + InterfaceType? base = translator.hierarchy + .getTypeAsInstanceOf(type, cls, member.enclosingLibrary) + ?.withDeclaredNullability(operandType.declaredNullability); + if (base != operandType) { + print("Not implemented: Type test with type arguments" + " at ${node.location}"); + } + } + List concrete = translator.subtypes + .getSubtypesOf(type.classNode) + .where((c) => !c.isAbstract) + .toList(); + if (concrete.isEmpty) { + b.drop(); + b.i32_const(0); + } else if (concrete.length == 1) { + ClassInfo info = translator.classInfo[concrete.single]!; + b.struct_get(translator.topInfo.struct, FieldIndex.classId); + b.i32_const(info.classId); + b.i32_eq(); + } else { + w.Local idLocal = addLocal(w.NumType.i32); + b.struct_get(translator.topInfo.struct, FieldIndex.classId); + b.local_set(idLocal); + w.Label done = b.block(const [], const [w.NumType.i32]); + b.i32_const(1); + for (Class cls in concrete) { + ClassInfo info = translator.classInfo[cls]!; + b.i32_const(info.classId); + b.local_get(idLocal); + b.i32_eq(); + b.br_if(done); + } + b.drop(); + b.i32_const(0); + b.end(); // done + } + if (isNullable) { + b.br(resultLabel!); + b.end(); // nullLabel + b.i32_const(type.declaredNullability == Nullability.nullable ? 1 : 0); + b.end(); // resultLabel + } + } + + @override + w.ValueType visitAsExpression(AsExpression node, w.ValueType expectedType) { + // TODO(joshualitt): Emit type test and throw exception on failure + return wrap(node.operand, expectedType); + } +} diff --git a/pkg/dart2wasm/lib/compile.dart b/pkg/dart2wasm/lib/compile.dart new file mode 100644 index 00000000000..c69fdabfe99 --- /dev/null +++ b/pkg/dart2wasm/lib/compile.dart @@ -0,0 +1,69 @@ +// 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:front_end/src/api_unstable/vm.dart' + show + CompilerOptions, + CompilerResult, + DiagnosticMessage, + kernelForProgram, + Severity; + +import 'package:kernel/ast.dart'; +import 'package:kernel/core_types.dart'; +import 'package:kernel/target/targets.dart'; +import 'package:kernel/type_environment.dart'; + +import 'package:vm/transformations/type_flow/transformer.dart' as globalTypeFlow + show transformComponent; + +import 'package:dart2wasm/target.dart'; +import 'package:dart2wasm/translator.dart'; + +/// 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. +Future compileToModule( + Uri mainUri, + Uri sdkRoot, + TranslatorOptions options, + void Function(DiagnosticMessage) handleDiagnosticMessage) async { + var succeeded = true; + void diagnosticMessageHandler(DiagnosticMessage message) { + if (message.severity == Severity.error) { + succeeded = false; + } + handleDiagnosticMessage(message); + } + + Target target = WasmTarget(); + CompilerOptions compilerOptions = CompilerOptions() + ..target = target + ..compileSdk = true + ..sdkRoot = sdkRoot + ..environmentDefines = {} + ..verbose = false + ..onDiagnostic = diagnosticMessageHandler; + + CompilerResult? compilerResult = + await kernelForProgram(mainUri, compilerOptions); + if (compilerResult == null || !succeeded) { + return null; + } + Component component = compilerResult.component!; + CoreTypes coreTypes = compilerResult.coreTypes!; + + globalTypeFlow.transformComponent(target, coreTypes, component, + treeShakeSignatures: true, + treeShakeWriteOnlyFields: true, + useRapidTypeAnalysis: false); + + var translator = Translator(component, coreTypes, + TypeEnvironment(coreTypes, compilerResult.classHierarchy!), options); + return translator.translate(); +} diff --git a/pkg/dart2wasm/lib/constants.dart b/pkg/dart2wasm/lib/constants.dart new file mode 100644 index 00000000000..789f070dafe --- /dev/null +++ b/pkg/dart2wasm/lib/constants.dart @@ -0,0 +1,637 @@ +// 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:dart2wasm/class_info.dart'; +import 'package:dart2wasm/translator.dart'; + +import 'package:kernel/ast.dart'; +import 'package:kernel/type_algebra.dart' show substitute; + +import 'package:wasm_builder/wasm_builder.dart' as w; + +class ConstantInfo { + final Constant constant; + final w.DefinedGlobal global; + final w.DefinedFunction? function; + + ConstantInfo(this.constant, this.global, this.function); +} + +typedef ConstantCodeGenerator = void Function( + w.DefinedFunction?, w.Instructions); + +/// Handles the creation of Dart constants. Can operate in two modes - eager and +/// lazy - controlled by [TranslatorOptions.lazyConstants]. +/// +/// Each (non-trivial) constant is assigned to a Wasm global. Multiple +/// occurrences of the same constant use the same global. +/// +/// In eager mode, the constant is contained within the global initializer, +/// meaning all constants are initialized eagerly during module initialization. +/// In lazy mode, the global starts out uninitialized, and every use of the +/// constant checks the global to see if it has been initialized and calls an +/// initialization function otherwise. +class Constants { + final Translator translator; + final Map constantInfo = {}; + final StringBuffer oneByteStrings = StringBuffer(); + final StringBuffer twoByteStrings = StringBuffer(); + late final w.DefinedFunction oneByteStringFunction; + late final w.DefinedFunction twoByteStringFunction; + late final w.DataSegment oneByteStringSegment; + late final w.DataSegment twoByteStringSegment; + late final w.DefinedGlobal emptyString; + late final w.DefinedGlobal emptyTypeList; + late final ClassInfo typeInfo = translator.classInfo[translator.typeClass]!; + + bool currentlyCreating = false; + + Constants(this.translator) { + if (lazyConstants) { + oneByteStringFunction = makeStringFunction(translator.oneByteStringClass); + twoByteStringFunction = makeStringFunction(translator.twoByteStringClass); + } else if (stringDataSegments) { + oneByteStringSegment = m.addDataSegment(); + twoByteStringSegment = m.addDataSegment(); + } + initEmptyString(); + initEmptyTypeList(); + } + + w.Module get m => translator.m; + bool get lazyConstants => translator.options.lazyConstants; + bool get stringDataSegments => translator.options.stringDataSegments; + + void initEmptyString() { + ClassInfo info = translator.classInfo[translator.oneByteStringClass]!; + translator.functions.allocateClass(info.classId); + w.ArrayType arrayType = + (info.struct.fields.last.type as w.RefType).heapType as w.ArrayType; + + if (lazyConstants) { + w.RefType emptyStringType = info.nullableType; + emptyString = m.addGlobal(w.GlobalType(emptyStringType)); + emptyString.initializer.ref_null(emptyStringType.heapType); + emptyString.initializer.end(); + + w.Instructions b = translator.initFunction.body; + b.i32_const(info.classId); + b.i32_const(initialIdentityHash); + b.i32_const(0); + translator.array_new_default(b, arrayType); + translator.struct_new(b, info); + b.global_set(emptyString); + } else { + w.RefType emptyStringType = info.nonNullableType; + emptyString = m.addGlobal(w.GlobalType(emptyStringType, mutable: false)); + w.Instructions ib = emptyString.initializer; + ib.i32_const(info.classId); + ib.i32_const(initialIdentityHash); + translator.array_init(ib, arrayType, 0); + translator.struct_new(ib, info); + ib.end(); + } + + Constant emptyStringConstant = StringConstant(""); + constantInfo[emptyStringConstant] = + ConstantInfo(emptyStringConstant, emptyString, null); + } + + void initEmptyTypeList() { + ClassInfo info = translator.classInfo[translator.immutableListClass]!; + translator.functions.allocateClass(info.classId); + w.RefType refType = info.struct.fields.last.type.unpacked as w.RefType; + w.ArrayType arrayType = refType.heapType as w.ArrayType; + + // Create the empty type list with its type parameter uninitialized for now. + if (lazyConstants) { + w.RefType emptyListType = info.nullableType; + emptyTypeList = m.addGlobal(w.GlobalType(emptyListType)); + emptyTypeList.initializer.ref_null(emptyListType.heapType); + emptyTypeList.initializer.end(); + + w.Instructions b = translator.initFunction.body; + b.i32_const(info.classId); + b.i32_const(initialIdentityHash); + b.ref_null(typeInfo.struct); // Initialized later + b.i64_const(0); + b.i32_const(0); + translator.array_new_default(b, arrayType); + translator.struct_new(b, info); + b.global_set(emptyTypeList); + } else { + w.RefType emptyListType = info.nonNullableType; + emptyTypeList = m.addGlobal(w.GlobalType(emptyListType, mutable: false)); + w.Instructions ib = emptyTypeList.initializer; + ib.i32_const(info.classId); + ib.i32_const(initialIdentityHash); + ib.ref_null(typeInfo.struct); // Initialized later + ib.i64_const(0); + translator.array_init(ib, arrayType, 0); + translator.struct_new(ib, info); + ib.end(); + } + + Constant emptyTypeListConstant = ListConstant( + InterfaceType(translator.typeClass, Nullability.nonNullable), const []); + constantInfo[emptyTypeListConstant] = + ConstantInfo(emptyTypeListConstant, emptyTypeList, null); + + // Initialize the type parameter of the empty type list to the type object + // for _Type, which itself refers to the empty type list. + w.Instructions b = translator.initFunction.body; + b.global_get(emptyTypeList); + instantiateConstant( + translator.initFunction, + b, + TypeLiteralConstant( + InterfaceType(translator.typeClass, Nullability.nonNullable)), + typeInfo.nullableType); + b.struct_set(info.struct, + translator.typeParameterIndex[info.cls!.typeParameters.single]!); + } + + void finalize() { + if (lazyConstants) { + finalizeStrings(); + } + } + + void finalizeStrings() { + Uint8List oneByteStringsAsBytes = + Uint8List.fromList(oneByteStrings.toString().codeUnits); + assert(Endian.host == Endian.little); + Uint8List twoByteStringsAsBytes = + Uint16List.fromList(twoByteStrings.toString().codeUnits) + .buffer + .asUint8List(); + Uint8List stringsAsBytes = (BytesBuilder() + ..add(twoByteStringsAsBytes) + ..add(oneByteStringsAsBytes)) + .toBytes(); + + w.Memory stringMemory = + m.addMemory(stringsAsBytes.length, stringsAsBytes.length); + m.addDataSegment(stringsAsBytes, stringMemory, 0); + makeStringFunctionBody(translator.oneByteStringClass, oneByteStringFunction, + (b) { + b.i32_load8_u(stringMemory, twoByteStringsAsBytes.length); + }); + makeStringFunctionBody(translator.twoByteStringClass, twoByteStringFunction, + (b) { + b.i32_const(1); + b.i32_shl(); + b.i32_load16_u(stringMemory, 0); + }); + } + + /// Create one of the two Wasm functions (one for each string type) called + /// from every lazily initialized string constant (of that type) to create and + /// initialize the string. + /// + /// The function signature is (i32 offset, i32 length) -> (ref stringClass) + /// where offset and length are measured in characters and indicate the place + /// in the corresponding string data segment from which to copy this string. + w.DefinedFunction makeStringFunction(Class cls) { + ClassInfo info = translator.classInfo[cls]!; + w.FunctionType ftype = translator.functionType( + const [w.NumType.i32, w.NumType.i32], [info.nonNullableType]); + return m.addFunction(ftype, "makeString (${cls.name})"); + } + + void makeStringFunctionBody(Class cls, w.DefinedFunction function, + void Function(w.Instructions) emitLoad) { + ClassInfo info = translator.classInfo[cls]!; + w.ArrayType arrayType = + (info.struct.fields.last.type as w.RefType).heapType as w.ArrayType; + + w.Local offset = function.locals[0]; + w.Local length = function.locals[1]; + w.Local array = function.addLocal( + translator.typeForLocal(w.RefType.def(arrayType, nullable: false))); + w.Local index = function.addLocal(w.NumType.i32); + + w.Instructions b = function.body; + b.local_get(length); + translator.array_new_default(b, arrayType); + b.local_set(array); + + b.i32_const(0); + b.local_set(index); + w.Label loop = b.loop(); + b.local_get(array); + b.local_get(index); + b.local_get(offset); + b.local_get(index); + b.i32_add(); + emitLoad(b); + b.array_set(arrayType); + b.local_get(index); + b.i32_const(1); + b.i32_add(); + b.local_tee(index); + b.local_get(length); + b.i32_lt_u(); + b.br_if(loop); + b.end(); + + b.i32_const(info.classId); + b.i32_const(initialIdentityHash); + b.local_get(array); + translator.struct_new(b, info); + b.end(); + } + + /// Ensure that the constant has a Wasm global assigned. + /// + /// In eager mode, sub-constants must have Wasm globals assigned before the + /// global for the composite constant is assigned, since global initializers + /// can only refer to earlier globals. + void ensureConstant(Constant constant) { + ConstantCreator(this).ensureConstant(constant); + } + + /// Emit code to push a constant onto the stack. + void instantiateConstant(w.DefinedFunction? function, w.Instructions b, + Constant constant, w.ValueType expectedType) { + if (expectedType == translator.voidMarker) return; + ConstantInstantiator(this, function, b, expectedType).instantiate(constant); + } +} + +class ConstantInstantiator extends ConstantVisitor { + final Constants constants; + final w.DefinedFunction? function; + final w.Instructions b; + final w.ValueType expectedType; + + ConstantInstantiator( + this.constants, this.function, this.b, this.expectedType); + + Translator get translator => constants.translator; + w.Module get m => translator.m; + + void instantiate(Constant constant) { + w.ValueType resultType = constant.accept(this); + assert(!translator.needsConversion(resultType, expectedType), + "For $constant: expected $expectedType, got $resultType"); + } + + @override + w.ValueType defaultConstant(Constant constant) { + ConstantInfo info = ConstantCreator(constants).ensureConstant(constant)!; + w.ValueType globalType = info.global.type.type; + if (globalType.nullable) { + if (info.function != null) { + // Lazily initialized constant. + w.Label done = b.block(const [], [globalType.withNullability(false)]); + b.global_get(info.global); + b.br_on_non_null(done); + b.call(info.function!); + b.end(); + } else { + // Constant initialized in the module init function. + b.global_get(info.global); + b.ref_as_non_null(); + } + return globalType.withNullability(false); + } else { + // Constant initialized eagerly in a global initializer. + b.global_get(info.global); + return globalType; + } + } + + @override + w.ValueType visitNullConstant(NullConstant node) { + w.ValueType? expectedType = this.expectedType; + if (expectedType != translator.voidMarker) { + if (expectedType.nullable) { + w.HeapType heapType = + expectedType is w.RefType ? expectedType.heapType : w.HeapType.data; + b.ref_null(heapType); + } else { + // This only happens in invalid but unreachable code produced by the + // TFA dead-code elimination. + b.comment("Non-nullable null constant"); + b.block(const [], [expectedType]); + b.unreachable(); + b.end(); + } + } + return expectedType; + } + + w.ValueType _maybeBox(w.ValueType wasmType, void Function() pushValue) { + if (expectedType is w.RefType) { + ClassInfo info = translator.classInfo[translator.boxedClasses[wasmType]]!; + b.i32_const(info.classId); + pushValue(); + translator.struct_new(b, info); + return info.nonNullableType; + } else { + pushValue(); + return wasmType; + } + } + + @override + w.ValueType visitBoolConstant(BoolConstant constant) { + return _maybeBox(w.NumType.i32, () { + b.i32_const(constant.value ? 1 : 0); + }); + } + + @override + w.ValueType visitIntConstant(IntConstant constant) { + return _maybeBox(w.NumType.i64, () { + b.i64_const(constant.value); + }); + } + + @override + w.ValueType visitDoubleConstant(DoubleConstant constant) { + return _maybeBox(w.NumType.f64, () { + b.f64_const(constant.value); + }); + } +} + +class ConstantCreator extends ConstantVisitor { + final Constants constants; + + ConstantCreator(this.constants); + + Translator get translator => constants.translator; + w.Module get m => constants.m; + bool get lazyConstants => constants.lazyConstants; + + ConstantInfo? ensureConstant(Constant constant) { + ConstantInfo? info = constants.constantInfo[constant]; + if (info == null) { + info = constant.accept(this); + if (info != null) { + constants.constantInfo[constant] = info; + } + } + return info; + } + + ConstantInfo createConstant( + Constant constant, w.RefType type, ConstantCodeGenerator generator) { + assert(!type.nullable); + if (lazyConstants) { + // Create uninitialized global and function to initialize it. + w.DefinedGlobal global = + m.addGlobal(w.GlobalType(type.withNullability(true))); + global.initializer.ref_null(type.heapType); + global.initializer.end(); + w.FunctionType ftype = translator.functionType(const [], [type]); + w.DefinedFunction function = m.addFunction(ftype, "$constant"); + generator(function, function.body); + w.Local temp = function.addLocal(translator.typeForLocal(type)); + w.Instructions b2 = function.body; + b2.local_tee(temp); + b2.global_set(global); + b2.local_get(temp); + translator.convertType(function, temp.type, type); + b2.end(); + + return ConstantInfo(constant, global, function); + } else { + // Create global with the constant in its initializer. + assert(!constants.currentlyCreating); + constants.currentlyCreating = true; + w.DefinedGlobal global = m.addGlobal(w.GlobalType(type, mutable: false)); + generator(null, global.initializer); + global.initializer.end(); + constants.currentlyCreating = false; + + return ConstantInfo(constant, global, null); + } + } + + @override + ConstantInfo? defaultConstant(Constant constant) => null; + + @override + ConstantInfo? visitStringConstant(StringConstant constant) { + bool isOneByte = constant.value.codeUnits.every((c) => c <= 255); + ClassInfo info = translator.classInfo[isOneByte + ? translator.oneByteStringClass + : translator.twoByteStringClass]!; + translator.functions.allocateClass(info.classId); + w.RefType type = info.nonNullableType; + return createConstant(constant, type, (function, b) { + if (lazyConstants) { + // Copy string contents from linear memory on initialization. The memory + // is initialized by an active data segment for each string type. + StringBuffer buffer = + isOneByte ? constants.oneByteStrings : constants.twoByteStrings; + int offset = buffer.length; + int length = constant.value.length; + buffer.write(constant.value); + + b.i32_const(offset); + b.i32_const(length); + b.call(isOneByte + ? constants.oneByteStringFunction + : constants.twoByteStringFunction); + } else { + w.ArrayType arrayType = + (info.struct.fields.last.type as w.RefType).heapType as w.ArrayType; + + b.i32_const(info.classId); + b.i32_const(initialIdentityHash); + if (constants.stringDataSegments) { + // Initialize string contents from passive data segment. + w.DataSegment segment; + Uint8List bytes; + if (isOneByte) { + segment = constants.oneByteStringSegment; + bytes = Uint8List.fromList(constant.value.codeUnits); + } else { + assert(Endian.host == Endian.little); + segment = constants.twoByteStringSegment; + bytes = Uint16List.fromList(constant.value.codeUnits) + .buffer + .asUint8List(); + } + int offset = segment.length; + segment.append(bytes); + b.i32_const(offset); + b.i32_const(constant.value.length); + translator.array_init_from_data(b, arrayType, segment); + } else { + // Initialize string contents from i32 constants on the stack. + for (int charCode in constant.value.codeUnits) { + b.i32_const(charCode); + } + translator.array_init(b, arrayType, constant.value.length); + } + translator.struct_new(b, info); + } + }); + } + + @override + ConstantInfo? visitInstanceConstant(InstanceConstant constant) { + Class cls = constant.classNode; + ClassInfo info = translator.classInfo[cls]!; + translator.functions.allocateClass(info.classId); + w.RefType type = info.nonNullableType; + + // Collect sub-constants for field values. + const int baseFieldCount = 2; + int fieldCount = info.struct.fields.length; + List subConstants = List.filled(fieldCount, null); + constant.fieldValues.forEach((reference, subConstant) { + int index = translator.fieldIndex[reference.asField]!; + assert(subConstants[index] == null); + subConstants[index] = subConstant; + ensureConstant(subConstant); + }); + + // Collect sub-constants for type arguments. + Map substitution = {}; + List args = constant.typeArguments; + while (true) { + for (int i = 0; i < cls.typeParameters.length; i++) { + TypeParameter parameter = cls.typeParameters[i]; + DartType arg = substitute(args[i], substitution); + substitution[parameter] = arg; + int index = translator.typeParameterIndex[parameter]!; + Constant typeArgConstant = TypeLiteralConstant(arg); + subConstants[index] = typeArgConstant; + ensureConstant(typeArgConstant); + } + Supertype? supertype = cls.supertype; + if (supertype == null) break; + cls = supertype.classNode; + args = supertype.typeArguments; + } + + return createConstant(constant, type, (function, b) { + b.i32_const(info.classId); + b.i32_const(initialIdentityHash); + for (int i = baseFieldCount; i < fieldCount; i++) { + Constant subConstant = subConstants[i]!; + constants.instantiateConstant( + function, b, subConstant, info.struct.fields[i].type.unpacked); + } + translator.struct_new(b, info); + }); + } + + @override + ConstantInfo? visitListConstant(ListConstant constant) { + Constant typeArgConstant = TypeLiteralConstant(constant.typeArgument); + ensureConstant(typeArgConstant); + for (Constant subConstant in constant.entries) { + ensureConstant(subConstant); + } + + ClassInfo info = translator.classInfo[translator.immutableListClass]!; + translator.functions.allocateClass(info.classId); + w.RefType type = info.nonNullableType; + return createConstant(constant, type, (function, b) { + w.RefType refType = info.struct.fields.last.type.unpacked as w.RefType; + w.ArrayType arrayType = refType.heapType as w.ArrayType; + w.ValueType elementType = arrayType.elementType.type.unpacked; + int length = constant.entries.length; + b.i32_const(info.classId); + b.i32_const(initialIdentityHash); + constants.instantiateConstant( + function, b, typeArgConstant, constants.typeInfo.nullableType); + b.i64_const(length); + if (lazyConstants) { + // Allocate array and set each entry to the corresponding sub-constant. + w.Local arrayLocal = function!.addLocal( + refType.withNullability(!translator.options.localNullability)); + b.i32_const(length); + translator.array_new_default(b, arrayType); + b.local_set(arrayLocal); + for (int i = 0; i < length; i++) { + b.local_get(arrayLocal); + b.i32_const(i); + constants.instantiateConstant( + function, b, constant.entries[i], elementType); + b.array_set(arrayType); + } + b.local_get(arrayLocal); + if (arrayLocal.type.nullable) { + b.ref_as_non_null(); + } + } else { + // Push all sub-constants on the stack and initialize array from them. + for (int i = 0; i < length; i++) { + constants.instantiateConstant( + function, b, constant.entries[i], elementType); + } + translator.array_init(b, arrayType, length); + } + translator.struct_new(b, info); + }); + } + + @override + ConstantInfo? visitStaticTearOffConstant(StaticTearOffConstant constant) { + w.DefinedFunction closureFunction = + translator.getTearOffFunction(constant.targetReference.asProcedure); + int parameterCount = closureFunction.type.inputs.length - 1; + w.StructType struct = translator.closureStructType(parameterCount); + w.RefType type = w.RefType.def(struct, nullable: false); + return createConstant(constant, type, (function, b) { + ClassInfo info = translator.classInfo[translator.functionClass]!; + translator.functions.allocateClass(info.classId); + + b.i32_const(info.classId); + b.i32_const(initialIdentityHash); + b.global_get(translator.globals.dummyGlobal); // Dummy context + if (lazyConstants) { + w.DefinedGlobal global = translator.makeFunctionRef(closureFunction); + b.global_get(global); + } else { + b.ref_func(closureFunction); + } + translator.struct_new(b, parameterCount); + }); + } + + @override + ConstantInfo? visitTypeLiteralConstant(TypeLiteralConstant constant) { + DartType cType = constant.type; + assert(cType is! TypeParameterType); + DartType type = cType is DynamicType || + cType is VoidType || + cType is NeverType || + cType is NullType + ? translator.coreTypes.objectRawType(Nullability.nullable) + : cType is FunctionType + ? InterfaceType(translator.functionClass, cType.declaredNullability) + : cType; + if (type is! InterfaceType) throw "Not implemented: $constant"; + + ListConstant typeArgs = ListConstant( + InterfaceType(translator.typeClass, Nullability.nonNullable), + type.typeArguments.map((t) => TypeLiteralConstant(t)).toList()); + ensureConstant(typeArgs); + + ClassInfo info = constants.typeInfo; + translator.functions.allocateClass(info.classId); + return createConstant(constant, info.nonNullableType, (function, b) { + ClassInfo typeInfo = translator.classInfo[type.classNode]!; + w.ValueType typeListExpectedType = info.struct.fields[3].type.unpacked; + + b.i32_const(info.classId); + b.i32_const(initialIdentityHash); + b.i64_const(typeInfo.classId); + constants.instantiateConstant( + function, b, typeArgs, typeListExpectedType); + translator.struct_new(b, info); + }); + } +} diff --git a/pkg/dart2wasm/lib/constants_backend.dart b/pkg/dart2wasm/lib/constants_backend.dart new file mode 100644 index 00000000000..68715996b00 --- /dev/null +++ b/pkg/dart2wasm/lib/constants_backend.dart @@ -0,0 +1,110 @@ +// 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 'package:kernel/ast.dart'; +import 'package:kernel/target/targets.dart'; +import 'package:kernel/core_types.dart'; + +class WasmConstantsBackend extends ConstantsBackend { + final Class immutableMapClass; + final Class unmodifiableSetClass; + final Field unmodifiableSetMap; + + WasmConstantsBackend._(this.immutableMapClass, this.unmodifiableSetMap, + this.unmodifiableSetClass); + + factory WasmConstantsBackend(CoreTypes coreTypes) { + final Library coreLibrary = coreTypes.coreLibrary; + final Class immutableMapClass = coreLibrary.classes + .firstWhere((Class klass) => klass.name == '_ImmutableMap'); + Field unmodifiableSetMap = + coreTypes.index.getField('dart:collection', '_UnmodifiableSet', '_map'); + + return new WasmConstantsBackend._(immutableMapClass, unmodifiableSetMap, + unmodifiableSetMap.enclosingClass!); + } + + @override + Constant lowerMapConstant(MapConstant constant) { + // The _ImmutableMap class is implemented via one field pointing to a list + // of key/value pairs -- see runtime/lib/immutable_map.dart! + final List kvListPairs = + new List.generate(2 * constant.entries.length, (int i) { + final int index = i ~/ 2; + final ConstantMapEntry entry = constant.entries[index]; + return i % 2 == 0 ? entry.key : entry.value; + }); + // This is a bit fishy, since we merge the key and the value type by + // putting both into the same list. + final ListConstant kvListConstant = + new ListConstant(const DynamicType(), kvListPairs); + assert(immutableMapClass.fields.length == 1); + final Field kvPairListField = immutableMapClass.fields[0]; + return new InstanceConstant(immutableMapClass.reference, [ + constant.keyType, + constant.valueType, + ], { + // We use getterReference as we refer to the field itself. + kvPairListField.getterReference: kvListConstant, + }); + } + + @override + bool isLoweredMapConstant(Constant constant) { + return constant is InstanceConstant && + constant.classNode == immutableMapClass; + } + + @override + void forEachLoweredMapConstantEntry( + Constant constant, void Function(Constant key, Constant value) f) { + assert(isLoweredMapConstant(constant)); + final InstanceConstant instance = constant as InstanceConstant; + assert(immutableMapClass.fields.length == 1); + final Field kvPairListField = immutableMapClass.fields[0]; + final ListConstant kvListConstant = + instance.fieldValues[kvPairListField.getterReference] as ListConstant; + assert(kvListConstant.entries.length % 2 == 0); + for (int index = 0; index < kvListConstant.entries.length; index += 2) { + f(kvListConstant.entries[index], kvListConstant.entries[index + 1]); + } + } + + @override + Constant lowerSetConstant(SetConstant constant) { + final DartType elementType = constant.typeArgument; + final List entries = constant.entries; + final List mapEntries = + new List.generate(entries.length, (int index) { + return new ConstantMapEntry(entries[index], new NullConstant()); + }); + Constant map = lowerMapConstant( + new MapConstant(elementType, const NullType(), mapEntries)); + return new InstanceConstant(unmodifiableSetClass.reference, [elementType], + {unmodifiableSetMap.getterReference: map}); + } + + @override + bool isLoweredSetConstant(Constant constant) { + if (constant is InstanceConstant && + constant.classNode == unmodifiableSetClass) { + InstanceConstant instance = constant; + return isLoweredMapConstant( + instance.fieldValues[unmodifiableSetMap.getterReference]!); + } + return false; + } + + @override + void forEachLoweredSetConstantElement( + Constant constant, void Function(Constant element) f) { + assert(isLoweredSetConstant(constant)); + final InstanceConstant instance = constant as InstanceConstant; + final Constant mapConstant = + instance.fieldValues[unmodifiableSetMap.getterReference]!; + forEachLoweredMapConstantEntry(mapConstant, (Constant key, Constant value) { + f(key); + }); + } +} diff --git a/pkg/dart2wasm/lib/dispatch_table.dart b/pkg/dart2wasm/lib/dispatch_table.dart new file mode 100644 index 00000000000..3c6b02475ea --- /dev/null +++ b/pkg/dart2wasm/lib/dispatch_table.dart @@ -0,0 +1,308 @@ +// 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:math'; + +import 'package:dart2wasm/class_info.dart'; +import 'package:dart2wasm/param_info.dart'; +import 'package:dart2wasm/reference_extensions.dart'; +import 'package:dart2wasm/translator.dart'; + +import 'package:kernel/ast.dart'; + +import 'package:vm/metadata/procedure_attributes.dart'; +import 'package:vm/metadata/table_selector.dart'; + +import 'package:wasm_builder/wasm_builder.dart' as w; + +/// Information for a dispatch table selector. +class SelectorInfo { + final Translator translator; + + final int id; + final int callCount; + final bool tornOff; + final ParameterInfo paramInfo; + int returnCount; + + final Map targets = {}; + late final w.FunctionType signature = computeSignature(); + + late final List classIds; + late final int targetCount; + bool forced = false; + Reference? singularTarget; + int? offset; + + String get name => paramInfo.member.name.text; + + bool get alive => callCount > 0 && targetCount > 1 || forced; + + int get sortWeight => classIds.length * 10 + callCount; + + SelectorInfo(this.translator, this.id, this.callCount, this.tornOff, + this.paramInfo, this.returnCount); + + /// Compute the signature for the functions implementing members targeted by + /// this selector. + /// + /// When the selector has multiple targets, the type of each parameter/return + /// is the upper bound across all targets, such that all targets have the + /// same signature, and the actual representation types of the parameters and + /// returns are subtypes (resp. supertypes) of the types in the signature. + w.FunctionType computeSignature() { + var nameIndex = paramInfo.nameIndex; + List> inputSets = + List.generate(1 + paramInfo.paramCount, (_) => {}); + List> outputSets = List.generate(returnCount, (_) => {}); + List inputNullable = List.filled(1 + paramInfo.paramCount, false); + List outputNullable = List.filled(returnCount, false); + targets.forEach((id, target) { + ClassInfo receiver = translator.classes[id]; + List positional; + Map named; + List returns; + Member member = target.asMember; + if (member is Field) { + if (target.isImplicitGetter) { + positional = const []; + named = const {}; + returns = [member.getterType]; + } else { + positional = [member.setterType]; + named = const {}; + returns = const []; + } + } else { + FunctionNode function = member.function!; + if (target.isTearOffReference) { + positional = const []; + named = const {}; + returns = [function.computeFunctionType(Nullability.nonNullable)]; + } else { + positional = [ + for (VariableDeclaration param in function.positionalParameters) + param.type + ]; + named = { + for (VariableDeclaration param in function.namedParameters) + param.name!: param.type + }; + returns = function.returnType is VoidType + ? const [] + : [function.returnType]; + } + } + assert(returns.length <= outputSets.length); + inputSets[0].add(receiver); + for (int i = 0; i < positional.length; i++) { + DartType type = positional[i]; + inputSets[1 + i] + .add(translator.classInfo[translator.classForType(type)]!); + inputNullable[1 + i] |= type.isPotentiallyNullable; + } + for (String name in named.keys) { + int i = nameIndex[name]!; + DartType type = named[name]!; + inputSets[1 + i] + .add(translator.classInfo[translator.classForType(type)]!); + inputNullable[1 + i] |= type.isPotentiallyNullable; + } + for (int i = 0; i < returnCount; i++) { + if (i < returns.length) { + outputSets[i] + .add(translator.classInfo[translator.classForType(returns[i])]!); + outputNullable[i] |= returns[i].isPotentiallyNullable; + } else { + outputNullable[i] = true; + } + } + }); + + List typeParameters = List.filled(paramInfo.typeParamCount, + translator.classInfo[translator.typeClass]!.nullableType); + List inputs = List.generate( + inputSets.length, + (i) => translator.typeForInfo( + upperBound(inputSets[i]), inputNullable[i]) as w.ValueType); + inputs[0] = translator.ensureBoxed(inputs[0]); + if (name == '==') { + // == can't be called with null + inputs[1] = inputs[1].withNullability(false); + } + List outputs = List.generate( + outputSets.length, + (i) => translator.typeForInfo( + upperBound(outputSets[i]), outputNullable[i]) as w.ValueType); + return translator.functionType( + [inputs[0], ...typeParameters, ...inputs.sublist(1)], outputs); + } +} + +// Build dispatch table for member calls. +class DispatchTable { + final Translator translator; + final List selectorMetadata; + final Map procedureAttributeMetadata; + + final Map selectorInfo = {}; + final Map dynamicGets = {}; + late final List table; + + DispatchTable(this.translator) + : selectorMetadata = + (translator.component.metadata["vm.table-selector.metadata"] + as TableSelectorMetadataRepository) + .mapping[translator.component]! + .selectors, + procedureAttributeMetadata = + (translator.component.metadata["vm.procedure-attributes.metadata"] + as ProcedureAttributesMetadataRepository) + .mapping; + + SelectorInfo selectorForTarget(Reference target) { + Member member = target.asMember; + bool isGetter = target.isGetter || target.isTearOffReference; + ProcedureAttributesMetadata metadata = procedureAttributeMetadata[member]!; + int selectorId = isGetter + ? metadata.getterSelectorId + : metadata.methodOrSetterSelectorId; + ParameterInfo paramInfo = ParameterInfo.fromMember(target); + int returnCount = isGetter || + member is Procedure && member.function.returnType is! VoidType + ? 1 + : 0; + bool calledDynamically = isGetter && metadata.getterCalledDynamically; + if (calledDynamically) { + // Merge all same-named getter selectors that are called dynamically. + selectorId = dynamicGets.putIfAbsent(member.name.text, () => selectorId); + } + var selector = selectorInfo.putIfAbsent( + selectorId, + () => SelectorInfo( + translator, + selectorId, + selectorMetadata[selectorId].callCount, + selectorMetadata[selectorId].tornOff, + paramInfo, + returnCount) + ..forced = calledDynamically); + selector.paramInfo.merge(paramInfo); + selector.returnCount = max(selector.returnCount, returnCount); + return selector; + } + + SelectorInfo selectorForDynamicName(String name) { + return selectorInfo[dynamicGets[name]!]!; + } + + void build() { + // Collect class/selector combinations + List> selectorsInClass = []; + for (ClassInfo info in translator.classes) { + List selectorIds = []; + ClassInfo? superInfo = info.superInfo; + if (superInfo != null) { + int superId = superInfo.classId; + selectorIds = List.of(selectorsInClass[superId]); + for (int selectorId in selectorIds) { + SelectorInfo selector = selectorInfo[selectorId]!; + selector.targets[info.classId] = selector.targets[superId]!; + } + } + + SelectorInfo addMember(Reference reference) { + SelectorInfo selector = selectorForTarget(reference); + if (reference.asMember.isAbstract) { + selector.targets[info.classId] ??= reference; + } else { + selector.targets[info.classId] = reference; + } + selectorIds.add(selector.id); + return selector; + } + + for (Member member + in info.cls?.members ?? translator.coreTypes.objectClass.members) { + if (member.isInstanceMember) { + if (member is Field) { + addMember(member.getterReference); + if (member.hasSetter) addMember(member.setterReference!); + } else if (member is Procedure) { + SelectorInfo method = addMember(member.reference); + if (method.tornOff) { + addMember(member.tearOffReference); + } + } + } + } + selectorsInClass.add(selectorIds); + } + + // Build lists of class IDs and count targets + for (SelectorInfo selector in selectorInfo.values) { + selector.classIds = selector.targets.keys + .where((id) => !(translator.classes[id].cls?.isAbstract ?? true)) + .toList() + ..sort(); + Set targets = + selector.targets.values.where((t) => !t.asMember.isAbstract).toSet(); + selector.targetCount = targets.length; + if (targets.length == 1) selector.singularTarget = targets.single; + } + + // Assign selector offsets + List selectors = selectorInfo.values + .where((s) => s.alive) + .toList() + ..sort((a, b) => b.sortWeight - a.sortWeight); + int firstAvailable = 0; + table = []; + bool first = true; + for (SelectorInfo selector in selectors) { + int offset = first ? 0 : firstAvailable - selector.classIds.first; + first = false; + bool fits; + do { + fits = true; + for (int classId in selector.classIds) { + int entry = offset + classId; + if (entry >= table.length) { + // Fits + break; + } + if (table[entry] != null) { + fits = false; + break; + } + } + if (!fits) offset++; + } while (!fits); + selector.offset = offset; + for (int classId in selector.classIds) { + int entry = offset + classId; + while (table.length <= entry) table.add(null); + assert(table[entry] == null); + table[entry] = selector.targets[classId]; + } + while (firstAvailable < table.length && table[firstAvailable] != null) { + firstAvailable++; + } + } + } + + void output() { + w.Module m = translator.m; + w.Table wasmTable = m.addTable(table.length); + for (int i = 0; i < table.length; i++) { + Reference? target = table[i]; + if (target != null) { + w.BaseFunction? fun = translator.functions.getExistingFunction(target); + if (fun != null) { + wasmTable.setElement(i, fun); + } + } + } + } +} diff --git a/pkg/dart2wasm/lib/functions.dart b/pkg/dart2wasm/lib/functions.dart new file mode 100644 index 00000000000..eaee6d18425 --- /dev/null +++ b/pkg/dart2wasm/lib/functions.dart @@ -0,0 +1,216 @@ +// 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 'package:dart2wasm/dispatch_table.dart'; +import 'package:dart2wasm/reference_extensions.dart'; +import 'package:dart2wasm/translator.dart'; + +import 'package:kernel/ast.dart'; + +import 'package:wasm_builder/wasm_builder.dart' as w; + +/// This class is responsible for collecting import and export annotations. +/// It also creates Wasm functions for Dart members and manages the worklist +/// used to achieve tree shaking. +class FunctionCollector extends MemberVisitor1 { + final Translator translator; + + // Wasm function for each Dart function + final Map _functions = {}; + // Names of exported functions + final Map exports = {}; + // Functions for which code has not yet been generated + final List worklist = []; + // Class IDs for classes that are allocated somewhere in the program + final Set _allocatedClasses = {}; + // For each class ID, which functions should be added to the worklist if an + // allocation of that class is encountered + final Map> _pendingAllocation = {}; + + FunctionCollector(this.translator); + + w.Module get m => translator.m; + + void collectImportsAndExports() { + for (Library library in translator.libraries) { + for (Procedure procedure in library.procedures) { + _importOrExport(procedure); + } + for (Class cls in library.classes) { + for (Procedure procedure in cls.procedures) { + _importOrExport(procedure); + } + } + } + } + + void _importOrExport(Procedure procedure) { + String? importName = translator.getPragma(procedure, "wasm:import"); + if (importName != null) { + int dot = importName.indexOf('.'); + if (dot != -1) { + assert(!procedure.isInstanceMember); + String module = importName.substring(0, dot); + String name = importName.substring(dot + 1); + w.FunctionType ftype = _makeFunctionType( + procedure.reference, procedure.function.returnType, null, + isImportOrExport: true); + _functions[procedure.reference] = + m.importFunction(module, name, ftype, "$importName (import)"); + } + } + String? exportName = + translator.getPragma(procedure, "wasm:export", procedure.name.text); + if (exportName != null) { + addExport(procedure.reference, exportName); + } + } + + void addExport(Reference target, String exportName) { + exports[target] = exportName; + } + + void initialize() { + // Add all exports to the worklist + for (Reference target in exports.keys) { + worklist.add(target); + Procedure node = target.asProcedure; + assert(!node.isInstanceMember); + assert(!node.isGetter); + w.FunctionType ftype = _makeFunctionType( + target, node.function.returnType, null, + isImportOrExport: true); + _functions[target] = m.addFunction(ftype, "$node"); + } + + // Value classes are always implicitly allocated. + allocateClass(translator.classInfo[translator.boxedBoolClass]!.classId); + allocateClass(translator.classInfo[translator.boxedIntClass]!.classId); + allocateClass(translator.classInfo[translator.boxedDoubleClass]!.classId); + } + + w.BaseFunction? getExistingFunction(Reference target) { + return _functions[target]; + } + + w.BaseFunction getFunction(Reference target) { + return _functions.putIfAbsent(target, () { + worklist.add(target); + w.FunctionType ftype = target.isTearOffReference + ? translator.dispatchTable.selectorForTarget(target).signature + : target.asMember.accept1(this, target); + return m.addFunction(ftype, "${target.asMember}"); + }); + } + + void activateSelector(SelectorInfo selector) { + selector.targets.forEach((classId, target) { + if (!target.asMember.isAbstract) { + if (_allocatedClasses.contains(classId)) { + // Class declaring or inheriting member is allocated somewhere. + getFunction(target); + } else { + // Remember the member in case an allocation is encountered later. + _pendingAllocation.putIfAbsent(classId, () => []).add(target); + } + } + }); + } + + void allocateClass(int classId) { + if (_allocatedClasses.add(classId)) { + // Schedule all members that were pending allocation of this class. + for (Reference target in _pendingAllocation[classId] ?? const []) { + getFunction(target); + } + } + } + + @override + w.FunctionType defaultMember(Member node, Reference target) { + throw "No Wasm function for member: $node"; + } + + @override + w.FunctionType visitField(Field node, Reference target) { + if (!node.isInstanceMember) { + if (target == node.fieldReference) { + // Static field initializer function + return _makeFunctionType(target, node.type, null); + } + String kind = target == node.setterReference ? "setter" : "getter"; + throw "No implicit $kind function for static field: $node"; + } + return translator.dispatchTable.selectorForTarget(target).signature; + } + + @override + w.FunctionType visitProcedure(Procedure node, Reference target) { + assert(!node.isAbstract); + return node.isInstanceMember + ? translator.dispatchTable.selectorForTarget(node.reference).signature + : _makeFunctionType(target, node.function.returnType, null); + } + + @override + w.FunctionType visitConstructor(Constructor node, Reference target) { + return _makeFunctionType(target, VoidType(), + translator.classInfo[node.enclosingClass]!.nonNullableType); + } + + w.FunctionType _makeFunctionType( + Reference target, DartType returnType, w.ValueType? receiverType, + {bool isImportOrExport = false}) { + Member member = target.asMember; + int typeParamCount = 0; + Iterable params; + if (member is Field) { + params = [if (target.isImplicitSetter) member.setterType]; + } else { + FunctionNode function = member.function!; + typeParamCount = (member is Constructor + ? member.enclosingClass.typeParameters + : function.typeParameters) + .length; + List names = [for (var p in function.namedParameters) p.name!] + ..sort(); + Map nameTypes = { + for (var p in function.namedParameters) p.name!: p.type + }; + params = [ + for (var p in function.positionalParameters) p.type, + for (String name in names) nameTypes[name]! + ]; + function.positionalParameters.map((p) => p.type); + } + + List typeParameters = List.filled(typeParamCount, + translator.classInfo[translator.typeClass]!.nullableType); + + // The JS embedder will not accept Wasm struct types as parameter or return + // types for functions called from JS. We need to use eqref instead. + w.ValueType adjustExternalType(w.ValueType type) { + if (isImportOrExport && type.isSubtypeOf(w.RefType.eq())) { + return w.RefType.eq(); + } + return type; + } + + List inputs = []; + if (receiverType != null) { + inputs.add(adjustExternalType(receiverType)); + } + inputs.addAll(typeParameters.map(adjustExternalType)); + inputs.addAll( + params.map((t) => adjustExternalType(translator.translateType(t)))); + + List outputs = returnType is VoidType || + returnType is NeverType || + returnType is NullType + ? const [] + : [adjustExternalType(translator.translateType(returnType))]; + + return translator.functionType(inputs, outputs); + } +} diff --git a/pkg/dart2wasm/lib/globals.dart b/pkg/dart2wasm/lib/globals.dart new file mode 100644 index 00000000000..c72aeb1f675 --- /dev/null +++ b/pkg/dart2wasm/lib/globals.dart @@ -0,0 +1,198 @@ +// 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 'package:kernel/ast.dart'; + +import 'package:wasm_builder/wasm_builder.dart' as w; + +import 'package:dart2wasm/translator.dart'; + +/// Handles lazy initialization of static fields. +class Globals { + final Translator translator; + + final Map globals = {}; + final Map globalInitializers = {}; + final Map globalInitializedFlag = {}; + final Map dummyValues = {}; + late final w.DefinedGlobal dummyGlobal; + + Globals(this.translator) { + _initDummyValues(); + } + + void _initDummyValues() { + // Create dummy struct for anyref/eqref/dataref/context dummy values + w.StructType structType = translator.structType("#Dummy"); + w.RefType type = w.RefType.def(structType, nullable: false); + dummyGlobal = translator.m.addGlobal(w.GlobalType(type, mutable: false)); + w.Instructions ib = dummyGlobal.initializer; + translator.struct_new(ib, structType); + ib.end(); + dummyValues[w.HeapType.any] = dummyGlobal; + dummyValues[w.HeapType.eq] = dummyGlobal; + dummyValues[w.HeapType.data] = dummyGlobal; + } + + w.Global? prepareDummyValue(w.ValueType type) { + if (type is w.RefType && !type.nullable) { + w.HeapType heapType = type.heapType; + w.DefinedGlobal? global = dummyValues[heapType]; + if (global != null) return global; + if (heapType is w.DefType) { + if (heapType is w.StructType) { + for (w.FieldType field in heapType.fields) { + prepareDummyValue(field.type.unpacked); + } + global = translator.m.addGlobal(w.GlobalType(type, mutable: false)); + w.Instructions ib = global.initializer; + for (w.FieldType field in heapType.fields) { + instantiateDummyValue(ib, field.type.unpacked); + } + translator.struct_new(ib, heapType); + ib.end(); + } else if (heapType is w.ArrayType) { + global = translator.m.addGlobal(w.GlobalType(type, mutable: false)); + w.Instructions ib = global.initializer; + translator.array_init(ib, heapType, 0); + ib.end(); + } else if (heapType is w.FunctionType) { + w.DefinedFunction function = + translator.m.addFunction(heapType, "#dummy function $heapType"); + w.Instructions b = function.body; + b.unreachable(); + b.end(); + global = translator.m.addGlobal(w.GlobalType(type, mutable: false)); + w.Instructions ib = global.initializer; + ib.ref_func(function); + ib.end(); + } + dummyValues[heapType] = global!; + } + return global; + } + + return null; + } + + void instantiateDummyValue(w.Instructions b, w.ValueType type) { + w.Global? global = prepareDummyValue(type); + switch (type) { + case w.NumType.i32: + b.i32_const(0); + break; + case w.NumType.i64: + b.i64_const(0); + break; + case w.NumType.f32: + b.f32_const(0); + break; + case w.NumType.f64: + b.f64_const(0); + break; + default: + if (type is w.RefType) { + w.HeapType heapType = type.heapType; + if (type.nullable) { + b.ref_null(heapType); + } else { + b.global_get(global!); + } + } else { + throw "Unsupported global type ${type} ($type)"; + } + break; + } + } + + Constant? _getConstantInitializer(Field variable) { + Expression? init = variable.initializer; + if (init == null || init is NullLiteral) return NullConstant(); + if (init is IntLiteral) return IntConstant(init.value); + if (init is DoubleLiteral) return DoubleConstant(init.value); + if (init is BoolLiteral) return BoolConstant(init.value); + if (translator.options.lazyConstants) return null; + if (init is StringLiteral) return StringConstant(init.value); + if (init is ConstantExpression) return init.constant; + return null; + } + + /// Return (and if needed create) the Wasm global corresponding to a static + /// field. + w.Global getGlobal(Field variable) { + assert(!variable.isLate); + return globals.putIfAbsent(variable, () { + w.ValueType type = translator.translateType(variable.type); + Constant? init = _getConstantInitializer(variable); + if (init != null) { + // Initialized to a constant + translator.constants.ensureConstant(init); + w.DefinedGlobal global = translator.m + .addGlobal(w.GlobalType(type, mutable: !variable.isFinal)); + translator.constants + .instantiateConstant(null, global.initializer, init, type); + global.initializer.end(); + return global; + } else { + if (type is w.RefType && !type.nullable) { + // Null signals uninitialized + type = type.withNullability(true); + } else { + // Explicit initialization flag + w.DefinedGlobal flag = + translator.m.addGlobal(w.GlobalType(w.NumType.i32)); + flag.initializer.i32_const(0); + flag.initializer.end(); + globalInitializedFlag[variable] = flag; + } + + w.DefinedGlobal global = translator.m.addGlobal(w.GlobalType(type)); + instantiateDummyValue(global.initializer, type); + global.initializer.end(); + + globalInitializers[variable] = + translator.functions.getFunction(variable.fieldReference); + return global; + } + }); + } + + /// Return the Wasm global containing the flag indicating whether this static + /// field has been initialized, if such a flag global is needed. + /// + /// Note that [getGlobal] must have been called for the field beforehand. + w.Global? getGlobalInitializedFlag(Field variable) { + return globalInitializedFlag[variable]; + } + + /// Emit code to read a static field. + w.ValueType readGlobal(w.Instructions b, Field variable) { + w.Global global = getGlobal(variable); + w.BaseFunction? initFunction = globalInitializers[variable]; + if (initFunction == null) { + // Statically initialized + b.global_get(global); + return global.type.type; + } + w.Global? flag = globalInitializedFlag[variable]; + if (flag != null) { + // Explicit initialization flag + assert(global.type.type == initFunction.type.outputs.single); + b.global_get(flag); + b.if_(const [], [global.type.type]); + b.global_get(global); + b.else_(); + b.call(initFunction); + b.end(); + } else { + // Null signals uninitialized + w.Label block = b.block(const [], [initFunction.type.outputs.single]); + b.global_get(global); + b.br_on_non_null(block); + b.call(initFunction); + b.end(); + } + return initFunction.type.outputs.single; + } +} diff --git a/pkg/dart2wasm/lib/intrinsics.dart b/pkg/dart2wasm/lib/intrinsics.dart new file mode 100644 index 00000000000..d94cd8ca719 --- /dev/null +++ b/pkg/dart2wasm/lib/intrinsics.dart @@ -0,0 +1,931 @@ +// 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 'package:dart2wasm/class_info.dart'; +import 'package:dart2wasm/code_generator.dart'; +import 'package:dart2wasm/translator.dart'; + +import 'package:kernel/ast.dart'; + +import 'package:wasm_builder/wasm_builder.dart' as w; + +/// Specialized code generation for external members. +/// +/// The code is generated either inlined at the call site, or as the body of the +/// member in [generateMemberIntrinsic]. +class Intrinsifier { + final CodeGenerator codeGen; + static const w.ValueType boolType = w.NumType.i32; + static const w.ValueType intType = w.NumType.i64; + static const w.ValueType doubleType = w.NumType.f64; + + static final Map>> + binaryOperatorMap = { + intType: { + intType: { + '+': (b) => b.i64_add(), + '-': (b) => b.i64_sub(), + '*': (b) => b.i64_mul(), + '~/': (b) => b.i64_div_s(), + '&': (b) => b.i64_and(), + '|': (b) => b.i64_or(), + '^': (b) => b.i64_xor(), + '<<': (b) => b.i64_shl(), + '>>': (b) => b.i64_shr_s(), + '>>>': (b) => b.i64_shr_u(), + '<': (b) => b.i64_lt_s(), + '<=': (b) => b.i64_le_s(), + '>': (b) => b.i64_gt_s(), + '>=': (b) => b.i64_ge_s(), + } + }, + doubleType: { + doubleType: { + '+': (b) => b.f64_add(), + '-': (b) => b.f64_sub(), + '*': (b) => b.f64_mul(), + '/': (b) => b.f64_div(), + '<': (b) => b.f64_lt(), + '<=': (b) => b.f64_le(), + '>': (b) => b.f64_gt(), + '>=': (b) => b.f64_ge(), + } + }, + }; + static final Map> unaryOperatorMap = + { + intType: { + 'unary-': (b) { + b.i64_const(-1); + b.i64_mul(); + }, + '~': (b) { + b.i64_const(-1); + b.i64_xor(); + }, + 'toDouble': (b) { + b.f64_convert_i64_s(); + }, + }, + doubleType: { + 'unary-': (b) { + b.f64_neg(); + }, + 'toInt': (b) { + b.i64_trunc_sat_f64_s(); + }, + 'roundToDouble': (b) { + b.f64_nearest(); + }, + 'floorToDouble': (b) { + b.f64_floor(); + }, + 'ceilToDouble': (b) { + b.f64_ceil(); + }, + 'truncateToDouble': (b) { + b.f64_trunc(); + }, + }, + }; + static final Map unaryResultMap = { + 'toDouble': w.NumType.f64, + 'toInt': w.NumType.i64, + 'roundToDouble': w.NumType.f64, + 'floorToDouble': w.NumType.f64, + 'ceilToDouble': w.NumType.f64, + 'truncateToDouble': w.NumType.f64, + }; + + Translator get translator => codeGen.translator; + w.Instructions get b => codeGen.b; + + DartType dartTypeOf(Expression exp) => codeGen.dartTypeOf(exp); + + w.ValueType typeOfExp(Expression exp) { + return translator.translateType(dartTypeOf(exp)); + } + + static bool isComparison(String op) => + op == '<' || op == '<=' || op == '>' || op == '>='; + + Intrinsifier(this.codeGen); + + w.ValueType? generateInstanceGetterIntrinsic(InstanceGet node) { + DartType receiverType = dartTypeOf(node.receiver); + String name = node.name.text; + + // _WasmArray.length + if (node.interfaceTarget.enclosingClass == translator.wasmArrayBaseClass) { + assert(name == 'length'); + DartType elementType = + (receiverType as InterfaceType).typeArguments.single; + w.ArrayType arrayType = translator.arrayTypeForDartType(elementType); + Expression array = node.receiver; + codeGen.wrap(array, w.RefType.def(arrayType, nullable: true)); + b.array_len(arrayType); + b.i64_extend_i32_u(); + return w.NumType.i64; + } + + // int.bitlength + if (node.interfaceTarget.enclosingClass == translator.coreTypes.intClass && + name == 'bitLength') { + w.Local temp = codeGen.function.addLocal(w.NumType.i64); + b.i64_const(64); + codeGen.wrap(node.receiver, w.NumType.i64); + b.local_tee(temp); + b.local_get(temp); + b.i64_const(63); + b.i64_shr_s(); + b.i64_xor(); + b.i64_clz(); + b.i64_sub(); + return w.NumType.i64; + } + + return null; + } + + w.ValueType? generateInstanceIntrinsic(InstanceInvocation node) { + Expression receiver = node.receiver; + DartType receiverType = dartTypeOf(receiver); + String name = node.name.text; + Procedure target = node.interfaceTarget; + + // _TypedListBase._setRange + if (target.enclosingClass == translator.typedListBaseClass && + name == "_setRange") { + // Always fall back to alternative implementation. + b.i32_const(0); + return w.NumType.i32; + } + + // _TypedList._(get|set)(Int|Uint|Float)(8|16|32|64) + if (node.interfaceTarget.enclosingClass == translator.typedListClass) { + Match? match = RegExp("^_(get|set)(Int|Uint|Float)(8|16|32|64)\$") + .matchAsPrefix(name); + if (match != null) { + bool setter = match.group(1) == "set"; + bool signed = match.group(2) == "Int"; + bool float = match.group(2) == "Float"; + int bytes = int.parse(match.group(3)!) ~/ 8; + bool wide = bytes == 8; + + ClassInfo typedListInfo = + translator.classInfo[translator.typedListClass]!; + w.RefType arrayType = typedListInfo.struct + .fields[FieldIndex.typedListArray].type.unpacked as w.RefType; + w.ArrayType arrayHeapType = arrayType.heapType as w.ArrayType; + w.ValueType valueType = float ? w.NumType.f64 : w.NumType.i64; + w.ValueType intType = wide ? w.NumType.i64 : w.NumType.i32; + + // Prepare array and offset + w.Local array = codeGen.addLocal(arrayType); + w.Local offset = codeGen.addLocal(w.NumType.i32); + codeGen.wrap(receiver, typedListInfo.nullableType); + b.struct_get(typedListInfo.struct, FieldIndex.typedListArray); + b.local_set(array); + codeGen.wrap(node.arguments.positional[0], w.NumType.i64); + b.i32_wrap_i64(); + b.local_set(offset); + + if (setter) { + // Setter + w.Local value = codeGen.addLocal(intType); + codeGen.wrap(node.arguments.positional[1], valueType); + if (wide) { + if (float) { + b.i64_reinterpret_f64(); + } + } else { + if (float) { + b.f32_demote_f64(); + b.i32_reinterpret_f32(); + } else { + b.i32_wrap_i64(); + } + } + b.local_set(value); + + for (int i = 0; i < bytes; i++) { + b.local_get(array); + b.local_get(offset); + if (i > 0) { + b.i32_const(i); + b.i32_add(); + } + b.local_get(value); + if (i > 0) { + if (wide) { + b.i64_const(i * 8); + b.i64_shr_u(); + } else { + b.i32_const(i * 8); + b.i32_shr_u(); + } + } + if (wide) { + b.i32_wrap_i64(); + } + b.array_set(arrayHeapType); + } + return translator.voidMarker; + } else { + // Getter + for (int i = 0; i < bytes; i++) { + b.local_get(array); + b.local_get(offset); + if (i > 0) { + b.i32_const(i); + b.i32_add(); + } + if (signed && i == bytes - 1) { + b.array_get_s(arrayHeapType); + } else { + b.array_get_u(arrayHeapType); + } + if (wide) { + if (signed) { + b.i64_extend_i32_s(); + } else { + b.i64_extend_i32_u(); + } + } + if (i > 0) { + if (wide) { + b.i64_const(i * 8); + b.i64_shl(); + b.i64_or(); + } else { + b.i32_const(i * 8); + b.i32_shl(); + b.i32_or(); + } + } + } + + if (wide) { + if (float) { + b.f64_reinterpret_i64(); + } + } else { + if (float) { + b.f32_reinterpret_i32(); + b.f64_promote_f32(); + } else { + if (signed) { + b.i64_extend_i32_s(); + } else { + b.i64_extend_i32_u(); + } + } + } + return valueType; + } + } + } + + // WasmIntArray.(readSigned|readUnsigned|write) + // WasmFloatArray.(read|write) + // WasmObjectArray.(read|write) + if (node.interfaceTarget.enclosingClass?.superclass == + translator.wasmArrayBaseClass) { + DartType elementType = + (receiverType as InterfaceType).typeArguments.single; + w.ArrayType arrayType = translator.arrayTypeForDartType(elementType); + w.StorageType wasmType = arrayType.elementType.type; + bool innerExtend = + wasmType == w.PackedType.i8 || wasmType == w.PackedType.i16; + bool outerExtend = + wasmType.unpacked == w.NumType.i32 || wasmType == w.NumType.f32; + switch (name) { + case 'read': + case 'readSigned': + case 'readUnsigned': + bool unsigned = name == 'readUnsigned'; + Expression array = receiver; + Expression index = node.arguments.positional.single; + codeGen.wrap(array, w.RefType.def(arrayType, nullable: true)); + codeGen.wrap(index, w.NumType.i64); + b.i32_wrap_i64(); + if (innerExtend) { + if (unsigned) { + b.array_get_u(arrayType); + } else { + b.array_get_s(arrayType); + } + } else { + b.array_get(arrayType); + } + if (outerExtend) { + if (wasmType == w.NumType.f32) { + b.f64_promote_f32(); + return w.NumType.f64; + } else { + if (unsigned) { + b.i64_extend_i32_u(); + } else { + b.i64_extend_i32_s(); + } + return w.NumType.i64; + } + } + return wasmType.unpacked; + case 'write': + Expression array = receiver; + Expression index = node.arguments.positional[0]; + Expression value = node.arguments.positional[1]; + codeGen.wrap(array, w.RefType.def(arrayType, nullable: true)); + codeGen.wrap(index, w.NumType.i64); + b.i32_wrap_i64(); + codeGen.wrap(value, typeOfExp(value)); + if (outerExtend) { + if (wasmType == w.NumType.f32) { + b.f32_demote_f64(); + } else { + b.i32_wrap_i64(); + } + } + b.array_set(arrayType); + return codeGen.voidMarker; + default: + throw "Unsupported array method: $name"; + } + } + + // List.[] on list constants + if (receiver is ConstantExpression && + receiver.constant is ListConstant && + name == '[]') { + ClassInfo info = translator.classInfo[translator.listBaseClass]!; + w.RefType listType = info.nullableType; + Field arrayField = translator.listBaseClass.fields + .firstWhere((f) => f.name.text == '_data'); + int arrayFieldIndex = translator.fieldIndex[arrayField]!; + w.ArrayType arrayType = + (info.struct.fields[arrayFieldIndex].type as w.RefType).heapType + as w.ArrayType; + codeGen.wrap(receiver, listType); + b.struct_get(info.struct, arrayFieldIndex); + codeGen.wrap(node.arguments.positional.single, w.NumType.i64); + b.i32_wrap_i64(); + b.array_get(arrayType); + return translator.topInfo.nullableType; + } + + if (node.arguments.positional.length == 1) { + // Binary operator + Expression left = node.receiver; + Expression right = node.arguments.positional.single; + DartType argType = dartTypeOf(right); + if (argType is VoidType) return null; + w.ValueType leftType = translator.translateType(receiverType); + w.ValueType rightType = translator.translateType(argType); + var code = binaryOperatorMap[leftType]?[rightType]?[name]; + if (code != null) { + w.ValueType outType = isComparison(name) ? w.NumType.i32 : leftType; + codeGen.wrap(left, leftType); + codeGen.wrap(right, rightType); + code(b); + return outType; + } + } else if (node.arguments.positional.length == 0) { + // Unary operator + Expression operand = node.receiver; + w.ValueType opType = translator.translateType(receiverType); + var code = unaryOperatorMap[opType]?[name]; + if (code != null) { + codeGen.wrap(operand, opType); + code(b); + return unaryResultMap[name] ?? opType; + } + } + + return null; + } + + w.ValueType? generateEqualsIntrinsic(EqualsCall node) { + w.ValueType leftType = typeOfExp(node.left); + w.ValueType rightType = typeOfExp(node.right); + + if (leftType == boolType && rightType == boolType) { + codeGen.wrap(node.left, w.NumType.i32); + codeGen.wrap(node.right, w.NumType.i32); + b.i32_eq(); + return w.NumType.i32; + } + + if (leftType == intType && rightType == intType) { + codeGen.wrap(node.left, w.NumType.i64); + codeGen.wrap(node.right, w.NumType.i64); + b.i64_eq(); + return w.NumType.i32; + } + + if (leftType == doubleType && rightType == doubleType) { + codeGen.wrap(node.left, w.NumType.f64); + codeGen.wrap(node.right, w.NumType.f64); + b.f64_eq(); + return w.NumType.i32; + } + + return null; + } + + w.ValueType? generateStaticGetterIntrinsic(StaticGet node) { + Member target = node.target; + + // ClassID getters + String? className = translator.getPragma(target, "wasm:class-id"); + if (className != null) { + List libAndClass = className.split("#"); + Class cls = translator.libraries + .firstWhere((l) => l.name == libAndClass[0]) + .classes + .firstWhere((c) => c.name == libAndClass[1]); + int classId = translator.classInfo[cls]!.classId; + b.i64_const(classId); + return w.NumType.i64; + } + + return null; + } + + w.ValueType? generateStaticIntrinsic(StaticInvocation node) { + String name = node.name.text; + + // dart:core static functions + if (node.target.enclosingLibrary == translator.coreTypes.coreLibrary) { + switch (name) { + case "identical": + Expression first = node.arguments.positional[0]; + Expression second = node.arguments.positional[1]; + DartType boolType = translator.coreTypes.boolNonNullableRawType; + InterfaceType intType = translator.coreTypes.intNonNullableRawType; + DartType doubleType = translator.coreTypes.doubleNonNullableRawType; + List types = [dartTypeOf(first), dartTypeOf(second)]; + if (types.every((t) => t == intType)) { + codeGen.wrap(first, w.NumType.i64); + codeGen.wrap(second, w.NumType.i64); + b.i64_eq(); + return w.NumType.i32; + } + if (types.any((t) => + t is InterfaceType && + t != boolType && + t != doubleType && + !translator.hierarchy + .isSubtypeOf(intType.classNode, t.classNode))) { + codeGen.wrap(first, w.RefType.eq(nullable: true)); + codeGen.wrap(second, w.RefType.eq(nullable: true)); + b.ref_eq(); + return w.NumType.i32; + } + break; + case "_getHash": + Expression arg = node.arguments.positional[0]; + w.ValueType objectType = translator.objectInfo.nullableType; + codeGen.wrap(arg, objectType); + b.struct_get(translator.objectInfo.struct, FieldIndex.identityHash); + b.i64_extend_i32_u(); + return w.NumType.i64; + case "_setHash": + Expression arg = node.arguments.positional[0]; + Expression hash = node.arguments.positional[1]; + w.ValueType objectType = translator.objectInfo.nullableType; + codeGen.wrap(arg, objectType); + codeGen.wrap(hash, w.NumType.i64); + b.i32_wrap_i64(); + b.struct_set(translator.objectInfo.struct, FieldIndex.identityHash); + return codeGen.voidMarker; + } + } + + // dart:_internal static functions + if (node.target.enclosingLibrary.name == "dart._internal") { + switch (name) { + case "unsafeCast": + w.ValueType targetType = + translator.translateType(node.arguments.types.single); + Expression operand = node.arguments.positional.single; + return codeGen.wrap(operand, targetType); + case "allocateOneByteString": + ClassInfo info = translator.classInfo[translator.oneByteStringClass]!; + translator.functions.allocateClass(info.classId); + w.ArrayType arrayType = + translator.wasmArrayType(w.PackedType.i8, "WasmI8"); + Expression length = node.arguments.positional[0]; + b.i32_const(info.classId); + b.i32_const(initialIdentityHash); + codeGen.wrap(length, w.NumType.i64); + b.i32_wrap_i64(); + translator.array_new_default(b, arrayType); + translator.struct_new(b, info); + return info.nonNullableType; + case "writeIntoOneByteString": + ClassInfo info = translator.classInfo[translator.oneByteStringClass]!; + w.ArrayType arrayType = + translator.wasmArrayType(w.PackedType.i8, "WasmI8"); + Field arrayField = translator.oneByteStringClass.fields + .firstWhere((f) => f.name.text == '_array'); + int arrayFieldIndex = translator.fieldIndex[arrayField]!; + Expression string = node.arguments.positional[0]; + Expression index = node.arguments.positional[1]; + Expression codePoint = node.arguments.positional[2]; + codeGen.wrap(string, info.nonNullableType); + b.struct_get(info.struct, arrayFieldIndex); + codeGen.wrap(index, w.NumType.i64); + b.i32_wrap_i64(); + codeGen.wrap(codePoint, w.NumType.i64); + b.i32_wrap_i64(); + b.array_set(arrayType); + return codeGen.voidMarker; + case "allocateTwoByteString": + ClassInfo info = translator.classInfo[translator.twoByteStringClass]!; + translator.functions.allocateClass(info.classId); + w.ArrayType arrayType = + translator.wasmArrayType(w.PackedType.i16, "WasmI16"); + Expression length = node.arguments.positional[0]; + b.i32_const(info.classId); + b.i32_const(initialIdentityHash); + codeGen.wrap(length, w.NumType.i64); + b.i32_wrap_i64(); + translator.array_new_default(b, arrayType); + translator.struct_new(b, info); + return info.nonNullableType; + case "writeIntoTwoByteString": + ClassInfo info = translator.classInfo[translator.twoByteStringClass]!; + w.ArrayType arrayType = + translator.wasmArrayType(w.PackedType.i16, "WasmI16"); + Field arrayField = translator.oneByteStringClass.fields + .firstWhere((f) => f.name.text == '_array'); + int arrayFieldIndex = translator.fieldIndex[arrayField]!; + Expression string = node.arguments.positional[0]; + Expression index = node.arguments.positional[1]; + Expression codePoint = node.arguments.positional[2]; + codeGen.wrap(string, info.nonNullableType); + b.struct_get(info.struct, arrayFieldIndex); + codeGen.wrap(index, w.NumType.i64); + b.i32_wrap_i64(); + codeGen.wrap(codePoint, w.NumType.i64); + b.i32_wrap_i64(); + b.array_set(arrayType); + return codeGen.voidMarker; + case "floatToIntBits": + codeGen.wrap(node.arguments.positional.single, w.NumType.f64); + b.f32_demote_f64(); + b.i32_reinterpret_f32(); + b.i64_extend_i32_u(); + return w.NumType.i64; + case "intBitsToFloat": + codeGen.wrap(node.arguments.positional.single, w.NumType.i64); + b.i32_wrap_i64(); + b.f32_reinterpret_i32(); + b.f64_promote_f32(); + return w.NumType.f64; + case "doubleToIntBits": + codeGen.wrap(node.arguments.positional.single, w.NumType.f64); + b.i64_reinterpret_f64(); + return w.NumType.i64; + case "intBitsToDouble": + codeGen.wrap(node.arguments.positional.single, w.NumType.i64); + b.f64_reinterpret_i64(); + return w.NumType.f64; + case "getID": + assert(node.target.enclosingClass?.name == "ClassID"); + ClassInfo info = translator.topInfo; + codeGen.wrap(node.arguments.positional.single, info.nullableType); + b.struct_get(info.struct, FieldIndex.classId); + b.i64_extend_i32_u(); + return w.NumType.i64; + } + } + + // Wasm(Int|Float|Object)Array constructors + if (node.target.enclosingClass?.superclass == + translator.wasmArrayBaseClass) { + Expression length = node.arguments.positional[0]; + w.ArrayType arrayType = + translator.arrayTypeForDartType(node.arguments.types.single); + codeGen.wrap(length, w.NumType.i64); + b.i32_wrap_i64(); + translator.array_new_default(b, arrayType); + return w.RefType.def(arrayType, nullable: false); + } + + return null; + } + + bool generateMemberIntrinsic(Reference target, w.DefinedFunction function, + List paramLocals, w.Label? returnLabel) { + Member member = target.asMember; + if (member is! Procedure) return false; + String name = member.name.text; + FunctionNode functionNode = member.function; + + // Object.== + if (member == translator.coreTypes.objectEquals) { + b.local_get(paramLocals[0]); + b.local_get(paramLocals[1]); + b.ref_eq(); + return true; + } + + // Object.runtimeType + if (member.enclosingClass == translator.coreTypes.objectClass && + name == "runtimeType") { + w.Local receiver = paramLocals[0]; + ClassInfo info = translator.classInfo[translator.typeClass]!; + translator.functions.allocateClass(info.classId); + w.ValueType typeListExpectedType = info.struct.fields[3].type.unpacked; + + b.i32_const(info.classId); + b.i32_const(initialIdentityHash); + b.local_get(receiver); + b.struct_get(translator.topInfo.struct, FieldIndex.classId); + b.i64_extend_i32_u(); + // TODO(askesc): Type arguments + b.global_get(translator.constants.emptyTypeList); + translator.convertType(function, + translator.constants.emptyTypeList.type.type, typeListExpectedType); + translator.struct_new(b, info); + + return true; + } + + // identical + if (member == translator.coreTypes.identicalProcedure) { + w.Local first = paramLocals[0]; + w.Local second = paramLocals[1]; + ClassInfo boolInfo = translator.classInfo[translator.boxedBoolClass]!; + ClassInfo intInfo = translator.classInfo[translator.boxedIntClass]!; + ClassInfo doubleInfo = translator.classInfo[translator.boxedDoubleClass]!; + w.Local cid = function.addLocal(w.NumType.i32); + w.Label ref_eq = b.block(); + b.local_get(first); + b.br_on_null(ref_eq); + b.struct_get(translator.topInfo.struct, FieldIndex.classId); + b.local_tee(cid); + + // Both bool? + b.i32_const(boolInfo.classId); + b.i32_eq(); + b.if_(); + b.local_get(first); + translator.ref_cast(b, boolInfo); + b.struct_get(boolInfo.struct, FieldIndex.boxValue); + w.Label bothBool = b.block(const [], [boolInfo.nullableType]); + b.local_get(second); + translator.br_on_cast(b, bothBool, boolInfo); + b.i32_const(0); + b.return_(); + b.end(); + b.struct_get(boolInfo.struct, FieldIndex.boxValue); + b.i32_eq(); + b.return_(); + b.end(); + + // Both int? + b.local_get(cid); + b.i32_const(intInfo.classId); + b.i32_eq(); + b.if_(); + b.local_get(first); + translator.ref_cast(b, intInfo); + b.struct_get(intInfo.struct, FieldIndex.boxValue); + w.Label bothInt = b.block(const [], [intInfo.nullableType]); + b.local_get(second); + translator.br_on_cast(b, bothInt, intInfo); + b.i32_const(0); + b.return_(); + b.end(); + b.struct_get(intInfo.struct, FieldIndex.boxValue); + b.i64_eq(); + b.return_(); + b.end(); + + // Both double? + b.local_get(cid); + b.i32_const(doubleInfo.classId); + b.i32_eq(); + b.if_(); + b.local_get(first); + translator.ref_cast(b, doubleInfo); + b.struct_get(doubleInfo.struct, FieldIndex.boxValue); + b.i64_reinterpret_f64(); + w.Label bothDouble = b.block(const [], [doubleInfo.nullableType]); + b.local_get(second); + translator.br_on_cast(b, bothDouble, doubleInfo); + b.i32_const(0); + b.return_(); + b.end(); + b.struct_get(doubleInfo.struct, FieldIndex.boxValue); + b.i64_reinterpret_f64(); + b.i64_eq(); + b.return_(); + b.end(); + + // Compare as references + b.end(); + b.local_get(first); + b.local_get(second); + b.ref_eq(); + + return true; + } + + // (Int|Uint|Float)(8|16|32|64)(Clamped)?(List|ArrayView) constructors + if (member.isExternal && + member.enclosingLibrary.name == "dart.typed_data") { + if (member.isFactory) { + String className = member.enclosingClass!.name; + + Match? match = RegExp("^(Int|Uint|Float)(8|16|32|64)(Clamped)?List\$") + .matchAsPrefix(className); + if (match != null) { + int shift = int.parse(match.group(2)!).bitLength - 4; + Class cls = member.enclosingLibrary.classes + .firstWhere((c) => c.name == "_$className"); + ClassInfo info = translator.classInfo[cls]!; + translator.functions.allocateClass(info.classId); + w.ArrayType arrayType = + translator.wasmArrayType(w.PackedType.i8, "i8"); + + w.Local length = paramLocals[0]; + b.i32_const(info.classId); + b.i32_const(initialIdentityHash); + b.local_get(length); + b.i32_wrap_i64(); + b.local_get(length); + if (shift > 0) { + b.i64_const(shift); + b.i64_shl(); + } + b.i32_wrap_i64(); + translator.array_new_default(b, arrayType); + translator.struct_new(b, info); + return true; + } + + match = RegExp("^_(Int|Uint|Float)(8|16|32|64)(Clamped)?ArrayView\$") + .matchAsPrefix(className); + if (match != null || + member.enclosingClass == translator.byteDataViewClass) { + ClassInfo info = translator.classInfo[member.enclosingClass]!; + translator.functions.allocateClass(info.classId); + + w.Local buffer = paramLocals[0]; + w.Local offsetInBytes = paramLocals[1]; + w.Local length = paramLocals[2]; + b.i32_const(info.classId); + b.i32_const(initialIdentityHash); + b.local_get(length); + b.i32_wrap_i64(); + b.local_get(buffer); + b.local_get(offsetInBytes); + b.i32_wrap_i64(); + translator.struct_new(b, info); + return true; + } + } + + // _TypedListBase.length + // _TypedListView.offsetInBytes + // _TypedListView._typedData + // _ByteDataView.length + // _ByteDataView.offsetInBytes + // _ByteDataView._typedData + if (member.isGetter) { + Class cls = member.enclosingClass!; + ClassInfo info = translator.classInfo[cls]!; + b.local_get(paramLocals[0]); + translator.ref_cast(b, info); + switch (name) { + case "length": + assert(cls == translator.typedListBaseClass || + cls == translator.byteDataViewClass); + if (cls == translator.typedListBaseClass) { + b.struct_get(info.struct, FieldIndex.typedListBaseLength); + } else { + b.struct_get(info.struct, FieldIndex.byteDataViewLength); + } + b.i64_extend_i32_u(); + return true; + case "offsetInBytes": + assert(cls == translator.typedListViewClass || + cls == translator.byteDataViewClass); + if (cls == translator.typedListViewClass) { + b.struct_get(info.struct, FieldIndex.typedListViewOffsetInBytes); + } else { + b.struct_get(info.struct, FieldIndex.byteDataViewOffsetInBytes); + } + b.i64_extend_i32_u(); + return true; + case "_typedData": + assert(cls == translator.typedListViewClass || + cls == translator.byteDataViewClass); + if (cls == translator.typedListViewClass) { + b.struct_get(info.struct, FieldIndex.typedListViewTypedData); + } else { + b.struct_get(info.struct, FieldIndex.byteDataViewTypedData); + } + return true; + } + throw "Unrecognized typed data getter: ${cls.name}.$name"; + } + } + + // int members + if (member.enclosingClass == translator.boxedIntClass && + member.function.body == null) { + String op = member.name.text; + if (functionNode.requiredParameterCount == 0) { + CodeGenCallback? code = unaryOperatorMap[intType]![op]; + if (code != null) { + w.ValueType resultType = unaryResultMap[op] ?? intType; + w.ValueType inputType = function.type.inputs.single; + w.ValueType outputType = function.type.outputs.single; + b.local_get(function.locals[0]); + translator.convertType(function, inputType, intType); + code(b); + translator.convertType(function, resultType, outputType); + return true; + } + } else if (functionNode.requiredParameterCount == 1) { + CodeGenCallback? code = binaryOperatorMap[intType]![intType]![op]; + if (code != null) { + w.ValueType leftType = function.type.inputs[0]; + w.ValueType rightType = function.type.inputs[1]; + w.ValueType outputType = function.type.outputs.single; + if (rightType == intType) { + // int parameter + b.local_get(function.locals[0]); + translator.convertType(function, leftType, intType); + b.local_get(function.locals[1]); + code(b); + if (!isComparison(op)) { + translator.convertType(function, intType, outputType); + } + return true; + } + // num parameter + ClassInfo intInfo = translator.classInfo[translator.boxedIntClass]!; + w.Label intArg = b.block(const [], [intInfo.nonNullableType]); + b.local_get(function.locals[1]); + translator.br_on_cast(b, intArg, intInfo); + // double argument + b.drop(); + b.local_get(function.locals[0]); + translator.convertType(function, leftType, intType); + b.f64_convert_i64_s(); + b.local_get(function.locals[1]); + translator.convertType(function, rightType, doubleType); + // Inline double op + CodeGenCallback doubleCode = + binaryOperatorMap[doubleType]![doubleType]![op]!; + doubleCode(b); + if (!isComparison(op)) { + translator.convertType(function, doubleType, outputType); + } + b.return_(); + b.end(); + // int argument + translator.convertType(function, intInfo.nonNullableType, intType); + w.Local rightTemp = function.addLocal(intType); + b.local_set(rightTemp); + b.local_get(function.locals[0]); + translator.convertType(function, leftType, intType); + b.local_get(rightTemp); + code(b); + if (!isComparison(op)) { + translator.convertType(function, intType, outputType); + } + return true; + } + } + } + + // double unary members + if (member.enclosingClass == translator.boxedDoubleClass && + member.function.body == null) { + String op = member.name.text; + if (functionNode.requiredParameterCount == 0) { + CodeGenCallback? code = unaryOperatorMap[doubleType]![op]; + if (code != null) { + w.ValueType resultType = unaryResultMap[op] ?? doubleType; + w.ValueType inputType = function.type.inputs.single; + w.ValueType outputType = function.type.outputs.single; + b.local_get(function.locals[0]); + translator.convertType(function, inputType, doubleType); + code(b); + translator.convertType(function, resultType, outputType); + return true; + } + } + } + + return false; + } +} diff --git a/pkg/dart2wasm/lib/param_info.dart b/pkg/dart2wasm/lib/param_info.dart new file mode 100644 index 00000000000..182af47906b --- /dev/null +++ b/pkg/dart2wasm/lib/param_info.dart @@ -0,0 +1,93 @@ +// 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 'package:dart2wasm/reference_extensions.dart'; + +import 'package:kernel/ast.dart'; + +/// Information about optional parameters and their default values for a +/// member or a set of members belonging to the same override group. +class ParameterInfo { + final Member member; + int typeParamCount = 0; + late final List positional; + late final Map named; + + // Do not access these until the info is complete. + late final List names = named.keys.toList()..sort(); + late final Map nameIndex = { + for (int i = 0; i < names.length; i++) names[i]: positional.length + i + }; + + int get paramCount => positional.length + named.length; + + static Constant? defaultValue(VariableDeclaration param) { + Expression? initializer = param.initializer; + if (initializer is ConstantExpression) { + return initializer.constant; + } else if (initializer == null) { + return null; + } else { + throw "Non-constant default value"; + } + } + + ParameterInfo.fromMember(Reference target) : member = target.asMember { + FunctionNode? function = member.function; + if (target.isTearOffReference) { + positional = []; + named = {}; + } else if (function != null) { + typeParamCount = (member is Constructor + ? member.enclosingClass!.typeParameters + : function.typeParameters) + .length; + positional = List.generate(function.positionalParameters.length, (i) { + // A required parameter has no default value. + if (i < function.requiredParameterCount) return null; + return defaultValue(function.positionalParameters[i]); + }); + named = { + for (VariableDeclaration param in function.namedParameters) + param.name!: defaultValue(param) + }; + } else { + // A setter parameter has no default value. + positional = [if (target.isSetter) null]; + named = {}; + } + } + + void merge(ParameterInfo other) { + assert(typeParamCount == other.typeParamCount); + for (int i = 0; i < other.positional.length; i++) { + if (i >= positional.length) { + positional.add(other.positional[i]); + } else { + if (positional[i] == null) { + positional[i] = other.positional[i]; + } else if (other.positional[i] != null) { + if (positional[i] != other.positional[i]) { + print("Mismatching default value for parameter $i: " + "${member}: ${positional[i]} vs " + "${other.member}: ${other.positional[i]}"); + } + } + } + } + for (String name in other.named.keys) { + Constant? value = named[name]; + Constant? otherValue = other.named[name]; + if (value == null) { + named[name] = otherValue; + } else if (otherValue != null) { + if (value != otherValue) { + print("Mismatching default value for parameter '$name': " + "${member}: ${value} vs " + "${other.member}: ${otherValue}"); + } + } + } + } +} diff --git a/pkg/dart2wasm/lib/reference_extensions.dart b/pkg/dart2wasm/lib/reference_extensions.dart new file mode 100644 index 00000000000..ba9a68cc2b6 --- /dev/null +++ b/pkg/dart2wasm/lib/reference_extensions.dart @@ -0,0 +1,62 @@ +// 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 'package:kernel/ast.dart'; + +// Extend references with flags to more easily identify getters and setters. + +extension GetterSetterReference on Reference { + bool get isImplicitGetter { + Member member = asMember; + return member is Field && member.getterReference == this; + } + + bool get isImplicitSetter { + Member member = asMember; + return member is Field && member.setterReference == this; + } + + bool get isGetter { + Member member = asMember; + return member is Procedure && member.isGetter || isImplicitGetter; + } + + bool get isSetter { + Member member = asMember; + return member is Procedure && member.isSetter || isImplicitSetter; + } +} + +// Extend procedures with a tearOffReference that refers to the tear-off +// implementation for that procedure. This enables a Reference to refer to any +// implementation relating to a member, including its tear-off, which it can't +// do in plain kernel. + +extension TearOffReference on Procedure { + // Use an Expando to avoid keeping the procedure alive. + static final Expando _tearOffReference = Expando(); + + Reference get tearOffReference => + _tearOffReference[this] ??= Reference()..node = this; +} + +extension IsTearOffReference on Reference { + bool get isTearOffReference { + Member member = asMember; + return member is Procedure && member.tearOffReference == this; + } +} + +extension ReferenceAs on Member { + Reference referenceAs({required bool getter, required bool setter}) { + Member member = this; + return member is Field + ? setter + ? member.setterReference! + : member.getterReference + : getter && member is Procedure && member.kind == ProcedureKind.Method + ? member.tearOffReference + : member.reference; + } +} diff --git a/pkg/dart2wasm/lib/target.dart b/pkg/dart2wasm/lib/target.dart new file mode 100644 index 00000000000..a7827e1c1a8 --- /dev/null +++ b/pkg/dart2wasm/lib/target.dart @@ -0,0 +1,191 @@ +// 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 'package:kernel/ast.dart'; +import 'package:kernel/class_hierarchy.dart'; +import 'package:kernel/clone.dart'; +import 'package:kernel/core_types.dart'; +import 'package:kernel/reference_from_index.dart'; +import 'package:kernel/target/changed_structure_notifier.dart'; +import 'package:kernel/target/targets.dart'; +import 'package:kernel/transformations/mixin_full_resolution.dart' + as transformMixins show transformLibraries; + +import 'package:dart2wasm/constants_backend.dart'; +import 'package:dart2wasm/transformers.dart' as wasmTrans; + +class WasmTarget extends Target { + Class? _growableList; + Class? _immutableList; + Class? _immutableMap; + Class? _unmodifiableSet; + Class? _compactLinkedCustomHashMap; + Class? _compactLinkedHashSet; + Class? _oneByteString; + Class? _twoByteString; + + @override + late final ConstantsBackend constantsBackend; + + @override + String get name => 'wasm'; + + @override + TargetFlags get flags => TargetFlags(enableNullSafety: true); + + @override + List get extraIndexedLibraries => const [ + "dart:collection", + "dart:typed_data", + ]; + + void _patchHostEndian(CoreTypes coreTypes) { + // Fix Endian.host to be a const field equal to Endian.little instead of + // a final field. Wasm is a little-endian platform. + // Can't use normal patching process for this because CFE does not + // support patching fields. + // See http://dartbug.com/32836 for the background. + final Field host = + coreTypes.index.getField('dart:typed_data', 'Endian', 'host'); + final Field little = + coreTypes.index.getField('dart:typed_data', 'Endian', 'little'); + host.isConst = true; + host.initializer = new CloneVisitorNotMembers().clone(little.initializer!) + ..parent = host; + } + + @override + void performPreConstantEvaluationTransformations( + Component component, + CoreTypes coreTypes, + List libraries, + DiagnosticReporter diagnosticReporter, + {void Function(String msg)? logger, + ChangedStructureNotifier? changedStructureNotifier}) { + constantsBackend = WasmConstantsBackend(coreTypes); + _patchHostEndian(coreTypes); + } + + @override + void performModularTransformationsOnLibraries( + Component component, + CoreTypes coreTypes, + ClassHierarchy hierarchy, + List libraries, + Map? environmentDefines, + DiagnosticReporter diagnosticReporter, + ReferenceFromIndex? referenceFromIndex, + {void logger(String msg)?, + ChangedStructureNotifier? changedStructureNotifier}) { + transformMixins.transformLibraries( + this, coreTypes, hierarchy, libraries, referenceFromIndex); + logger?.call("Transformed mixin applications"); + + wasmTrans.transformLibraries(libraries, coreTypes, hierarchy); + } + + @override + void performTransformationsOnProcedure( + CoreTypes coreTypes, + ClassHierarchy hierarchy, + Procedure procedure, + Map? environmentDefines, + {void logger(String msg)?}) { + wasmTrans.transformProcedure(procedure, coreTypes, hierarchy); + } + + @override + Expression instantiateInvocation(CoreTypes coreTypes, Expression receiver, + String name, Arguments arguments, int offset, bool isSuper) { + throw "Unsupported: instantiateInvocation"; + } + + Expression instantiateNoSuchMethodError(CoreTypes coreTypes, + Expression receiver, String name, Arguments arguments, int offset, + {bool isMethod: false, + bool isGetter: false, + bool isSetter: false, + bool isField: false, + bool isLocalVariable: false, + bool isDynamic: false, + bool isSuper: false, + bool isStatic: false, + bool isConstructor: false, + bool isTopLevel: false}) { + throw "Unsupported: instantiateNoSuchMethodError"; + } + + @override + bool get supportsSetLiterals => false; + + @override + int get enabledLateLowerings => LateLowering.all; + + @override + int get enabledConstructorTearOffLowerings => ConstructorTearOffLowering.all; + + @override + bool get supportsExplicitGetterCalls => true; + + @override + bool get supportsLateLoweringSentinel => false; + + @override + bool get useStaticFieldLowering => false; + + @override + bool enableNative(Uri uri) => true; + + @override + Class concreteListLiteralClass(CoreTypes coreTypes) { + return _growableList ??= + coreTypes.index.getClass('dart:core', '_GrowableList'); + } + + @override + Class concreteConstListLiteralClass(CoreTypes coreTypes) { + return _immutableList ??= + coreTypes.index.getClass('dart:core', '_ImmutableList'); + } + + @override + Class concreteMapLiteralClass(CoreTypes coreTypes) { + return _compactLinkedCustomHashMap ??= coreTypes.index + .getClass('dart:collection', '_CompactLinkedCustomHashMap'); + } + + @override + Class concreteConstMapLiteralClass(CoreTypes coreTypes) { + return _immutableMap ??= + coreTypes.index.getClass('dart:collection', '_ImmutableMap'); + } + + @override + Class concreteSetLiteralClass(CoreTypes coreTypes) { + return _compactLinkedHashSet ??= + coreTypes.index.getClass('dart:collection', '_CompactLinkedHashSet'); + } + + @override + Class concreteConstSetLiteralClass(CoreTypes coreTypes) { + return _unmodifiableSet ??= + coreTypes.index.getClass('dart:collection', '_UnmodifiableSet'); + } + + @override + Class concreteStringLiteralClass(CoreTypes coreTypes, String value) { + const int maxLatin1 = 0xff; + for (int i = 0; i < value.length; ++i) { + if (value.codeUnitAt(i) > maxLatin1) { + return _twoByteString ??= + coreTypes.index.getClass('dart:core', '_TwoByteString'); + } + } + return _oneByteString ??= + coreTypes.index.getClass('dart:core', '_OneByteString'); + } + + @override + bool isSupportedPragma(String pragmaName) => pragmaName.startsWith("wasm:"); +} diff --git a/pkg/dart2wasm/lib/transformers.dart b/pkg/dart2wasm/lib/transformers.dart new file mode 100644 index 00000000000..83793e381bc --- /dev/null +++ b/pkg/dart2wasm/lib/transformers.dart @@ -0,0 +1,108 @@ +// 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 'package:kernel/ast.dart'; +import 'package:kernel/class_hierarchy.dart'; +import 'package:kernel/core_types.dart'; +import 'package:kernel/type_environment.dart'; + +void transformLibraries( + List libraries, CoreTypes coreTypes, ClassHierarchy hierarchy) { + final transformer = _WasmTransformer(coreTypes, hierarchy); + libraries.forEach(transformer.visitLibrary); +} + +void transformProcedure( + Procedure procedure, CoreTypes coreTypes, ClassHierarchy hierarchy) { + final transformer = _WasmTransformer(coreTypes, hierarchy); + procedure.accept(transformer); +} + +class _WasmTransformer extends Transformer { + final TypeEnvironment env; + + Member? _currentMember; + StaticTypeContext? _cachedTypeContext; + + StaticTypeContext get typeContext => + _cachedTypeContext ??= StaticTypeContext(_currentMember!, env); + + _WasmTransformer(CoreTypes coreTypes, ClassHierarchy hierarchy) + : env = TypeEnvironment(coreTypes, hierarchy); + + @override + defaultMember(Member node) { + _currentMember = node; + _cachedTypeContext = null; + + final result = super.defaultMember(node); + + _currentMember = null; + _cachedTypeContext = null; + return result; + } + + @override + TreeNode visitForInStatement(ForInStatement stmt) { + // Transform + // + // for ({var/final} T in ) { ... } + // + // Into + // + // { + // final Iterator #forIterator = .iterator; + // for (; #forIterator.moveNext() ;) { + // {var/final} T variable = #forIterator.current; + // ... + // } + // } + // } + final CoreTypes coreTypes = typeContext.typeEnvironment.coreTypes; + + // The CFE might invoke this transformation despite the program having + // compile-time errors. So we will not transform this [stmt] if the + // `stmt.iterable` is an invalid expression or has an invalid type and + // instead eliminate the entire for-in and replace it with a invalid + // expression statement. + final iterable = stmt.iterable; + final iterableType = iterable.getStaticType(typeContext); + if (iterableType is InvalidType) { + return ExpressionStatement( + InvalidExpression('Invalid iterable type in for-in')); + } + + final DartType elementType = stmt.getElementType(typeContext); + final iteratorType = InterfaceType( + coreTypes.iteratorClass, Nullability.nonNullable, [elementType]); + + final iterator = VariableDeclaration("#forIterator", + initializer: InstanceGet( + InstanceAccessKind.Instance, iterable, Name('iterator'), + interfaceTarget: coreTypes.iterableGetIterator, + resultType: coreTypes.iterableGetIterator.function.returnType) + ..fileOffset = iterable.fileOffset, + type: iteratorType) + ..fileOffset = iterable.fileOffset; + + final condition = InstanceInvocation(InstanceAccessKind.Instance, + VariableGet(iterator), Name('moveNext'), Arguments(const []), + interfaceTarget: coreTypes.iteratorMoveNext, + functionType: coreTypes.iteratorMoveNext.function + .computeFunctionType(Nullability.nonNullable)) + ..fileOffset = iterable.fileOffset; + + final variable = stmt.variable + ..initializer = (InstanceGet( + InstanceAccessKind.Instance, VariableGet(iterator), Name('current'), + interfaceTarget: coreTypes.iteratorGetCurrent, + resultType: coreTypes.iteratorGetCurrent.function.returnType) + ..fileOffset = stmt.bodyOffset); + + final Block body = Block([variable, stmt.body]); + + return Block([iterator, ForStatement(const [], condition, const [], body)]) + .accept(this); + } +} diff --git a/pkg/dart2wasm/lib/translator.dart b/pkg/dart2wasm/lib/translator.dart new file mode 100644 index 00000000000..7a33ce43864 --- /dev/null +++ b/pkg/dart2wasm/lib/translator.dart @@ -0,0 +1,819 @@ +// 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:dart2wasm/class_info.dart'; +import 'package:dart2wasm/closures.dart'; +import 'package:dart2wasm/code_generator.dart'; +import 'package:dart2wasm/constants.dart'; +import 'package:dart2wasm/dispatch_table.dart'; +import 'package:dart2wasm/functions.dart'; +import 'package:dart2wasm/globals.dart'; +import 'package:dart2wasm/param_info.dart'; +import 'package:dart2wasm/reference_extensions.dart'; + +import 'package:kernel/ast.dart'; +import 'package:kernel/class_hierarchy.dart' + show ClassHierarchy, ClassHierarchySubtypes, ClosedWorldClassHierarchy; +import 'package:kernel/core_types.dart'; +import 'package:kernel/src/printer.dart'; +import 'package:kernel/type_environment.dart'; +import 'package:vm/metadata/direct_call.dart'; + +import 'package:wasm_builder/wasm_builder.dart' as w; + +/// Options controlling the translation. +class TranslatorOptions { + bool exportAll = false; + bool inlining = false; + int inliningLimit = 3; + bool lazyConstants = false; + bool localNullability = false; + bool nameSection = true; + bool nominalTypes = false; + bool parameterNullability = true; + bool polymorphicSpecialization = false; + bool printKernel = false; + bool printWasm = false; + bool runtimeTypes = true; + bool stringDataSegments = false; + List? watchPoints = null; + + bool get useRttGlobals => runtimeTypes && !nominalTypes; +} + +typedef CodeGenCallback = void Function(w.Instructions); + +/// The main entry point for the translation from kernel to Wasm and the hub for +/// all global state in the compiler. +/// +/// This class also contains utility methods for types and code generation used +/// throughout the compiler. +class Translator { + // Options for the translation. + final TranslatorOptions options; + + // Kernel input and context. + final Component component; + final List libraries; + final CoreTypes coreTypes; + final TypeEnvironment typeEnvironment; + final ClosedWorldClassHierarchy hierarchy; + late final ClassHierarchySubtypes subtypes; + + // Classes and members referenced specifically by the compiler. + late final Class wasmTypesBaseClass; + late final Class wasmArrayBaseClass; + late final Class wasmAnyRefClass; + late final Class wasmEqRefClass; + late final Class wasmDataRefClass; + late final Class boxedBoolClass; + late final Class boxedIntClass; + late final Class boxedDoubleClass; + late final Class functionClass; + late final Class listBaseClass; + late final Class fixedLengthListClass; + late final Class growableListClass; + late final Class immutableListClass; + late final Class stringBaseClass; + late final Class oneByteStringClass; + late final Class twoByteStringClass; + late final Class typeClass; + late final Class typedListBaseClass; + late final Class typedListClass; + late final Class typedListViewClass; + late final Class byteDataViewClass; + late final Procedure stringEquals; + late final Procedure stringInterpolate; + late final Procedure mapFactory; + late final Procedure mapPut; + late final Map builtinTypes; + late final Map boxedClasses; + + // Other parts of the global compiler state. + late final ClassInfoCollector classInfoCollector; + late final DispatchTable dispatchTable; + late final Globals globals; + late final Constants constants; + late final FunctionCollector functions; + + // Information about the program used and updated by the various phases. + final List classes = []; + final Map classInfo = {}; + final Map classForHeapType = {}; + final Map fieldIndex = {}; + final Map typeParameterIndex = {}; + final Map staticParamInfo = {}; + late Procedure mainFunction; + late final w.Module m; + late final w.DefinedFunction initFunction; + late final w.ValueType voidMarker; + + // Caches for when identical source constructs need a common representation. + final Map arrayTypeCache = {}; + final Map functionTypeCache = {}; + final Map functionTypeParameterCount = {}; + final Map functionTypeRtt = {}; + final Map functionRefCache = {}; + final Map tearOffFunctionCache = {}; + + ClassInfo get topInfo => classes[0]; + ClassInfo get objectInfo => classInfo[coreTypes.objectClass]!; + + Translator(this.component, this.coreTypes, this.typeEnvironment, this.options) + : libraries = component.libraries, + hierarchy = + ClassHierarchy(component, coreTypes) as ClosedWorldClassHierarchy { + subtypes = hierarchy.computeSubtypesInformation(); + classInfoCollector = ClassInfoCollector(this); + dispatchTable = DispatchTable(this); + functions = FunctionCollector(this); + + Library coreLibrary = + component.libraries.firstWhere((l) => l.name == "dart.core"); + Class lookupCore(String name) { + return coreLibrary.classes.firstWhere((c) => c.name == name); + } + + Library collectionLibrary = + component.libraries.firstWhere((l) => l.name == "dart.collection"); + Class lookupCollection(String name) { + return collectionLibrary.classes.firstWhere((c) => c.name == name); + } + + Library typedDataLibrary = + component.libraries.firstWhere((l) => l.name == "dart.typed_data"); + Class lookupTypedData(String name) { + return typedDataLibrary.classes.firstWhere((c) => c.name == name); + } + + Library wasmLibrary = + component.libraries.firstWhere((l) => l.name == "dart.wasm"); + Class lookupWasm(String name) { + return wasmLibrary.classes.firstWhere((c) => c.name == name); + } + + wasmTypesBaseClass = lookupWasm("_WasmBase"); + wasmArrayBaseClass = lookupWasm("_WasmArray"); + wasmAnyRefClass = lookupWasm("WasmAnyRef"); + wasmEqRefClass = lookupWasm("WasmEqRef"); + wasmDataRefClass = lookupWasm("WasmDataRef"); + boxedBoolClass = lookupCore("_BoxedBool"); + boxedIntClass = lookupCore("_BoxedInt"); + boxedDoubleClass = lookupCore("_BoxedDouble"); + functionClass = lookupCore("_Function"); + fixedLengthListClass = lookupCore("_List"); + listBaseClass = lookupCore("_ListBase"); + growableListClass = lookupCore("_GrowableList"); + immutableListClass = lookupCore("_ImmutableList"); + stringBaseClass = lookupCore("_StringBase"); + oneByteStringClass = lookupCore("_OneByteString"); + twoByteStringClass = lookupCore("_TwoByteString"); + typeClass = lookupCore("_Type"); + typedListBaseClass = lookupTypedData("_TypedListBase"); + typedListClass = lookupTypedData("_TypedList"); + typedListViewClass = lookupTypedData("_TypedListView"); + byteDataViewClass = lookupTypedData("_ByteDataView"); + stringEquals = + stringBaseClass.procedures.firstWhere((p) => p.name.text == "=="); + stringInterpolate = stringBaseClass.procedures + .firstWhere((p) => p.name.text == "_interpolate"); + mapFactory = lookupCollection("LinkedHashMap").procedures.firstWhere( + (p) => p.kind == ProcedureKind.Factory && p.name.text == "_default"); + mapPut = lookupCollection("_CompactLinkedCustomHashMap") + .superclass! // _HashBase + .superclass! // _LinkedHashMapMixin + .procedures + .firstWhere((p) => p.name.text == "[]="); + builtinTypes = { + coreTypes.boolClass: w.NumType.i32, + coreTypes.intClass: w.NumType.i64, + coreTypes.doubleClass: w.NumType.f64, + wasmAnyRefClass: w.RefType.any(nullable: false), + wasmEqRefClass: w.RefType.eq(nullable: false), + wasmDataRefClass: w.RefType.data(nullable: false), + boxedBoolClass: w.NumType.i32, + boxedIntClass: w.NumType.i64, + boxedDoubleClass: w.NumType.f64, + lookupWasm("WasmI8"): w.PackedType.i8, + lookupWasm("WasmI16"): w.PackedType.i16, + lookupWasm("WasmI32"): w.NumType.i32, + lookupWasm("WasmI64"): w.NumType.i64, + lookupWasm("WasmF32"): w.NumType.f32, + lookupWasm("WasmF64"): w.NumType.f64, + }; + boxedClasses = { + w.NumType.i32: boxedBoolClass, + w.NumType.i64: boxedIntClass, + w.NumType.f64: boxedDoubleClass, + }; + } + + Uint8List translate() { + m = w.Module(watchPoints: options.watchPoints); + voidMarker = w.RefType.def(w.StructType("void"), nullable: true); + + classInfoCollector.collect(); + + functions.collectImportsAndExports(); + mainFunction = + libraries.first.procedures.firstWhere((p) => p.name.text == "main"); + functions.addExport(mainFunction.reference, "main"); + + initFunction = m.addFunction(functionType(const [], const []), "#init"); + m.startFunction = initFunction; + + globals = Globals(this); + constants = Constants(this); + + dispatchTable.build(); + + functions.initialize(); + while (functions.worklist.isNotEmpty) { + Reference reference = functions.worklist.removeLast(); + Member member = reference.asMember; + var function = + functions.getExistingFunction(reference) as w.DefinedFunction; + + String canonicalName = "$member"; + if (reference.isSetter) { + canonicalName = "$canonicalName="; + } else if (reference.isGetter || reference.isTearOffReference) { + int dot = canonicalName.indexOf('.'); + canonicalName = canonicalName.substring(0, dot + 1) + + '=' + + canonicalName.substring(dot + 1); + } + canonicalName = member.enclosingLibrary == libraries.first + ? canonicalName + : "${member.enclosingLibrary.importUri} $canonicalName"; + + String? exportName = functions.exports[reference]; + + if (options.printKernel || options.printWasm) { + if (exportName != null) { + print("#${function.index}: $canonicalName (exported as $exportName)"); + } else { + print("#${function.index}: $canonicalName"); + } + print(member.function + ?.computeFunctionType(Nullability.nonNullable) + .toStringInternal()); + } + if (options.printKernel) { + if (member is Constructor) { + Class cls = member.enclosingClass; + for (Field field in cls.fields) { + if (field.isInstanceMember && field.initializer != null) { + print("${field.name}: ${field.initializer}"); + } + } + for (Initializer initializer in member.initializers) { + print(initializer); + } + } + Statement? body = member.function?.body; + if (body != null) { + print(body); + } + if (!options.printWasm) print(""); + } + + if (exportName != null) { + m.exportFunction(exportName, function); + } else if (options.exportAll) { + m.exportFunction(canonicalName, function); + } + var codeGen = CodeGenerator(this, function, reference); + codeGen.generate(); + + if (options.printWasm) { + print(function.type); + print(function.body.trace); + } + + for (Lambda lambda in codeGen.closures.lambdas.values) { + CodeGenerator(this, lambda.function, reference) + .generateLambda(lambda, codeGen.closures); + _printFunction(lambda.function, "$canonicalName (closure)"); + } + } + + dispatchTable.output(); + constants.finalize(); + initFunction.body.end(); + + for (ConstantInfo info in constants.constantInfo.values) { + w.DefinedFunction? function = info.function; + if (function != null) { + _printFunction(function, info.constant); + } else { + if (options.printWasm) { + print("Global #${info.global.index}: ${info.constant}"); + print(info.global.initializer.trace); + } + } + } + if (options.lazyConstants) { + _printFunction(constants.oneByteStringFunction, "makeOneByteString"); + _printFunction(constants.twoByteStringFunction, "makeTwoByteString"); + } + _printFunction(initFunction, "init"); + + return m.encode(emitNameSection: options.nameSection); + } + + void _printFunction(w.DefinedFunction function, Object name) { + if (options.printWasm) { + print("#${function.index}: $name"); + print(function.body.trace); + } + } + + Class classForType(DartType type) { + return type is InterfaceType + ? type.classNode + : type is TypeParameterType + ? classForType(type.bound) + : coreTypes.objectClass; + } + + w.ValueType translateType(DartType type) { + w.StorageType wasmType = translateStorageType(type); + if (wasmType is w.ValueType) return wasmType; + throw "Packed types are only allowed in arrays and fields"; + } + + bool isWasmType(Class cls) { + while (cls.superclass != null) { + cls = cls.superclass!; + if (cls == wasmTypesBaseClass) return true; + } + return false; + } + + w.StorageType typeForInfo(ClassInfo info, bool nullable) { + Class? cls = info.cls; + if (cls != null) { + w.StorageType? builtin = builtinTypes[cls]; + if (builtin != null) { + if (!nullable) return builtin; + if (isWasmType(cls)) { + if (builtin.isPrimitive) throw "Wasm numeric types can't be nullable"; + return (builtin as w.RefType).withNullability(true); + } + Class? boxedClass = boxedClasses[builtin]; + if (boxedClass != null) { + info = classInfo[boxedClass]!; + } + } + } + return w.RefType.def(info.repr.struct, + nullable: !options.parameterNullability || nullable); + } + + w.StorageType translateStorageType(DartType type) { + if (type is InterfaceType) { + if (type.classNode.superclass == wasmArrayBaseClass) { + DartType elementType = type.typeArguments.single; + return w.RefType.def(arrayTypeForDartType(elementType), + nullable: false); + } + return typeForInfo( + classInfo[type.classNode]!, type.isPotentiallyNullable); + } + if (type is DynamicType) { + return topInfo.nullableType; + } + if (type is NullType) { + return topInfo.nullableType; + } + if (type is NeverType) { + return topInfo.nullableType; + } + if (type is VoidType) { + return voidMarker; + } + if (type is TypeParameterType) { + return translateStorageType(type.isPotentiallyNullable + ? type.bound.withDeclaredNullability(type.nullability) + : type.bound); + } + if (type is FutureOrType) { + return topInfo.nullableType; + } + if (type is FunctionType) { + if (type.requiredParameterCount != type.positionalParameters.length || + type.namedParameters.isNotEmpty) { + throw "Function types with optional parameters not supported: $type"; + } + return w.RefType.def(closureStructType(type.requiredParameterCount), + nullable: + !options.parameterNullability || type.isPotentiallyNullable); + } + throw "Unsupported type ${type.runtimeType}"; + } + + w.ArrayType arrayTypeForDartType(DartType type) { + while (type is TypeParameterType) type = type.bound; + return wasmArrayType( + translateStorageType(type), type.toText(defaultAstTextStrategy)); + } + + w.ArrayType wasmArrayType(w.StorageType type, String name) { + return arrayTypeCache.putIfAbsent( + type, () => arrayType("Array<$name>", elementType: w.FieldType(type))); + } + + w.StructType closureStructType(int parameterCount) { + return functionTypeCache.putIfAbsent(parameterCount, () { + ClassInfo info = classInfo[functionClass]!; + w.StructType struct = structType("Function$parameterCount", + fields: info.struct.fields, superType: info.struct); + assert(struct.fields.length == FieldIndex.closureFunction); + struct.fields.add(w.FieldType( + w.RefType.def(closureFunctionType(parameterCount), nullable: false), + mutable: false)); + if (options.useRttGlobals) { + functionTypeRtt[parameterCount] = + classInfoCollector.makeRtt(struct, info); + } + functionTypeParameterCount[struct] = parameterCount; + return struct; + }); + } + + w.FunctionType closureFunctionType(int parameterCount) { + return functionType([ + w.RefType.data(), + ...List.filled(parameterCount, topInfo.nullableType) + ], [ + topInfo.nullableType + ]); + } + + int parameterCountForFunctionStruct(w.HeapType heapType) { + return functionTypeParameterCount[heapType]!; + } + + w.DefinedGlobal makeFunctionRef(w.DefinedFunction f) { + return functionRefCache.putIfAbsent(f, () { + w.DefinedGlobal global = m.addGlobal( + w.GlobalType(w.RefType.def(f.type, nullable: false), mutable: false)); + global.initializer.ref_func(f); + global.initializer.end(); + return global; + }); + } + + w.DefinedFunction getTearOffFunction(Procedure member) { + return tearOffFunctionCache.putIfAbsent(member, () { + assert(member.kind == ProcedureKind.Method); + FunctionNode functionNode = member.function; + int parameterCount = functionNode.requiredParameterCount; + if (functionNode.positionalParameters.length != parameterCount || + functionNode.namedParameters.isNotEmpty) { + throw "Not supported: Tear-off with optional parameters" + " at ${member.location}"; + } + if (functionNode.typeParameters.isNotEmpty) { + throw "Not supported: Tear-off with type parameters" + " at ${member.location}"; + } + w.FunctionType memberSignature = signatureFor(member.reference); + w.FunctionType closureSignature = closureFunctionType(parameterCount); + int signatureOffset = member.isInstanceMember ? 1 : 0; + assert(memberSignature.inputs.length == signatureOffset + parameterCount); + assert(closureSignature.inputs.length == 1 + parameterCount); + w.DefinedFunction function = + m.addFunction(closureSignature, "$member (tear-off)"); + w.BaseFunction target = functions.getFunction(member.reference); + w.Instructions b = function.body; + for (int i = 0; i < memberSignature.inputs.length; i++) { + w.Local paramLocal = function.locals[(1 - signatureOffset) + i]; + b.local_get(paramLocal); + convertType(function, paramLocal.type, memberSignature.inputs[i]); + } + b.call(target); + convertType(function, outputOrVoid(target.type.outputs), + outputOrVoid(closureSignature.outputs)); + b.end(); + return function; + }); + } + + w.ValueType ensureBoxed(w.ValueType type) { + // Box receiver if it's primitive + if (type is w.RefType) return type; + return w.RefType.def(classInfo[boxedClasses[type]!]!.struct, + nullable: false); + } + + w.ValueType typeForLocal(w.ValueType type) { + return options.localNullability ? type : type.withNullability(true); + } + + w.ValueType outputOrVoid(List outputs) { + return outputs.isEmpty ? voidMarker : outputs.single; + } + + bool needsConversion(w.ValueType from, w.ValueType to) { + return (from == voidMarker) ^ (to == voidMarker) || !from.isSubtypeOf(to); + } + + void convertType( + w.DefinedFunction function, w.ValueType from, w.ValueType to) { + w.Instructions b = function.body; + if (from == voidMarker || to == voidMarker) { + if (from != voidMarker) { + b.drop(); + return; + } + if (to != voidMarker) { + if (to is w.RefType && to.nullable) { + // This can happen when a void method has its return type overridden to + // return a value, in which case the selector signature will have a + // non-void return type to encompass all possible return values. + b.ref_null(to.heapType); + } else { + // This only happens in invalid but unreachable code produced by the + // TFA dead-code elimination. + b.comment("Non-nullable void conversion"); + b.unreachable(); + } + return; + } + } + if (!from.isSubtypeOf(to)) { + if (from is! w.RefType && to is w.RefType) { + // Boxing + ClassInfo info = classInfo[boxedClasses[from]!]!; + assert(info.struct.isSubtypeOf(to.heapType)); + w.Local temp = function.addLocal(from); + b.local_set(temp); + b.i32_const(info.classId); + b.local_get(temp); + struct_new(b, info); + } else if (from is w.RefType && to is! w.RefType) { + // Unboxing + ClassInfo info = classInfo[boxedClasses[to]!]!; + if (!from.heapType.isSubtypeOf(info.struct)) { + // Cast to box type + if (!from.heapType.isSubtypeOf(w.HeapType.data)) { + b.ref_as_data(); + } + ref_cast(b, info); + } + b.struct_get(info.struct, FieldIndex.boxValue); + } else if (from.withNullability(false).isSubtypeOf(to)) { + // Null check + b.ref_as_non_null(); + } else { + // Downcast + var heapType = (to as w.RefType).heapType; + ClassInfo? info = classForHeapType[heapType]; + if (from.nullable && !to.nullable) { + b.ref_as_non_null(); + } + if (!(from as w.RefType).heapType.isSubtypeOf(w.HeapType.data)) { + b.ref_as_data(); + } + ref_cast( + b, + info ?? + (heapType.isSubtypeOf(classInfo[functionClass]!.struct) + ? parameterCountForFunctionStruct(heapType) + : heapType)); + } + } + } + + w.FunctionType signatureFor(Reference target) { + Member member = target.asMember; + if (member.isInstanceMember) { + return dispatchTable.selectorForTarget(target).signature; + } else { + return functions.getFunction(target).type; + } + } + + ParameterInfo paramInfoFor(Reference target) { + Member member = target.asMember; + if (member.isInstanceMember) { + return dispatchTable.selectorForTarget(target).paramInfo; + } else { + return staticParamInfo.putIfAbsent( + target, () => ParameterInfo.fromMember(target)); + } + } + + Member? singleTarget(TreeNode node) { + DirectCallMetadataRepository metadata = + component.metadata[DirectCallMetadataRepository.repositoryTag] + as DirectCallMetadataRepository; + return metadata.mapping[node]?.target; + } + + bool shouldInline(Reference target) { + if (!options.inlining) return false; + Member member = target.asMember; + if (member is Field) return true; + Statement? body = member.function!.body; + return body != null && + NodeCounter().countNodes(body) <= options.inliningLimit; + } + + T? getPragma(Annotatable node, String name, [T? defaultvalue]) { + for (Expression annotation in node.annotations) { + if (annotation is ConstantExpression) { + Constant constant = annotation.constant; + if (constant is InstanceConstant) { + if (constant.classNode == coreTypes.pragmaClass) { + Constant? nameConstant = + constant.fieldValues[coreTypes.pragmaName.fieldReference]; + if (nameConstant is StringConstant && nameConstant.value == name) { + Object? value = + constant.fieldValues[coreTypes.pragmaOptions.fieldReference]; + if (value is PrimitiveConstant) { + return value.value; + } + return value as T? ?? defaultvalue; + } + } + } + } + } + return null; + } + + // Wrappers for type creation to abstract over equi-recursive versus nominal + // typing. The given supertype is ignored when nominal types are disabled, + // and a suitable default is inserted when nominal types are enabled. + + w.FunctionType functionType( + Iterable inputs, Iterable outputs, + {w.HeapType? superType}) { + return m.addFunctionType(inputs, outputs, + superType: options.nominalTypes ? superType ?? w.HeapType.func : null); + } + + w.StructType structType(String name, + {Iterable? fields, w.HeapType? superType}) { + return m.addStructType(name, + fields: fields, + superType: options.nominalTypes ? superType ?? w.HeapType.data : null); + } + + w.ArrayType arrayType(String name, + {w.FieldType? elementType, w.HeapType? superType}) { + return m.addArrayType(name, + elementType: elementType, + superType: options.nominalTypes ? superType ?? w.HeapType.data : null); + } + + // Wrappers for object allocation and cast instructions to abstract over + // RTT-based and static versions of the instructions. + // The [type] parameter taken by the methods is either a [ClassInfo] (to use + // the RTT for the class), an [int] (to use the RTT for the closure struct + // corresponding to functions with that number of parameters) or a + // [w.DataType] (to use the canonical RTT for the type). + + void struct_new(w.Instructions b, Object type) { + if (options.runtimeTypes) { + final struct = _emitRtt(b, type) as w.StructType; + b.struct_new_with_rtt(struct); + } else { + b.struct_new(_targetType(type) as w.StructType); + } + } + + void struct_new_default(w.Instructions b, Object type) { + if (options.runtimeTypes) { + final struct = _emitRtt(b, type) as w.StructType; + b.struct_new_default_with_rtt(struct); + } else { + b.struct_new_default(_targetType(type) as w.StructType); + } + } + + void array_new(w.Instructions b, w.ArrayType type) { + if (options.runtimeTypes) { + b.rtt_canon(type); + b.array_new_with_rtt(type); + } else { + b.array_new(type); + } + } + + void array_new_default(w.Instructions b, w.ArrayType type) { + if (options.runtimeTypes) { + b.rtt_canon(type); + b.array_new_default_with_rtt(type); + } else { + b.array_new_default(type); + } + } + + void array_init(w.Instructions b, w.ArrayType type, int length) { + if (options.runtimeTypes) { + b.rtt_canon(type); + b.array_init(type, length); + } else { + b.array_init_static(type, length); + } + } + + void array_init_from_data( + w.Instructions b, w.ArrayType type, w.DataSegment data) { + if (options.runtimeTypes) { + b.rtt_canon(type); + b.array_init_from_data(type, data); + } else { + b.array_init_from_data_static(type, data); + } + } + + void ref_test(w.Instructions b, Object type) { + if (options.runtimeTypes) { + _emitRtt(b, type); + b.ref_test(); + } else { + b.ref_test_static(_targetType(type)); + } + } + + void ref_cast(w.Instructions b, Object type) { + if (options.runtimeTypes) { + _emitRtt(b, type); + b.ref_cast(); + } else { + b.ref_cast_static(_targetType(type)); + } + } + + void br_on_cast(w.Instructions b, w.Label label, Object type) { + if (options.runtimeTypes) { + _emitRtt(b, type); + b.br_on_cast(label); + } else { + b.br_on_cast_static(label, _targetType(type)); + } + } + + void br_on_cast_fail(w.Instructions b, w.Label label, Object type) { + if (options.runtimeTypes) { + _emitRtt(b, type); + b.br_on_cast_fail(label); + } else { + b.br_on_cast_static_fail(label, _targetType(type)); + } + } + + w.DefType _emitRtt(w.Instructions b, Object type) { + if (type is ClassInfo) { + if (options.nominalTypes) { + b.rtt_canon(type.struct); + } else { + b.global_get(type.rtt); + } + return type.struct; + } else if (type is int) { + int parameterCount = type; + w.StructType struct = closureStructType(parameterCount); + if (options.nominalTypes) { + b.rtt_canon(struct); + } else { + w.DefinedGlobal rtt = functionTypeRtt[parameterCount]!; + b.global_get(rtt); + } + return struct; + } else { + b.rtt_canon(type as w.DataType); + return type; + } + } + + w.DefType _targetType(Object type) => type is ClassInfo + ? type.struct + : type is int + ? closureStructType(type) + : type as w.DefType; +} + +class NodeCounter extends Visitor with VisitorVoidMixin { + int count = 0; + + int countNodes(Node node) { + count = 0; + node.accept(this); + return count; + } + + @override + void defaultNode(Node node) { + count++; + node.visitChildren(this); + } +} diff --git a/pkg/dart2wasm/pubspec.yaml b/pkg/dart2wasm/pubspec.yaml new file mode 100644 index 00000000000..dd0b8de28d5 --- /dev/null +++ b/pkg/dart2wasm/pubspec.yaml @@ -0,0 +1,26 @@ +name: dart2wasm +# This package is not intended for consumption on pub.dev. DO NOT publish. +publish_to: none +environment: + sdk: '>=2.12.0' + +dependencies: + front_end: + path: ../front_end + kernel: + path: ../kernel + vm: + path: ../vm + wasm_builder: + path: ../wasm_builder + +dependency_overrides: + # Packages with source in the SDK + front_end: + path: ../front_end + kernel: + path: ../kernel + vm: + path: ../vm + wasm_builder: + path: ../wasm_builder diff --git a/pkg/smith/lib/configuration.dart b/pkg/smith/lib/configuration.dart index bac7ac7899a..9284ec37e47 100644 --- a/pkg/smith/lib/configuration.dart +++ b/pkg/smith/lib/configuration.dart @@ -624,6 +624,7 @@ class Compiler extends NamedEnum { static const none = Compiler._('none'); static const dart2js = Compiler._('dart2js'); static const dart2analyzer = Compiler._('dart2analyzer'); + static const dart2wasm = Compiler._('dart2wasm'); static const compareAnalyzerCfe = Compiler._('compare_analyzer_cfe'); static const dartdevc = Compiler._('dartdevc'); static const dartdevk = Compiler._('dartdevk'); @@ -639,6 +640,7 @@ class Compiler extends NamedEnum { none, dart2js, dart2analyzer, + dart2wasm, compareAnalyzerCfe, dartdevc, dartdevk, @@ -691,6 +693,12 @@ class Compiler extends NamedEnum { Runtime.safari, ]; + case Compiler.dart2wasm: + return const [ + Runtime.none, + Runtime.d8, + Runtime.chrome, + ]; case Compiler.dart2analyzer: case Compiler.compareAnalyzerCfe: return const [Runtime.none]; @@ -716,6 +724,8 @@ class Compiler extends NamedEnum { switch (this) { case Compiler.dart2js: return Runtime.d8; + case Compiler.dart2wasm: + return Runtime.d8; case Compiler.dartdevc: case Compiler.dartdevk: return Runtime.chrome; @@ -742,6 +752,7 @@ class Compiler extends NamedEnum { case Compiler.dart2analyzer: case Compiler.compareAnalyzerCfe: case Compiler.dart2js: + case Compiler.dart2wasm: case Compiler.dartdevc: case Compiler.dartdevk: case Compiler.fasta: diff --git a/pkg/test_runner/lib/src/command.dart b/pkg/test_runner/lib/src/command.dart index 45fa5d2da42..7ba599fdf4d 100644 --- a/pkg/test_runner/lib/src/command.dart +++ b/pkg/test_runner/lib/src/command.dart @@ -186,6 +186,9 @@ class CompilationCommand extends ProcessCommand { if (displayName == 'precompiler' || displayName == 'app_jit') { return VMCommandOutput( this, exitCode, timedOut, stdout, stderr, time, pid); + } else if (displayName == 'dart2wasm') { + return Dart2WasmCompilerCommandOutput( + this, exitCode, timedOut, stdout, stderr, time, compilationSkipped); } return CompilationCommandOutput( diff --git a/pkg/test_runner/lib/src/command_output.dart b/pkg/test_runner/lib/src/command_output.dart index cc65097ea9c..03db24caed8 100644 --- a/pkg/test_runner/lib/src/command_output.dart +++ b/pkg/test_runner/lib/src/command_output.dart @@ -1060,6 +1060,45 @@ class Dart2jsCompilerCommandOutput extends CompilationCommandOutput } } +class Dart2WasmCompilerCommandOutput extends CompilationCommandOutput + with _StaticErrorOutput { + static void parseErrors(String stdout, List errors) { + _StaticErrorOutput._parseCfeErrors( + ErrorSource.web, _errorRegexp, stdout, errors); + } + + /// Matches the location and message of a dart2wasm error message, which looks + /// like: + /// + /// tests/language_2/some_test.dart:9:3: Error: Some message. + /// BadThing(); + /// ^ + /// + /// The test runner only validates the main error message, and not the + /// suggested fixes, so we only parse the first line. + // TODO(rnystrom): Support validating context messages. + static final _errorRegexp = + RegExp(r"^([^:]+):(\d+):(\d+): (Error): (.*)$", multiLine: true); + + Dart2WasmCompilerCommandOutput( + Command command, + int exitCode, + bool timedOut, + List stdout, + List stderr, + Duration time, + bool compilationSkipped) + : super(command, exitCode, timedOut, stdout, stderr, time, + compilationSkipped); + + @override + void _parseErrors() { + var errors = []; + parseErrors(decodeUtf8(stdout), errors); + errors.forEach(addError); + } +} + class DevCompilerCommandOutput extends CommandOutput with _StaticErrorOutput { /// Matches the first line of a DDC error message. DDC prints errors to /// stdout that look like: diff --git a/pkg/test_runner/lib/src/compiler_configuration.dart b/pkg/test_runner/lib/src/compiler_configuration.dart index 6d72e220b89..b287bcc520b 100644 --- a/pkg/test_runner/lib/src/compiler_configuration.dart +++ b/pkg/test_runner/lib/src/compiler_configuration.dart @@ -86,6 +86,9 @@ abstract class CompilerConfiguration { case Compiler.dart2js: return Dart2jsCompilerConfiguration(configuration); + case Compiler.dart2wasm: + return Dart2WasmCompilerConfiguration(configuration); + case Compiler.dartdevc: return DevCompilerConfiguration(configuration); @@ -499,6 +502,83 @@ class Dart2jsCompilerConfiguration extends CompilerConfiguration { } } +/// Common configuration for dart2wasm-based tools, such as dart2wasm. +class Dart2WasmCompilerConfiguration extends CompilerConfiguration { + Dart2WasmCompilerConfiguration(TestConfiguration configuration) + : super._subclass(configuration); + + String computeCompilerPath() { + var prefix = 'sdk/bin'; + if (_isHostChecked) { + if (_useSdk) { + throw "--host-checked and --use-sdk cannot be used together"; + } + // The script dart2wasm_developer is not included in the + // shipped SDK, that is the script is not installed in + // "$buildDir/dart-sdk/bin/" + return '$prefix/dart2wasm_developer$shellScriptExtension'; + } + if (_useSdk) { + prefix = '${_configuration.buildDirectory}/dart-sdk/bin'; + } + return '$prefix/dart2wasm$shellScriptExtension'; + } + + List computeCompilerArguments( + TestFile testFile, List vmOptions, List args) { + return [ + // The file being compiled is the last argument. + args.last + ]; + } + + Command computeCompilationCommand(String outputFileName, + List arguments, Map environmentOverrides) { + arguments = arguments.toList(); + arguments.add('$outputFileName'); + + return CompilationCommand( + 'dart2wasm', + outputFileName, + bootstrapDependencies(), + computeCompilerPath(), + arguments, + environmentOverrides, + alwaysCompile: !_useSdk); + } + + CommandArtifact computeCompilationArtifact(String tempDir, + List arguments, Map environmentOverrides) { + var compilerArguments = [ + ...arguments, + ]; + + var inputFile = arguments.last; + var inputFilename = Uri.file(inputFile).pathSegments.last; + var out = "$tempDir/${inputFilename.replaceAll('.dart', '.wasm')}"; + var commands = [ + computeCompilationCommand(out, compilerArguments, environmentOverrides), + ]; + + return CommandArtifact(commands, out, 'application/wasm'); + } + + List computeRuntimeArguments( + RuntimeConfiguration runtimeConfiguration, + TestFile testFile, + List vmOptions, + List originalArguments, + CommandArtifact artifact) { + return [ + '--experimental-wasm-gc', + '--wasm-gc-js-interop', + 'pkg/dart2wasm/bin/run_wasm.js', + '--', + artifact.filename, + ]; + } +} + /// Configuration for `dartdevc` and `dartdevk` (DDC with Kernel) class DevCompilerConfiguration extends CompilerConfiguration { DevCompilerConfiguration(TestConfiguration configuration) diff --git a/pkg/test_runner/lib/src/configuration.dart b/pkg/test_runner/lib/src/configuration.dart index 198c01aa74f..d98b8b8b97a 100644 --- a/pkg/test_runner/lib/src/configuration.dart +++ b/pkg/test_runner/lib/src/configuration.dart @@ -203,6 +203,7 @@ class TestConfiguration { Compiler.dartkp, Compiler.fasta, Compiler.dart2js, + Compiler.dart2wasm, ]; return fastaCompilers.contains(compiler); } diff --git a/pkg/test_runner/lib/src/runtime_configuration.dart b/pkg/test_runner/lib/src/runtime_configuration.dart index 23413619181..4cba5180155 100644 --- a/pkg/test_runner/lib/src/runtime_configuration.dart +++ b/pkg/test_runner/lib/src/runtime_configuration.dart @@ -168,7 +168,7 @@ class CommandLineJavaScriptRuntime extends RuntimeConfiguration { void checkArtifact(CommandArtifact artifact) { var type = artifact.mimeType; - if (type != 'application/javascript') { + if (type != 'application/javascript' && type != 'application/wasm') { throw "Runtime '$moniker' cannot run files of type '$type'."; } } diff --git a/pkg/test_runner/lib/src/test_suite.dart b/pkg/test_runner/lib/src/test_suite.dart index 5aeaf548b88..faf6d5703bb 100644 --- a/pkg/test_runner/lib/src/test_suite.dart +++ b/pkg/test_runner/lib/src/test_suite.dart @@ -597,6 +597,7 @@ class StandardTestSuite extends TestSuite { '$directory/${name}_analyzer.status', '$directory/${name}_analyzer2.status', '$directory/${name}_dart2js.status', + '$directory/${name}_dart2wasm.status', '$directory/${name}_dartdevc.status', '$directory/${name}_kernel.status', '$directory/${name}_precompiled.status', @@ -956,6 +957,7 @@ class StandardTestSuite extends TestSuite { var commands = []; const supportedCompilers = { Compiler.dart2js, + Compiler.dart2wasm, Compiler.dartdevc, Compiler.dartdevk }; diff --git a/pkg/wasm_builder/LICENSE b/pkg/wasm_builder/LICENSE new file mode 100644 index 00000000000..9566a27bdb4 --- /dev/null +++ b/pkg/wasm_builder/LICENSE @@ -0,0 +1,26 @@ +Copyright 2022, the Dart project authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/pkg/wasm_builder/lib/src/instructions.dart b/pkg/wasm_builder/lib/src/instructions.dart new file mode 100644 index 00000000000..24a9556d839 --- /dev/null +++ b/pkg/wasm_builder/lib/src/instructions.dart @@ -0,0 +1,2283 @@ +// 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 'module.dart'; +import 'serialize.dart'; +import 'types.dart'; + +/// Thrown when Wasm bytecode validation fails. +class ValidationError { + final String trace; + final String error; + + ValidationError(this.trace, this.error); + + @override + String toString() => "$trace\n$error"; +} + +/// Label to use as target for branch instructions. +abstract class Label { + final List inputs; + final List outputs; + + late final int? ordinal; + late final int depth; + late final int baseStackHeight; + late final bool reachable; + + Label._(this.inputs, this.outputs); + + List get targetTypes; + + bool get hasOrdinal => ordinal != null; + + @override + String toString() => "L$ordinal"; +} + +class Expression extends Label { + Expression(List inputs, List outputs) + : super._(inputs, outputs) { + ordinal = null; + depth = 0; + baseStackHeight = 0; + reachable = true; + } + + List get targetTypes => outputs; +} + +class Block extends Label { + Block(List inputs, List outputs) + : super._(inputs, outputs); + + List get targetTypes => outputs; +} + +class Loop extends Label { + Loop(List inputs, List outputs) + : super._(inputs, outputs); + + List get targetTypes => inputs; +} + +class If extends Label { + bool hasElse = false; + + If(List inputs, List outputs) + : super._(inputs, outputs); + + List get targetTypes => outputs; +} + +/// A sequence of Wasm instructions. +/// +/// Instructions can be added to the sequence by calling the corresponding +/// instruction methods. +/// +/// If asserts are enabled, the instruction methods will perform on-the-fly +/// validation and throw a [ValidationError] if validation fails. +class Instructions with SerializerMixin { + /// The module containing these instructions. + final Module module; + + /// Locals declared in this body, including parameters. + final List locals; + + /// Is this the initializer of a global variable? + final bool isGlobalInitializer; + + /// Whether a textual trace of the instruction stream should be recorded when + /// emitting instructions (provided asserts are enabled). + /// + /// This trace can be accessed via the [trace] property and will be part of + /// the exception text if a validation error occurs. + bool traceEnabled = true; + + /// Whether to print a byte offset for each instruction in the textual trace. + bool byteOffsetEnabled = false; + + /// Column width for the instruction byte offset. + int byteOffsetWidth = 7; + + /// Column width for the instructions. + int instructionColumnWidth = 50; + + int _indent = 1; + final List _traceLines = []; + + int _labelCount = 0; + final List