[dart2wasm] Support defining, exporting and accessing Wasm tables
Tested: Added tests/web/wasm/table_test.dart
Change-Id: I3971f4432a7a59bd6bc9874fc96202a7a9f2283d
Cq-Include-Trybots: luci.dart.try:dart2wasm-linux-x64-d8-try
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/248586
Commit-Queue: Aske Simon Christensen <askesc@google.com>
Reviewed-by: Joshua Litt <joshualitt@google.com>
diff --git a/pkg/dart2wasm/lib/code_generator.dart b/pkg/dart2wasm/lib/code_generator.dart
index 56dca65..0e0f2c1 100644
--- a/pkg/dart2wasm/lib/code_generator.dart
+++ b/pkg/dart2wasm/lib/code_generator.dart
@@ -1340,7 +1340,7 @@
b.i32_const(offset);
b.i32_add();
}
- b.call_indirect(selector.signature);
+ b.call_indirect(selector.signature, translator.dispatchTable.wasmTable);
translator.functions.activateSelector(selector);
}
@@ -1599,7 +1599,7 @@
b.i32_const(offset);
b.i32_add();
}
- b.call_indirect(selector.signature);
+ b.call_indirect(selector.signature, translator.dispatchTable.wasmTable);
translator.functions.activateSelector(selector);
@@ -1765,28 +1765,13 @@
@override
w.ValueType visitFunctionInvocation(
FunctionInvocation node, w.ValueType expectedType) {
- Expression receiver = node.receiver;
- if (receiver is InstanceGet &&
- receiver.interfaceTarget == translator.wasmFunctionCall) {
- // Receiver is a WasmFunction
- assert(receiver.name.text == "call");
- w.RefType receiverType =
- translator.translateType(dartTypeOf(receiver.receiver)) as w.RefType;
- w.Local temp = addLocal(receiverType);
- wrap(receiver.receiver, receiverType);
- b.local_set(temp);
- w.FunctionType functionType = receiverType.heapType as w.FunctionType;
- assert(node.arguments.positional.length == functionType.inputs.length);
- for (int i = 0; i < node.arguments.positional.length; i++) {
- wrap(node.arguments.positional[i], functionType.inputs[i]);
- }
- b.local_get(temp);
- b.call_ref();
- return translator.outputOrVoid(functionType.outputs);
- }
+ w.ValueType? intrinsicResult =
+ intrinsifier.generateFunctionCallIntrinsic(node);
+ if (intrinsicResult != null) return intrinsicResult;
+
int parameterCount = node.functionType?.requiredParameterCount ??
node.arguments.positional.length;
- return _functionCall(parameterCount, receiver, node.arguments);
+ return _functionCall(parameterCount, node.receiver, node.arguments);
}
w.ValueType _functionCall(
diff --git a/pkg/dart2wasm/lib/dispatch_table.dart b/pkg/dart2wasm/lib/dispatch_table.dart
index 9817d7d..a283258 100644
--- a/pkg/dart2wasm/lib/dispatch_table.dart
+++ b/pkg/dart2wasm/lib/dispatch_table.dart
@@ -149,6 +149,7 @@
final Map<int, SelectorInfo> selectorInfo = {};
final Map<String, int> dynamicGets = {};
late final List<Reference?> table;
+ late final w.DefinedTable wasmTable;
DispatchTable(this.translator)
: selectorMetadata =
@@ -292,11 +293,11 @@
firstAvailable++;
}
}
+
+ wasmTable = translator.m.addTable(w.RefType.func(), table.length);
}
void output() {
- w.Module m = translator.m;
- w.DefinedTable wasmTable = m.addTable(w.RefType.func(), table.length);
for (int i = 0; i < table.length; i++) {
Reference? target = table[i];
if (target != null) {
diff --git a/pkg/dart2wasm/lib/functions.dart b/pkg/dart2wasm/lib/functions.dart
index eaee6d1..6c190c7 100644
--- a/pkg/dart2wasm/lib/functions.dart
+++ b/pkg/dart2wasm/lib/functions.dart
@@ -34,36 +34,35 @@
void collectImportsAndExports() {
for (Library library in translator.libraries) {
- for (Procedure procedure in library.procedures) {
- _importOrExport(procedure);
- }
+ library.procedures.forEach(_importOrExport);
+ library.fields.forEach(_importOrExport);
for (Class cls in library.classes) {
- for (Procedure procedure in cls.procedures) {
- _importOrExport(procedure);
- }
+ cls.procedures.forEach(_importOrExport);
}
}
}
- void _importOrExport(Procedure procedure) {
- String? importName = translator.getPragma(procedure, "wasm:import");
+ void _importOrExport(Member member) {
+ String? importName = translator.getPragma(member, "wasm:import");
if (importName != null) {
int dot = importName.indexOf('.');
if (dot != -1) {
- assert(!procedure.isInstanceMember);
+ assert(!member.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)");
+ if (member is Procedure) {
+ w.FunctionType ftype = _makeFunctionType(
+ member.reference, member.function.returnType, null,
+ isImportOrExport: true);
+ _functions[member.reference] =
+ m.importFunction(module, name, ftype, "$importName (import)");
+ }
}
}
String? exportName =
- translator.getPragma(procedure, "wasm:export", procedure.name.text);
+ translator.getPragma(member, "wasm:export", member.name.text);
if (exportName != null) {
- addExport(procedure.reference, exportName);
+ addExport(member.reference, exportName);
}
}
@@ -72,16 +71,26 @@
}
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");
+ // Add exports to the module and add exported functions to the worklist
+ for (var export in exports.entries) {
+ Reference target = export.key;
+ Member node = target.asMember;
+ if (node is Procedure) {
+ worklist.add(target);
+ assert(!node.isInstanceMember);
+ assert(!node.isGetter);
+ w.FunctionType ftype = _makeFunctionType(
+ target, node.function.returnType, null,
+ isImportOrExport: true);
+ w.DefinedFunction function = m.addFunction(ftype, "$node");
+ _functions[target] = function;
+ m.exportFunction(export.value, function);
+ } else if (node is Field) {
+ w.Table? table = translator.getTable(node);
+ if (table != null) {
+ m.exportTable(export.value, table);
+ }
+ }
}
// Value classes are always implicitly allocated.
diff --git a/pkg/dart2wasm/lib/intrinsics.dart b/pkg/dart2wasm/lib/intrinsics.dart
index f7b12e47..7971b28 100644
--- a/pkg/dart2wasm/lib/intrinsics.dart
+++ b/pkg/dart2wasm/lib/intrinsics.dart
@@ -152,6 +152,18 @@
return w.NumType.i64;
}
+ // WasmTable.size
+ if (cls == translator.wasmTableClass) {
+ if (receiver is! StaticGet || receiver.target is! Field) {
+ throw "Table size not directly on a static field"
+ " at ${node.location}";
+ }
+ w.Table table = translator.getTable(receiver.target as Field)!;
+ assert(name == "size");
+ b.table_size(table);
+ return w.NumType.i32;
+ }
+
// int.bitlength
if (cls == translator.coreTypes.intClass && name == 'bitLength') {
w.Local temp = codeGen.function.addLocal(w.NumType.i64);
@@ -447,6 +459,25 @@
}
}
+ // WasmTable.[] and WasmTable.[]=
+ if (cls == translator.wasmTableClass) {
+ if (receiver is! StaticGet || receiver.target is! Field) {
+ throw "Table indexing not directly on a static field"
+ " at ${node.location}";
+ }
+ w.Table table = translator.getTable(receiver.target as Field)!;
+ codeGen.wrap(node.arguments.positional[0], w.NumType.i32);
+ if (name == '[]') {
+ b.table_get(table);
+ return table.type;
+ } else {
+ assert(name == '[]=');
+ codeGen.wrap(node.arguments.positional[1], table.type);
+ b.table_set(table);
+ return codeGen.voidMarker;
+ }
+ }
+
// List.[] on list constants
if (receiver is ConstantExpression &&
receiver.constant is ListConstant &&
@@ -974,6 +1005,60 @@
return null;
}
+ w.ValueType? generateFunctionCallIntrinsic(FunctionInvocation node) {
+ Expression receiver = node.receiver;
+
+ if (receiver is InstanceGet &&
+ receiver.interfaceTarget == translator.wasmFunctionCall) {
+ // Receiver is a WasmFunction
+ assert(receiver.name.text == "call");
+ w.RefType receiverType =
+ translator.translateType(dartTypeOf(receiver.receiver)) as w.RefType;
+ w.Local temp = codeGen.addLocal(receiverType);
+ codeGen.wrap(receiver.receiver, receiverType);
+ b.local_set(temp);
+ w.FunctionType functionType = receiverType.heapType as w.FunctionType;
+ assert(node.arguments.positional.length == functionType.inputs.length);
+ for (int i = 0; i < node.arguments.positional.length; i++) {
+ codeGen.wrap(node.arguments.positional[i], functionType.inputs[i]);
+ }
+ b.local_get(temp);
+ b.call_ref();
+ return translator.outputOrVoid(functionType.outputs);
+ }
+
+ if (receiver is InstanceInvocation &&
+ receiver.interfaceTarget == translator.wasmTableCallIndirect) {
+ // Receiver is a WasmTable.callIndirect
+ assert(receiver.name.text == "callIndirect");
+ Expression tableExp = receiver.receiver;
+ if (tableExp is! StaticGet || tableExp.target is! Field) {
+ throw "Table callIndirect not directly on a static field"
+ " at ${node.location}";
+ }
+ w.Table table = translator.getTable(tableExp.target as Field)!;
+ InterfaceType wasmFunctionType = InterfaceType(
+ translator.wasmFunctionClass,
+ Nullability.nonNullable,
+ [receiver.arguments.types.single]);
+ w.RefType receiverType =
+ translator.translateType(wasmFunctionType) as w.RefType;
+ w.Local tableIndex = codeGen.addLocal(w.NumType.i32);
+ codeGen.wrap(receiver.arguments.positional.single, w.NumType.i32);
+ b.local_set(tableIndex);
+ w.FunctionType functionType = receiverType.heapType as w.FunctionType;
+ assert(node.arguments.positional.length == functionType.inputs.length);
+ for (int i = 0; i < node.arguments.positional.length; i++) {
+ codeGen.wrap(node.arguments.positional[i], functionType.inputs[i]);
+ }
+ b.local_get(tableIndex);
+ b.call_indirect(functionType, table);
+ return translator.outputOrVoid(functionType.outputs);
+ }
+
+ return null;
+ }
+
bool generateMemberIntrinsic(Reference target, w.DefinedFunction function,
List<w.Local> paramLocals, w.Label? returnLabel) {
Member member = target.asMember;
diff --git a/pkg/dart2wasm/lib/translator.dart b/pkg/dart2wasm/lib/translator.dart
index 4fe9d4c..c130f70 100644
--- a/pkg/dart2wasm/lib/translator.dart
+++ b/pkg/dart2wasm/lib/translator.dart
@@ -76,6 +76,7 @@
late final Class wasmEqRefClass;
late final Class wasmDataRefClass;
late final Class wasmFunctionClass;
+ late final Class wasmTableClass;
late final Class boxedBoolClass;
late final Class boxedIntClass;
late final Class boxedDoubleClass;
@@ -112,6 +113,7 @@
late final Class typeErrorClass;
late final Class typeUniverseClass;
late final Procedure wasmFunctionCall;
+ late final Procedure wasmTableCallIndirect;
late final Procedure stackTraceCurrent;
late final Procedure stringEquals;
late final Procedure stringInterpolate;
@@ -143,6 +145,7 @@
final Map<Field, int> fieldIndex = {};
final Map<TypeParameter, int> typeParameterIndex = {};
final Map<Reference, ParameterInfo> staticParamInfo = {};
+ final Map<Field, w.DefinedTable> declaredTables = {};
late Procedure mainFunction;
late final w.Module m;
late final w.DefinedFunction initFunction;
@@ -194,6 +197,7 @@
wasmEqRefClass = lookupWasm("WasmEqRef");
wasmDataRefClass = lookupWasm("WasmDataRef");
wasmFunctionClass = lookupWasm("WasmFunction");
+ wasmTableClass = lookupWasm("WasmTable");
boxedBoolClass = lookupCore("_BoxedBool");
boxedIntClass = lookupCore("_BoxedInt");
boxedDoubleClass = lookupCore("_BoxedDouble");
@@ -232,6 +236,8 @@
byteDataViewClass = lookupTypedData("_ByteDataView");
wasmFunctionCall =
wasmFunctionClass.procedures.firstWhere((p) => p.name.text == "call");
+ wasmTableCallIndirect = wasmTableClass.procedures
+ .firstWhere((p) => p.name.text == "callIndirect");
stackTraceCurrent =
stackTraceClass.procedures.firstWhere((p) => p.name.text == "current");
stringEquals =
@@ -362,9 +368,7 @@
if (!options.printWasm) print("");
}
- if (exportName != null) {
- m.exportFunction(exportName, function);
- } else if (options.exportAll) {
+ if (options.exportAll && exportName == null) {
m.exportFunction(canonicalName, function);
}
var codeGen = CodeGenerator(this, function, reference);
@@ -734,6 +738,34 @@
}
}
+ /// Get the Wasm table declared by [field], or `null` if [field] is not a
+ /// declaration of a Wasm table.
+ ///
+ /// This function participates in tree shaking in the sense that if it's
+ /// never called for a particular table declaration, that table is not added
+ /// to the output module.
+ w.DefinedTable? getTable(Field field) {
+ w.DefinedTable? table = declaredTables[field];
+ if (table != null) return table;
+ DartType fieldType = field.type;
+ if (fieldType is InterfaceType && fieldType.classNode == wasmTableClass) {
+ w.RefType elementType =
+ translateType(fieldType.typeArguments.single) as w.RefType;
+ Expression sizeExp = (field.initializer as ConstructorInvocation)
+ .arguments
+ .positional
+ .single;
+ if (sizeExp is StaticGet && sizeExp.target is Field) {
+ sizeExp = (sizeExp.target as Field).initializer!;
+ }
+ int size = sizeExp is ConstantExpression
+ ? (sizeExp.constant as IntConstant).value
+ : (sizeExp as IntLiteral).value;
+ return declaredTables[field] = m.addTable(elementType, size);
+ }
+ return null;
+ }
+
Member? singleTarget(TreeNode node) {
DirectCallMetadataRepository metadata =
component.metadata[DirectCallMetadataRepository.repositoryTag]
diff --git a/pkg/vm/lib/transformations/pragma.dart b/pkg/vm/lib/transformations/pragma.dart
index fe33aed..cb8ffff 100644
--- a/pkg/vm/lib/transformations/pragma.dart
+++ b/pkg/vm/lib/transformations/pragma.dart
@@ -158,8 +158,8 @@
case kWasmEntryPointPragmaName:
return ParsedEntryPointPragma(PragmaEntryPointType.Default);
case kWasmExportPragmaName:
- // Exports are treated as called entry points.
- return ParsedEntryPointPragma(PragmaEntryPointType.CallOnly);
+ // Exports are treated as entry points.
+ return ParsedEntryPointPragma(PragmaEntryPointType.Default);
default:
return null;
}
diff --git a/pkg/wasm_builder/lib/src/instructions.dart b/pkg/wasm_builder/lib/src/instructions.dart
index beb2411..64fdd47 100644
--- a/pkg/wasm_builder/lib/src/instructions.dart
+++ b/pkg/wasm_builder/lib/src/instructions.dart
@@ -583,6 +583,32 @@
writeUnsigned(global.index);
}
+ // Table instructions
+
+ /// Emit a `table.get` instruction.
+ void table_get(Table table) {
+ assert(_verifyTypes(const [NumType.i32], [table.type],
+ trace: ['table.get', table.index]));
+ writeByte(0x25);
+ writeUnsigned(table.index);
+ }
+
+ /// Emit a `table.set` instruction.
+ void table_set(Table table) {
+ assert(_verifyTypes([NumType.i32, table.type], const [],
+ trace: ['table.set', table.index]));
+ writeByte(0x26);
+ writeUnsigned(table.index);
+ }
+
+ /// Emit a `table.size` instruction.
+ void table_size(Table table) {
+ assert(_verifyTypes(const [], const [NumType.i32],
+ trace: ['table.size', table.index]));
+ writeBytes([0xFC, 0x10]);
+ writeUnsigned(table.index);
+ }
+
// Memory instructions
void _writeMemArg(Memory memory, int offset, int align) {
diff --git a/sdk/lib/wasm/wasm_types.dart b/sdk/lib/wasm/wasm_types.dart
index ea09211..8139271 100644
--- a/sdk/lib/wasm/wasm_types.dart
+++ b/sdk/lib/wasm/wasm_types.dart
@@ -153,6 +153,32 @@
external F get call;
}
+/// A Wasm table.
+@pragma("wasm:entry-point")
+class WasmTable<T> {
+ /// Declare a table with the given size.
+ ///
+ /// Must be an initializer for a static field. The [size] argument must be
+ /// either a constant or a reference to a `static` `final` field with a
+ /// constant initializer.
+ external WasmTable(int size);
+
+ /// Read from an entry in the table.
+ external T operator [](WasmI32 index);
+
+ /// Write to an entry in the table.
+ external void operator []=(WasmI32 index, T value);
+
+ /// The size of the table.
+ external WasmI32 get size;
+
+ /// Call a function stored in the table using the `call_indirect` Wasm
+ /// instructionm. The function value returned from this method must be
+ /// called directly.
+ @pragma("wasm:entry-point")
+ external F callIndirect<F extends Function>(WasmI32 index);
+}
+
extension IntToWasmInt on int {
WasmI32 toWasmI32() => WasmI32.fromInt(this);
WasmI64 toWasmI64() => WasmI64.fromInt(this);
diff --git a/tests/web/wasm/table_test.dart b/tests/web/wasm/table_test.dart
new file mode 100644
index 0000000..fb6c31c
--- /dev/null
+++ b/tests/web/wasm/table_test.dart
@@ -0,0 +1,52 @@
+// 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:wasm';
+
+import 'package:expect/expect.dart';
+
+WasmTable<WasmFuncRef?> funcrefTable = WasmTable(3);
+WasmTable<WasmFunction<int Function(int)>?> funcTable = WasmTable(1);
+
+void f1() {}
+
+void f2(int x) {
+ Expect.equals(4, x);
+}
+
+int f3(int x) => x + 1;
+
+main() {
+ // Initialize untyped function table
+ Expect.equals(3, funcrefTable.size.toIntUnsigned());
+ funcrefTable[0.toWasmI32()] = WasmFunction.fromFunction(f1);
+ funcrefTable[1.toWasmI32()] = WasmFunction.fromFunction(f2);
+ funcrefTable[2.toWasmI32()] = WasmFunction.fromFunction(f3);
+
+ // Reading and calling functions in untyped function table
+ WasmFunction<void Function()>.fromRef(funcrefTable[0.toWasmI32()]!).call();
+ WasmFunction<void Function(int)>.fromRef(funcrefTable[1.toWasmI32()]!)
+ .call(4);
+ Expect.equals(
+ 6,
+ WasmFunction<int Function(int)>.fromRef(funcrefTable[2.toWasmI32()]!)
+ .call(5));
+
+ // Calling functions in untyped function table with callIndirect
+ funcrefTable.callIndirect<void Function()>(0.toWasmI32())();
+ funcrefTable.callIndirect<void Function(int)>(1.toWasmI32())(4);
+ Expect.equals(
+ 16, funcrefTable.callIndirect<int Function(int)>(2.toWasmI32())(15));
+
+ // Initialize typed function table
+ Expect.equals(1, funcTable.size.toIntUnsigned());
+ funcTable[0.toWasmI32()] = WasmFunction.fromFunction(f3);
+
+ // Reading and calling function in typed function table
+ Expect.equals(8, funcTable[0.toWasmI32()]!.call(7));
+
+ // Calling function in typed function table with callIndirect
+ Expect.equals(
+ 18, funcTable.callIndirect<int Function(int)>(0.toWasmI32())(17));
+}