blob: 984dd9542bde6058f8bd2769f6fc0408d178fbba [file] [edit]
//@ requireOptions("--useWasmTailCalls=true", "--wasmInliningMaximumWasmCalleeSize=0")
import { instantiate } from "../wabt-wrapper.js"
import * as assert from "../assert.js"
// Test cross-instance tail calls specifically exercising stack arguments (10+ params),
// indirect calls (call_ref via table), chained A→B→C, and deep recursion for tier-up.
// --- Test 1: Direct import with stack args ---
async function testDirectImportStackArgs() {
let watCallee = `
(module
(memory (export "mem") 1)
(data (i32.const 0) "\\2a\\00\\00\\00") ;; memory[0] = 42
(func (export "big_sum")
(param i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32) (result i32)
local.get 0 local.get 1 i32.add
local.get 2 i32.add local.get 3 i32.add
local.get 4 i32.add local.get 5 i32.add
local.get 6 i32.add local.get 7 i32.add
local.get 8 i32.add local.get 9 i32.add
local.get 10 i32.add local.get 11 i32.add
(i32.load (i32.const 0))
i32.add
)
)
`
let watCaller = `
(module
(import "m" "big_sum" (func $big_sum
(param i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32) (result i32)))
(memory (export "mem") 1)
(data (i32.const 0) "\\ff\\00\\00\\00") ;; memory[0] = 255
(func $tail_call_big_sum (export "tail_call_big_sum")
(param i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32) (result i32)
local.get 0 local.get 1 local.get 2 local.get 3
local.get 4 local.get 5 local.get 6 local.get 7
local.get 8 local.get 9 local.get 10 local.get 11
return_call $big_sum
)
;; Wrapper: call -> tail_call, then check our memory is restored.
(func (export "call_then_tail")
(param i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32) (result i32)
local.get 0 local.get 1 local.get 2 local.get 3
local.get 4 local.get 5 local.get 6 local.get 7
local.get 8 local.get 9 local.get 10 local.get 11
call $tail_call_big_sum
;; After return, our instance (caller) must be restored.
(i32.load (i32.const 0))
i32.add
)
)
`
const instCallee = await instantiate(watCallee, {}, { tail_call: true });
const instCaller = await instantiate(watCaller, {
m: { big_sum: instCallee.exports.big_sum }
}, { tail_call: true });
// sum(0..11) = 66, + callee memory[0]=42 => 108
for (let i = 0; i < wasmTestLoopCount; ++i) {
assert.eq(instCaller.exports.tail_call_big_sum(0,1,2,3,4,5,6,7,8,9,10,11), 108);
}
// call_then_tail: 108 + caller memory[0]=255 => 363
for (let i = 0; i < wasmTestLoopCount; ++i) {
assert.eq(instCaller.exports.call_then_tail(0,1,2,3,4,5,6,7,8,9,10,11), 363);
}
}
// --- Test 2: Indirect (call_ref via table) with stack args ---
async function testIndirectStackArgs() {
let watCallee = `
(module
(type $big (func (param i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32) (result i32)))
(memory (export "mem") 1)
(data (i32.const 0) "\\2a\\00\\00\\00") ;; memory[0] = 42
(func (export "big_sum") (type $big)
local.get 0 local.get 1 i32.add
local.get 2 i32.add local.get 3 i32.add
local.get 4 i32.add local.get 5 i32.add
local.get 6 i32.add local.get 7 i32.add
local.get 8 i32.add local.get 9 i32.add
local.get 10 i32.add local.get 11 i32.add
(i32.load (i32.const 0))
i32.add
)
)
`
let watCaller = `
(module
(type $big (func (param i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32) (result i32)))
(import "t" "table" (table 1 funcref))
(memory (export "mem") 1)
(data (i32.const 0) "\\ff\\00\\00\\00") ;; memory[0] = 255
;; Indirect tail call via table[0].
(func $indirect_tail (export "indirect_tail")
(param i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32) (result i32)
local.get 0 local.get 1 local.get 2 local.get 3
local.get 4 local.get 5 local.get 6 local.get 7
local.get 8 local.get 9 local.get 10 local.get 11
(i32.const 0)
(return_call_indirect (type $big))
)
;; Wrapper: call -> indirect tail call, then verify memory restored.
(func (export "call_then_indirect_tail")
(param i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32) (result i32)
local.get 0 local.get 1 local.get 2 local.get 3
local.get 4 local.get 5 local.get 6 local.get 7
local.get 8 local.get 9 local.get 10 local.get 11
call $indirect_tail
(i32.load (i32.const 0))
i32.add
)
)
`
const table = new WebAssembly.Table({ initial: 1, element: "funcref" });
const instCallee = await instantiate(watCallee, {}, { tail_call: true });
const instCaller = await instantiate(watCaller, { t: { table } }, { tail_call: true });
table.set(0, instCallee.exports.big_sum);
// sum(0..11) = 66, + callee memory[0]=42 => 108
for (let i = 0; i < wasmTestLoopCount; ++i) {
assert.eq(instCaller.exports.indirect_tail(0,1,2,3,4,5,6,7,8,9,10,11), 108);
}
// call_then_indirect_tail: 108 + caller memory[0]=255 => 363
for (let i = 0; i < wasmTestLoopCount; ++i) {
assert.eq(instCaller.exports.call_then_indirect_tail(0,1,2,3,4,5,6,7,8,9,10,11), 363);
}
}
// --- Test 3: Chain A→B→C with stack args (tests hasExistingThunk path) ---
async function testChainABC() {
let watC = `
(module
(memory (export "mem") 1)
(data (i32.const 0) "\\63\\00\\00\\00") ;; memory[0] = 99
(func (export "final_sum")
(param i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32) (result i32)
local.get 0 local.get 1 i32.add
local.get 2 i32.add local.get 3 i32.add
local.get 4 i32.add local.get 5 i32.add
local.get 6 i32.add local.get 7 i32.add
local.get 8 i32.add local.get 9 i32.add
local.get 10 i32.add local.get 11 i32.add
(i32.load (i32.const 0))
i32.add
)
)
`
let watB = `
(module
(import "c" "final_sum" (func $c_sum
(param i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32) (result i32)))
(memory (export "mem") 1)
(data (i32.const 0) "\\bb\\00\\00\\00") ;; memory[0] = 187
;; B tail-calls C second cross-instance hop, hasExistingThunk path.
(func (export "relay")
(param i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32) (result i32)
local.get 0 local.get 1 local.get 2 local.get 3
local.get 4 local.get 5 local.get 6 local.get 7
local.get 8 local.get 9 local.get 10 local.get 11
return_call $c_sum
)
)
`
let watA = `
(module
(import "b" "relay" (func $b_relay
(param i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32) (result i32)))
(memory (export "mem") 1)
(data (i32.const 0) "\\aa\\00\\00\\00") ;; memory[0] = 170
;; A tail-calls B first cross-instance hop.
(func $start_chain (export "start_chain")
(param i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32) (result i32)
local.get 0 local.get 1 local.get 2 local.get 3
local.get 4 local.get 5 local.get 6 local.get 7
local.get 8 local.get 9 local.get 10 local.get 11
return_call $b_relay
)
;; Wrapper with regular call so memory restoration is tested.
(func (export "call_start_chain")
(param i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32) (result i32)
local.get 0 local.get 1 local.get 2 local.get 3
local.get 4 local.get 5 local.get 6 local.get 7
local.get 8 local.get 9 local.get 10 local.get 11
call $start_chain
;; After return, A's instance must be restored.
(i32.load (i32.const 0))
i32.add
)
)
`
const instC = await instantiate(watC, {}, { tail_call: true });
const instB = await instantiate(watB, { c: { final_sum: instC.exports.final_sum } }, { tail_call: true });
const instA = await instantiate(watA, { b: { relay: instB.exports.relay } }, { tail_call: true });
// A -> B -> C: sum(0..11)=66 + C's memory[0]=99 => 165
for (let i = 0; i < wasmTestLoopCount; ++i) {
assert.eq(instA.exports.start_chain(0,1,2,3,4,5,6,7,8,9,10,11), 165);
}
// call_start_chain: 165 + A's memory[0]=170 => 335
for (let i = 0; i < wasmTestLoopCount; ++i) {
assert.eq(instA.exports.call_start_chain(0,1,2,3,4,5,6,7,8,9,10,11), 335);
}
}
// --- Test 4: Deep recursion with stack args (tier-up test) ---
async function testDeepRecursionStackArgs() {
let watPing = `
(module
(type $rec (func (param i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32) (result i32)))
(import "t" "table" (table 2 funcref))
(memory (export "mem") 1)
(data (i32.const 0) "\\2a\\00\\00\\00") ;; memory[0] = 42
(func $ping (export "ping") (type $rec)
(param i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32) (result i32)
(if (result i32) (i32.le_s (local.get 0) (i32.const 0))
(then
;; Base case: return sum of params 1..11 + memory[0]
local.get 1 local.get 2 i32.add
local.get 3 i32.add local.get 4 i32.add
local.get 5 i32.add local.get 6 i32.add
local.get 7 i32.add local.get 8 i32.add
local.get 9 i32.add local.get 10 i32.add
local.get 11 i32.add
(i32.load (i32.const 0))
i32.add
)
(else
;; Tail call pong (table[1]) with n-1
(i32.sub (local.get 0) (i32.const 1))
local.get 1 local.get 2 local.get 3
local.get 4 local.get 5 local.get 6
local.get 7 local.get 8 local.get 9
local.get 10 local.get 11
(i32.const 1)
(return_call_indirect (type $rec))
)
)
)
(func (export "start") (param i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32) (result i32)
local.get 0 local.get 1 local.get 2 local.get 3
local.get 4 local.get 5 local.get 6 local.get 7
local.get 8 local.get 9 local.get 10 local.get 11
call $ping
)
)
`
let watPong = `
(module
(type $rec (func (param i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32) (result i32)))
(import "t" "table" (table 2 funcref))
(memory (export "mem") 1)
(data (i32.const 0) "\\ff\\00\\00\\00") ;; memory[0] = 255
(func (export "pong") (type $rec)
(param i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32) (result i32)
(if (result i32) (i32.le_s (local.get 0) (i32.const 0))
(then
local.get 1 local.get 2 i32.add
local.get 3 i32.add local.get 4 i32.add
local.get 5 i32.add local.get 6 i32.add
local.get 7 i32.add local.get 8 i32.add
local.get 9 i32.add local.get 10 i32.add
local.get 11 i32.add
(i32.load (i32.const 0))
i32.add
)
(else
(i32.sub (local.get 0) (i32.const 1))
local.get 1 local.get 2 local.get 3
local.get 4 local.get 5 local.get 6
local.get 7 local.get 8 local.get 9
local.get 10 local.get 11
(i32.const 0)
(return_call_indirect (type $rec))
)
)
)
)
`
const table = new WebAssembly.Table({ initial: 2, element: "funcref" });
const instPing = await instantiate(watPing, { t: { table } }, { tail_call: true });
const instPong = await instantiate(watPong, { t: { table } }, { tail_call: true });
table.set(0, instPing.exports.ping);
table.set(1, instPong.exports.pong);
// sum(1..11) = 66, + memory[0]:
// Even n stops in ping (memory=42), odd n stops in pong (memory=255).
// sum(1..11) = 1+2+3+4+5+6+7+8+9+10+11 = 66
assert.eq(instPing.exports.start(0,1,2,3,4,5,6,7,8,9,10,11), 66 + 42); // n=0 base in ping
assert.eq(instPing.exports.start(1,1,2,3,4,5,6,7,8,9,10,11), 66 + 255); // n=1 base in pong
// Deep recursion (10000 iterations) — must not stack overflow.
assert.eq(instPing.exports.start(10000,1,2,3,4,5,6,7,8,9,10,11), 66 + 42); // even => ping
assert.eq(instPing.exports.start(10001,1,2,3,4,5,6,7,8,9,10,11), 66 + 255); // odd => pong
}
await assert.asyncTest(testDirectImportStackArgs());
await assert.asyncTest(testIndirectStackArgs());
await assert.asyncTest(testChainABC());
await assert.asyncTest(testDeepRecursionStackArgs());