diff --git a/pkg/dart2wasm/lib/dispatch_table.dart b/pkg/dart2wasm/lib/dispatch_table.dart index b2090e699fb..ce3391712e2 100644 --- a/pkg/dart2wasm/lib/dispatch_table.dart +++ b/pkg/dart2wasm/lib/dispatch_table.dart @@ -400,6 +400,10 @@ class DispatchTable { /// class member for the selector. late final List _table; + /// For direct calls across modules one can also use the existing table slots + /// in the dispatch table (instead of adding more slots to static call table). + late final Map _tableIndexForReference; + late final w.TableBuilder _definedWasmTable; late final WasmTableImporter _importedWasmTables = WasmTableImporter( translator, @@ -429,6 +433,13 @@ class DispatchTable { return _selectorInfo[selectorId]!; } + /// Returns a dispatch table index if the [target] is going to be in the + /// dispatch table. + /// + /// NOTE: The [target] can occur in multiple slots in the dispatch table and + /// we return the first such index. + int? indexForTarget(Reference target) => _tableIndexForReference[target]; + SelectorInfo _createSelectorForTarget(Reference target) { Member member = target.asMember; bool isGetter = target.isGetter || target.isTearOffReference; @@ -756,6 +767,12 @@ class DispatchTable { } _table = buildRowDisplacementTable(rows); + _tableIndexForReference = {}; + for (int i = 0; i < _table.length; ++i) { + final entry = _table[i]; + if (entry == null) continue; + _tableIndexForReference[entry] ??= i; + } int rowIndex = 0; for (final selector in selectors) { diff --git a/pkg/dart2wasm/lib/translator.dart b/pkg/dart2wasm/lib/translator.dart index b57cf2be533..7a2271e4142 100644 --- a/pkg/dart2wasm/lib/translator.dart +++ b/pkg/dart2wasm/lib/translator.dart @@ -765,10 +765,10 @@ class Translator with KernelNodes { outputs = b.inlineCallTo(callTarget); } else { b.comment('Not inlining, reason: ${decision.reason}'); - outputs = callFunction(callTarget.function, b); + outputs = callFunction(callTarget.function, b, reference); } } else { - outputs = callFunction(callTarget.function, b); + outputs = callFunction(callTarget.function, b, reference); } if (callTarget.synthesizeNullReturnValue) { assert(outputs.isEmpty); @@ -789,18 +789,37 @@ class Translator with KernelNodes { /// module. Otherwise does an indirect call through the static dispatch table. List callFunction( w.BaseFunction function, - w.InstructionsBuilder b, - ) { + w.InstructionsBuilder b, [ + Reference? target, + ]) { + // If the target function is defined in the same module as the caller, just + // invoke it. final targetModuleBuilder = moduleToBuilder[function.enclosingModule]!; if (targetModuleBuilder == b.moduleBuilder) { b.call(function); - } else { - b.i32_const(crossModuleFunctionTable.indexForFunction(function)); - b.call_indirect( - function.type, - crossModuleFunctionTable.getWasmTable(b.moduleBuilder), - ); + return b.emitUnreachableIfNoResult(function.type.outputs); } + + // If the target function is already available via the dispatch table, use + // it from there. + if (target != null) { + final dispatchTableIndex = dispatchTable.indexForTarget(target); + if (dispatchTableIndex != null) { + b.i32_const(dispatchTableIndex); + b.call_indirect( + function.type, + dispatchTable.getWasmTable(b.moduleBuilder), + ); + return b.emitUnreachableIfNoResult(function.type.outputs); + } + } + + // Otherwise add & call via the cross module function table. + b.i32_const(crossModuleFunctionTable.indexForFunction(function)); + b.call_indirect( + function.type, + crossModuleFunctionTable.getWasmTable(b.moduleBuilder), + ); return b.emitUnreachableIfNoResult(function.type.outputs); } diff --git a/pkg/dart2wasm/test/ir_tests/dispatch_table_reuse.dart b/pkg/dart2wasm/test/ir_tests/dispatch_table_reuse.dart new file mode 100644 index 00000000000..f1b7ea43653 --- /dev/null +++ b/pkg/dart2wasm/test/ir_tests/dispatch_table_reuse.dart @@ -0,0 +1,78 @@ +// Copyright (c) 2026, 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. + +// functionFilter=runTest +// typeFilter=NoMatch +// globalFilter=NoMatch +// compilerOption=--enable-deferred-loading + +import '' deferred as D; + +void main() async { + print('main($all)'); + all.forEach((x) => x.foo(always10)); + await D.loadLibrary(); + runTest(); +} + +@pragma('wasm:never-inline') +void runTest() { + final arg = always10; + for (final object in all) { + if (object is Sub1) { + object.foo(arg); + } else { + object.foo(arg); + } + } + print(D.all.toString()); +} + +final always10 = int.parse('10'); +final all = [Base(), Sub1(), Sub2(), Sub3(), Sub4(), Sub5()]; + +class Base { + void foo(int arg) { + print('Base.foo($arg)'); + } +} + +class Sub1 extends Base { + void foo(int arg) { + print('Sub1.foo($arg)'); + super.foo(arg); + } + + void bar(int arg) { + print('Sub1.bar($arg)'); + } +} + +class Sub2 extends Base { + void foo(int arg) { + print('Sub2.foo($arg)'); + super.foo(arg); + } +} + +class Sub4 extends Base { + void foo(int arg) { + print('Sub4.foo($arg)'); + super.foo(arg); + } +} + +class Sub5 extends Base { + void foo(int arg) { + print('Sub5.foo($arg)'); + super.foo(arg); + } +} + +class Sub3 extends Base { + void foo(int arg) { + print('Sub3.foo($arg)'); + super.foo(arg); + } +} diff --git a/pkg/dart2wasm/test/ir_tests/dispatch_table_reuse.wat b/pkg/dart2wasm/test/ir_tests/dispatch_table_reuse.wat new file mode 100644 index 00000000000..261787ca3c9 --- /dev/null +++ b/pkg/dart2wasm/test/ir_tests/dispatch_table_reuse.wat @@ -0,0 +1,2 @@ +(module $module0 +) \ No newline at end of file diff --git a/pkg/dart2wasm/test/ir_tests/dispatch_table_reuse_module1.wat b/pkg/dart2wasm/test/ir_tests/dispatch_table_reuse_module1.wat new file mode 100644 index 00000000000..aa1accc0a85 --- /dev/null +++ b/pkg/dart2wasm/test/ir_tests/dispatch_table_reuse_module1.wat @@ -0,0 +1,115 @@ +(module $module1 + (type $#Top <...>) + (type $Array <...>) + (type $ConcurrentModificationError <...>) + (type $JSExternWrapper <...>) + (type $Object <...>) + (type $WasmListBase <...>) + (type $_Type <...>) + (table $module0.cross-module-funcs-0 (import "module0" "cross-module-funcs-0") 12 funcref) + (table $module0.dispatch0 (import "module0" "dispatch0") 666 funcref) + (elem $module0.cross-module-funcs-0 + (set 0 (ref.func $"runTest "))) + (func $"runTest " + (local $var0 (ref $WasmListBase)) + (local $var1 (ref null $#Top)) + (local $var2 (ref $Object)) + (local $var3 (ref $_Type)) + (local $var4 i64) + (local $var5 i64) + (local $var6 i64) + i32.const 1 + call_indirect $module0.cross-module-funcs-0 (result i64) + local.set $var5 + i32.const 2 + call_indirect $module0.cross-module-funcs-0 (result (ref $WasmListBase)) + local.tee $var0 + struct.get $WasmListBase $field2 + local.set $var3 + local.get $var0 + struct.get $WasmListBase $_length + local.set $var6 + loop $label0 + block $label1 (result i32) + local.get $var6 + local.get $var0 + struct.get $WasmListBase $_length + i64.ne + if + local.get $var0 + i32.const 3 + call_indirect $module0.cross-module-funcs-0 (param (ref null $#Top)) (result (ref $ConcurrentModificationError)) + i32.const 4 + call_indirect $module0.cross-module-funcs-0 (param (ref $#Top)) + unreachable + end + local.get $var4 + local.get $var6 + i64.ge_s + if + ref.null none + local.set $var1 + i32.const 0 + br $label1 + end + local.get $var0 + struct.get $WasmListBase $_data + local.get $var4 + i32.wrap_i64 + array.get $Array + local.set $var1 + local.get $var4 + i64.const 1 + i64.add + local.set $var4 + i32.const 1 + end $label1 + if + local.get $var3 + struct.get $_Type $isDeclaredNullable + i32.const 1 + local.get $var1 + ref.is_null + select + i32.eqz + if + local.get $var1 + local.get $var3 + i32.const 5 + call_indirect $module0.cross-module-funcs-0 (param (ref null $#Top) (ref $_Type)) + end + local.get $var1 + ref.cast $Object + local.tee $var2 + struct.get $Object $field0 + i32.const 109 + i32.eq + if + local.get $var2 + local.get $var5 + i32.const 487 + call_indirect $module0.dispatch0 (param (ref $Object) i64) + else + local.get $var2 + local.get $var5 + local.get $var2 + struct.get $Object $field0 + i32.const 378 + i32.add + call_indirect $module0.dispatch0 (param (ref $Object) i64) + end + br $label0 + end + end $label0 + i64.const 0 + i32.const 6 + call_indirect $module0.cross-module-funcs-0 (param i64) (result i32) + drop + i32.const 2 + call_indirect $module0.cross-module-funcs-0 (result (ref $WasmListBase)) + i32.const 7 + call_indirect $module0.cross-module-funcs-0 (param (ref $Object)) (result (ref $JSExternWrapper)) + i32.const 8 + call_indirect $module0.cross-module-funcs-0 (param (ref null $#Top)) + ) +) \ No newline at end of file