diff --git a/src/engine/CodePtr.v3 b/src/engine/CodePtr.v3 index 76c722a8a..d6bf9cba9 100644 --- a/src/engine/CodePtr.v3 +++ b/src/engine/CodePtr.v3 @@ -157,6 +157,16 @@ class CodePtr extends DataReader { for (i < count) skip_catch(); return count; } + def skip_suspension_handler() { + var kind = read_uleb31(); + skip_leb(); // skip tag + if (kind == 0) skip_leb(); // skip label + } + def skip_suspension_handlers() -> u31 { + var count = read_uleb31(); + for (i < count) skip_suspension_handler(); + return count; + } def skip_handler() { skip_leb(); // tag skip_leb(); // depth/label diff --git a/src/engine/Sidetable.v3 b/src/engine/Sidetable.v3 index 05b4075ac..4f9cfb0c2 100644 --- a/src/engine/Sidetable.v3 +++ b/src/engine/Sidetable.v3 @@ -87,6 +87,9 @@ component Sidetables { var count = immptr.skip_catches(); size = count * Sidetable_CatchEntry.size; } + RESUME => size = computeResumeEntrySize(immptr); + RESUME_THROW => size = computeResumeEntrySize(immptr); + RESUME_THROW_REF => size = computeResumeEntrySize(immptr); BR_ON_NULL => size = Sidetable_BrEntry.size; BR_ON_NON_NULL => size = Sidetable_BrEntry.size; BR_ON_CAST => size = Sidetable_BrEntry.size; @@ -95,6 +98,12 @@ component Sidetables { } return size; } + // Compute the size (in bytes) of the sidetable entries for a resume opcode. + private def computeResumeEntrySize(immptr: CodePtr) -> int { + immptr.skip_leb(); // skip continuation type index + var count = immptr.skip_suspension_handlers(); + return Sidetable_ResumeEntry.size + count * Sidetable_CatchEntry.size; + } } // Implements an efficient mapping from program counter to sidetable position. diff --git a/src/engine/compression/Compression.v3 b/src/engine/compression/Compression.v3 index e6473a874..2fba231a7 100644 --- a/src/engine/compression/Compression.v3 +++ b/src/engine/compression/Compression.v3 @@ -6,8 +6,9 @@ type WasmFrameData(func: WasmFunction, pc: int, ret_addr: u64, vals: Array) #unboxed { } // Relocatable representation of a stack frame that can be serialized. -// {bytecode_ip} is unused when {is_spc} is true. -type RelocatableFrame(data: WasmFrameData, is_spc: bool, bytecode_ip: u64) #unboxed { } +// {bytecode_offset} is the byte offset into {data.func.decl.cur_bytecode} at which the interpreter +// resumes (unused when {is_spc} is true). +type RelocatableFrame(data: WasmFrameData, is_spc: bool, bytecode_offset: int) #unboxed { } class CompressionStrategy { def compress(frames: Range) -> C; diff --git a/src/engine/compression/PackedCompressionStrategy.v3 b/src/engine/compression/PackedCompressionStrategy.v3 index ddf40d93b..2704a69f2 100644 --- a/src/engine/compression/PackedCompressionStrategy.v3 +++ b/src/engine/compression/PackedCompressionStrategy.v3 @@ -1,7 +1,7 @@ // Copyright 2026 Wizard authors. All rights reserved. // See LICENSE for details of Apache 2.0 license. -type FrameHeader(func: WasmFunction, pc: int, is_spc: bool, ret_addr: u64, bytecode_ip: u64, n_vals: int) #unboxed {} +type FrameHeader(func: WasmFunction, pc: int, is_spc: bool, ret_addr: u64, bytecode_offset: int, n_vals: int) #unboxed {} class PackedCompressionStrategy extends CompressionStrategy { def frames_builder = Vector.new(); @@ -32,7 +32,7 @@ class PackedCompressionStrategy extends CompressionStrategy) -> PackedCompressedStack { for (frame in frames) { - var header = FrameHeader(frame.data.func, frame.data.pc, frame.is_spc, frame.data.ret_addr, frame.bytecode_ip, frame.data.vals.length); + var header = FrameHeader(frame.data.func, frame.data.pc, frame.is_spc, frame.data.ret_addr, frame.bytecode_offset, frame.data.vals.length); frames_builder.put(header); for (v in frame.data.vals) writeValue(v); } @@ -71,7 +71,7 @@ class PackedCompressionStrategy extends CompressionStrategy.new(header.n_vals); for (i < header.n_vals) vals[i] = readValue(comp); - builder.put(RelocatableFrame(WasmFrameData(header.func, header.pc, header.ret_addr, vals), header.is_spc, header.bytecode_ip)); + builder.put(RelocatableFrame(WasmFrameData(header.func, header.pc, header.ret_addr, vals), header.is_spc, header.bytecode_offset)); } return builder.extract(); } diff --git a/src/engine/x86-64/X86_64Frames.v3 b/src/engine/x86-64/X86_64Frames.v3 index 6c68543ec..d7805efbc 100644 --- a/src/engine/x86-64/X86_64Frames.v3 +++ b/src/engine/x86-64/X86_64Frames.v3 @@ -152,9 +152,7 @@ type X86_64FrameHandle #unboxed { (p + X86_64InterpreterFrame.code.offset).store>(bytecode); (p + X86_64InterpreterFrame.ip.offset).store(Pointer.atElement(bytecode, new_pc)); (p + X86_64InterpreterFrame.eip.offset).store(Pointer.atContents(bytecode) + bytecode.length); - var st_entries = func.sidetable.entries; - var st_ptr = if(stp_val == st_entries.length, Pointer.atContents(st_entries), Pointer.atElement(func.sidetable.entries, stp_val)); - (p + X86_64InterpreterFrame.stp.offset).store(st_ptr); + (p + X86_64InterpreterFrame.stp.offset).store(X86_64Frames.sidetablePtr(func.sidetable.entries, stp_val)); } // Redirect return address to the interpreter's deopt reentry point. @@ -186,6 +184,10 @@ component X86_64Frames { def fromSfp(sfp: Pointer) -> Ref { return Ref.of(CiRuntime.forgeRange(sfp, X86_64InterpreterFrame.size)); } + // Compute the address of sidetable entry {stp}, counted in {int}s from the first entry. + def sidetablePtr(entries: Array, stp: int) -> Pointer { + return Pointer.atContents(entries) + stp * 4; + } } // Native frame states used in the implementation of {FrameStateAccessor}. Since a frame diff --git a/src/engine/x86-64/X86_64Stack.v3 b/src/engine/x86-64/X86_64Stack.v3 index b0b0777c1..9d431f62d 100644 --- a/src/engine/x86-64/X86_64Stack.v3 +++ b/src/engine/x86-64/X86_64Stack.v3 @@ -45,17 +45,24 @@ class X86_64Stack extends WasmStack { return state_; } // Requires {state == EMPTY}. - // Resets this stack to be {SUSPENDED}, awaiting arguments for {func}. - def reset(func: Function) -> this { + def reset0(func: Function, remaining_params: int) -> this { checkState("reset()", StackState.EMPTY); this.func = func; - params_arity = func.sig.params.length; + params_arity = remaining_params; return_results = func.sig.results; state_ = if(params_arity == 0, StackState.RESUMABLE, StackState.SUSPENDED); parent_rsp_ptr = pushRspPointer(Pointer.NULL); // placeholder for parent pointer pushRspPointer(STACK_RETURN_PARENT_STUB.getEntry() + NOP_ENTRY); + } + // Requires {state == EMPTY}. + def reset(func: Function) -> this { + reset0(func, func.sig.params.length); pushRspPointer(STACK_ENTER_FUNC_STUB.getEntry() + NOP_ENTRY); } + // Requires {state == EMPTY}. + def resetForFrameWrites(func: Function) -> this { + reset0(func, 0); + } // Requires {state == SUSPENDED}. // Pushes {args} incrementally onto the value stack and transitions to {state == RESUMABLE} // when enough arguments are pushed. @@ -340,6 +347,7 @@ class X86_64Stack extends WasmStack { params_arity = -1; return_results = null; parent = null; + cont_bottom = null; parent_rsp_ptr = Pointer.NULL; func = null; } @@ -860,10 +868,10 @@ def genStackReturnParentStub(ic: X86_64InterpreterCode, w: DataWriter) { masm.emit_mov_m_l(MasmAddr(r_stack, masm.offsets.X86_64Stack_parent), 0); // mov [%stack.parent_rsp_ptr], nullptr masm.emit_mov_m_l(MasmAddr(r_stack, masm.offsets.X86_64Stack_parent_rsp_ptr), 0); - + // l_return: masm.bindLabel(l_return); - + // recycle %stack (already set at this point) masm.emit_mov_m_m( ValueKind.REF, @@ -871,7 +879,7 @@ def genStackReturnParentStub(ic: X86_64InterpreterCode, w: DataWriter) { MasmAddr(Reg(0), int.!(masm.offsets.X86_64StackManager_cache - Pointer.NULL)) ); masm.emit_mov_m_r(ValueKind.REF, MasmAddr(Reg(0), int.!(masm.offsets.X86_64StackManager_cache - Pointer.NULL)), r_stack); - + // mov [cur_stack], %parent masm.emit_set_curstack(r_parent); // pop %rsp diff --git a/src/engine/x86-64/X86_64StackCompression.v3 b/src/engine/x86-64/X86_64StackCompression.v3 new file mode 100644 index 000000000..c673dba06 --- /dev/null +++ b/src/engine/x86-64/X86_64StackCompression.v3 @@ -0,0 +1,142 @@ +// Copyright 2026 Wizard authors. All rights reserved. +// See LICENSE for details of Apache 2.0 license. + +def valuerep = Target.tagging; + +component X86_64Compression { + + def readFrames(stack: X86_64Stack) -> Array { + var collector = StackFrameCollector.new(); + stack.walk(collector.visitFrame, void, stack.rsp, false); + + var result = Vector.new(); + // {collector.frames} is in walk order: index 0 is the topmost frame. + for (i = collector.frames.length - 1; i >= 0; i--) { + var next_vfp = if(i == 0, stack.vsp, collector.frames[i-1].getFrameAccessorOfStack(stack).handle.vfp()); + result.put(loadTargetFrame(stack, collector.frames[i], next_vfp)); + } + return result.extract(); + } + + // Requires {to.state() == EMPTY}. + def writeFrames(to: X86_64Stack, frames: Array) { + if (to.state() != StackState.EMPTY) { + to.fatal(Strings.format1("writeFrames() requires state == EMPTY, got %s", to.state().name)); + } + if (frames.length == 0) return; // nothing to install; leave {to} empty + var root_func = frames[0].data.func; + var instance = root_func.instance; + to.resetForFrameWrites(instance.functions[root_func.decl.func_index]); + to.cont_bottom = to; + + for (f in frames) { + // The sidetable lookup allocates, so it must run before {rsp} moves over the new frame (due to GC). + var stp = if(f.is_spc, 0, SidetableMap.new(f.data.func.decl)[f.data.pc]); + + // Interpreter and SPC frames share the 104-byte footprint. + to.rsp += -X86_64InterpreterFrame.size; + var h: X86_64FrameHandle; + if (f.is_spc) { + var sh = X86_64FrameHandle.Spc(to.rsp); + setSpcFrameContext(sh, f.data.func, f.data.pc); + h = sh; + } else { + var ih = X86_64FrameHandle.Interpreter(to.rsp); + setFrameContext(ih, f.data.func); + setNewProgramLocation(ih, f.data.func.decl, f.data.pc, stp, f.bytecode_offset); + h = ih; + } + + // Push the value slots and bracket with vfp/vsp. + h.set_vfp(to.vsp); + for (val in f.data.vals) to.push(val); + h.set_vsp(to.vsp); + + // SPC resumes at the call-site continuation, interpreter resumes at its dispatch reentry. + to.rsp += -Pointer.SIZE; + to.rsp.store(Pointer.NULL + i64.view(f.data.ret_addr)); + } + } + + // {stack} must be the stack that {frame} lives on; the frame accessor reads values through it. + def loadTargetFrame(stack: X86_64Stack, frame: TargetFrame, next_vfp: Pointer) -> RelocatableFrame { + var accessor = frame.getFrameAccessorOfStack(stack); + var func = accessor.func(); + var pc = accessor.pc(); + var ret_addr = frame.srp().load(); + var is_spc = X86_64SpcCode.?(RiRuntime.findUserCode(ret_addr)); + + // {bytecode_offset} is interpreter-only; SPC derives its pc from the return address. + var h: X86_64FrameHandle; + var bytecode_offset = 0; + if (is_spc) { + h = X86_64FrameHandle.Spc(accessor.sfp()); + } else { + var ih = X86_64FrameHandle.Interpreter(accessor.sfp()); + h = ih; + bytecode_offset = int.!(ih.ip() - Pointer.atContents(ih.code())); + } + + var values = Vector.new(); + var offset = 0; + for (this_vfp = h.vfp(); this_vfp < next_vfp; this_vfp += valuerep.slot_size) { + values.put(accessor.getValue(offset)); + offset++; + } + + var data = WasmFrameData(func, pc, u64.view(ret_addr - Pointer.NULL), values.extract()); + return RelocatableFrame(data, is_spc, bytecode_offset); + } + + def setFrameContext(h: X86_64FrameHandle.Interpreter, wf: WasmFunction) { + var module = wf.instance.module; + h.set_wasm_func(wf); + h.set_instance(wf.instance); + h.set_sidetable(wf.decl.sidetable.entries); + h.set_accessor(null); + if (module.memories.length > 0) { + var memory = NativeWasmMemory.!(wf.instance.memories[0]); + h.set_mem0_base(memory.start); + } + } + + // SPC layout has no sidetable slot, so it is omitted here. + def setSpcFrameContext(h: X86_64FrameHandle.Spc, wf: WasmFunction, pc: int) { + var module = wf.instance.module; + h.set_wasm_func(wf); + h.set_instance(wf.instance); + h.writeSpcState(pc); + // Cleared to stay consistent with SPC prologue (and avoid GC). + h.set_inlined_instance(null); + if (module.memories.length > 0) { + var memory = NativeWasmMemory.!(wf.instance.memories[0]); + h.set_mem0_base(memory.start); + } + } + + // {bytecode_offset} is the byte offset into {func.cur_bytecode} at which this frame resumes. + def setNewProgramLocation(h: X86_64FrameHandle.Interpreter, func: FuncDecl, pc: int, stp: int, bytecode_offset: int) { + var code = func.cur_bytecode; + if (u32.view(bytecode_offset) > u32.view(code.length)) { + System.error("StackCompressionError", "bytecode offset out of bounds"); + } + h.set_func_decl(func); + h.set_curpc(pc); + h.set_code(code); + h.set_ip(Pointer.atContents(code) + bytecode_offset); + h.set_eip(Pointer.atContents(code) + code.length); + h.set_stp(X86_64Frames.sidetablePtr(func.sidetable.entries, stp)); + } +} + +// Visitor used by {X86_64Compression.readFrames} to gather module frames. +class StackFrameCollector { + def frames = Vector.new(); + + def reset() { frames.clear(); } + def visitFrame(p: Pointer, c: RiUserCode, pos: StackFramePos, v: void) -> bool { + // TODO: handle X86_64SpcInlinedFrame and X86_64SpcTrapsStub. + if (X86_64InterpreterCode.?(c) || X86_64SpcModuleCode.?(c)) frames.put(pos.frame); + return true; + } +} diff --git a/src/engine/x86-64/X86_64Target.v3 b/src/engine/x86-64/X86_64Target.v3 index 38dededaf..5ba77391f 100644 --- a/src/engine/x86-64/X86_64Target.v3 +++ b/src/engine/x86-64/X86_64Target.v3 @@ -160,11 +160,14 @@ component Target { return RedZones.addRedZone(mapping, offset, size); } - // TODO[sc]: empty function stubs for stack compression (real impl lands with the x86-64 backend). // Stack compression: read the frames of a stack into an array of target independent frames representation. - def readFramesFromStack(from: WasmStack) -> Array; + def readFramesFromStack(from: WasmStack) -> Array { + return X86_64Compression.readFrames(X86_64Stack.!(from)); + } // Stack compression: overwrite the destination stack with the content of the relocatable frames. - def writeFramesToStack(dest: WasmStack, frames: Array); + def writeFramesToStack(dest: WasmStack, frames: Array) { + X86_64Compression.writeFrames(X86_64Stack.!(dest), frames); + } } type TargetOsrInfo(spc_entry: Pointer, osr_entries: List<(int, int)>) #unboxed { } diff --git a/test/unittest/CompressionTest.v3 b/test/unittest/CompressionTest.v3 index 434cd4a71..ec03c2569 100644 --- a/test/unittest/CompressionTest.v3 +++ b/test/unittest/CompressionTest.v3 @@ -51,10 +51,10 @@ def X_ = registerAll([ ("ret_addr_max", test_ret_addr_max), ("ret_addr_alternating_bits", test_ret_addr_alternating_bits), ("ret_addr_sequential", test_ret_addr_sequential), - ("bytecode_ip_zero", test_bytecode_ip_zero), - ("bytecode_ip_max", test_bytecode_ip_max), - ("bytecode_ip_alternating_bits", test_bytecode_ip_alternating_bits), - ("bytecode_ip_sequential", test_bytecode_ip_sequential), + ("bytecode_offset_zero", test_bytecode_offset_zero), + ("bytecode_offset_max", test_bytecode_offset_max), + ("bytecode_offset_alternating_bits", test_bytecode_offset_alternating_bits), + ("bytecode_offset_sequential", test_bytecode_offset_sequential), ("is_spc_true_single", test_is_spc_true_single), ("is_spc_all_spc", test_is_spc_all_spc), ("is_spc_alternating", test_is_spc_alternating), @@ -92,13 +92,13 @@ class CompressionTester(t: Tester, adapter: CompressionStrategyAdapter) { return dummy_func; } - def make_frame_full(pc: int, vals: Array, ret_addr: u64, bytecode_ip: u64, is_spc: bool) -> RelocatableFrame { + def make_frame_full(pc: int, vals: Array, ret_addr: u64, bytecode_offset: int, is_spc: bool) -> RelocatableFrame { var func = getDummyFunc(); - return RelocatableFrame(WasmFrameData(func, pc, ret_addr, vals), is_spc, bytecode_ip); + return RelocatableFrame(WasmFrameData(func, pc, ret_addr, vals), is_spc, bytecode_offset); } def make_frame(pc: int, vals: Array) -> RelocatableFrame { - return make_frame_full(pc, vals, 0xCAFEBABEDEADBEEFuL ^ u64.view(pc), 0x1234567890ABCDEFuL ^ ~u64.view(pc), false); + return make_frame_full(pc, vals, 0xCAFEBABEDEADBEEFuL ^ u64.view(pc), 0x12345678 ^ ~pc, false); } def roundtrip(frames: Array) -> Array { @@ -112,11 +112,31 @@ class CompressionTester(t: Tester, adapter: CompressionStrategyAdapter) { } def assert_length(expected: int, got: int, what: string) { - if (expected != got) t.fail3("expected %d %s, got %d", expected, what, got); + FrameAsserts.assert_length(t, expected, got, what); } def assert_vals_eq(expected: Array, got: Array) { - assert_length(expected.length, got.length, "values"); + FrameAsserts.assert_vals_eq(t, expected, got); + } + + def assert_frames_eq(expected: Array, got: Array) { + FrameAsserts.assert_frames_eq(t, expected, got); + } +} + +// Assertions over {RelocatableFrame} arrays. +component FrameAsserts { + // Returns {true} if the lengths match. + def assert_length(t: Tester, expected: int, got: int, what: string) -> bool { + if (expected != got) { + t.fail3("expected %d %s, got %d", expected, what, got); + return false; + } + return true; + } + + def assert_vals_eq(t: Tester, expected: Array, got: Array) { + if (!assert_length(t, expected.length, got.length, "values")) return; for (i < expected.length) { if (expected[i] != got[i]) { t.fail3("value[%d]: expected %q, got %q", @@ -125,16 +145,16 @@ class CompressionTester(t: Tester, adapter: CompressionStrategyAdapter) { } } - def assert_frames_eq(expected: Array, got: Array) { - assert_length(expected.length, got.length, "frames"); + def assert_frames_eq(t: Tester, expected: Array, got: Array) { + if (!assert_length(t, expected.length, got.length, "frames")) return; for (i < expected.length) { var e = expected[i], g = got[i]; if (e.data.func != g.data.func) t.fail1("frame[%d]: func mismatch", i); if (e.data.pc != g.data.pc) t.fail3("frame[%d]: expected pc=%d, got pc=%d", i, e.data.pc, g.data.pc); if (e.is_spc != g.is_spc) t.fail3("frame[%d]: expected is_spc=%z, got is_spc=%z", i, e.is_spc, g.is_spc); if (e.data.ret_addr != g.data.ret_addr) t.fail3("frame[%d]: expected ret_addr=0x%x, got ret_addr=0x%x", i, e.data.ret_addr, g.data.ret_addr); - if (e.bytecode_ip != g.bytecode_ip) t.fail3("frame[%d]: expected bytecode_ip=0x%x, got bytecode_ip=0x%x", i, e.bytecode_ip, g.bytecode_ip); - assert_vals_eq(e.data.vals, g.data.vals); + if (e.bytecode_offset != g.bytecode_offset) t.fail3("frame[%d]: expected bytecode_offset=%d, got bytecode_offset=%d", i, e.bytecode_offset, g.bytecode_offset); + assert_vals_eq(t, e.data.vals, g.data.vals); } } } @@ -261,7 +281,7 @@ def test_cont(t: CompressionTester) { def test_frame_metadata(t: CompressionTester) { var func = t.getDummyFunc(); - var frame = RelocatableFrame(WasmFrameData(func, 42, 0xFEEDFACECAFEBEEFuL, [Value.I32(99)]), false, 0xABCDEF0123456789uL); + var frame = RelocatableFrame(WasmFrameData(func, 42, 0xFEEDFACECAFEBEEFuL, [Value.I32(99)]), false, 0x01234567); var got = t.roundtrip([frame]); t.assert_length(1, got.length, "frames"); if (got[0].data.func != func) t.t.fail("func not preserved"); @@ -403,30 +423,24 @@ def test_large_uniform_frames(t: CompressionTester) { t.assert_frames_eq(frames, got); } -// ===== Peripheral fuzz tests ===== -// These verify that ret_addr, bytecode_ip, and is_spc round-trip without -// corruption across both strategies. assert_frames_eq now compares all -// fields, so any cross-field swap, byte-order issue, or sign-extension bug -// in either strategy will surface here. - -// {ret_addr} fuzz tests. +// ===== Fuzz tests ===== def test_ret_addr_zero(t: CompressionTester) { - var frames = [t.make_frame_full(0, [Value.I32(0)], 0uL, 0uL, false)]; + var frames = [t.make_frame_full(0, [Value.I32(0)], 0uL, 0, false)]; var got = t.roundtrip(frames); t.assert_frames_eq(frames, got); } def test_ret_addr_max(t: CompressionTester) { - var frames = [t.make_frame_full(0, [Value.I32(0)], 0xFFFFFFFFFFFFFFFFuL, 0uL, false)]; + var frames = [t.make_frame_full(0, [Value.I32(0)], 0xFFFFFFFFFFFFFFFFuL, 0, false)]; var got = t.roundtrip(frames); t.assert_frames_eq(frames, got); } def test_ret_addr_alternating_bits(t: CompressionTester) { var frames = [ - t.make_frame_full(0, [Value.I32(0)], 0xAAAAAAAAAAAAAAAAuL, 0uL, false), - t.make_frame_full(1, [Value.I32(1)], 0x5555555555555555uL, 0uL, false) + t.make_frame_full(0, [Value.I32(0)], 0xAAAAAAAAAAAAAAAAuL, 0, false), + t.make_frame_full(1, [Value.I32(1)], 0x5555555555555555uL, 0, false) ]; var got = t.roundtrip(frames); t.assert_frames_eq(frames, got); @@ -437,50 +451,46 @@ def test_ret_addr_sequential(t: CompressionTester) { var frames = Array.new(n); for (i < n) { var ret_addr = u64.view(i) * 0x0123456789ABCDEFuL; - frames[i] = t.make_frame_full(i, [Value.I32(u32.view(i))], ret_addr, 0uL, false); + frames[i] = t.make_frame_full(i, [Value.I32(u32.view(i))], ret_addr, 0, false); } var got = t.roundtrip(frames); t.assert_frames_eq(frames, got); } -// {bytecode_ip} fuzz tests (mirror of ret_addr). - -def test_bytecode_ip_zero(t: CompressionTester) { - var frames = [t.make_frame_full(0, [Value.I32(0)], 0uL, 0uL, false)]; +def test_bytecode_offset_zero(t: CompressionTester) { + var frames = [t.make_frame_full(0, [Value.I32(0)], 0uL, 0, false)]; var got = t.roundtrip(frames); t.assert_frames_eq(frames, got); } -def test_bytecode_ip_max(t: CompressionTester) { - var frames = [t.make_frame_full(0, [Value.I32(0)], 0uL, 0xFFFFFFFFFFFFFFFFuL, false)]; +def test_bytecode_offset_max(t: CompressionTester) { + var frames = [t.make_frame_full(0, [Value.I32(0)], 0uL, int.max, false)]; var got = t.roundtrip(frames); t.assert_frames_eq(frames, got); } -def test_bytecode_ip_alternating_bits(t: CompressionTester) { +def test_bytecode_offset_alternating_bits(t: CompressionTester) { var frames = [ - t.make_frame_full(0, [Value.I32(0)], 0uL, 0xAAAAAAAAAAAAAAAAuL, false), - t.make_frame_full(1, [Value.I32(1)], 0uL, 0x5555555555555555uL, false) + t.make_frame_full(0, [Value.I32(0)], 0uL, 0x2AAAAAAA, false), + t.make_frame_full(1, [Value.I32(1)], 0uL, 0x55555555, false) ]; var got = t.roundtrip(frames); t.assert_frames_eq(frames, got); } -def test_bytecode_ip_sequential(t: CompressionTester) { +def test_bytecode_offset_sequential(t: CompressionTester) { var n = 50; var frames = Array.new(n); for (i < n) { - var bytecode_ip = u64.view(i) * 0x0123456789ABCDEFuL; - frames[i] = t.make_frame_full(i, [Value.I32(u32.view(i))], 0uL, bytecode_ip, false); + var bytecode_offset = i * 0x01234567; + frames[i] = t.make_frame_full(i, [Value.I32(u32.view(i))], 0uL, bytecode_offset, false); } var got = t.roundtrip(frames); t.assert_frames_eq(frames, got); } -// {is_spc} fuzz tests. - def test_is_spc_true_single(t: CompressionTester) { - var frames = [t.make_frame_full(0, [Value.I32(42)], 0uL, 0uL, true)]; + var frames = [t.make_frame_full(0, [Value.I32(42)], 0uL, 0, true)]; var got = t.roundtrip(frames); t.assert_frames_eq(frames, got); } @@ -489,7 +499,7 @@ def test_is_spc_all_spc(t: CompressionTester) { var n = 20; var frames = Array.new(n); for (i < n) { - frames[i] = t.make_frame_full(i, [Value.I32(u32.view(i))], 0uL, 0uL, true); + frames[i] = t.make_frame_full(i, [Value.I32(u32.view(i))], 0uL, 0, true); } var got = t.roundtrip(frames); t.assert_frames_eq(frames, got); @@ -500,20 +510,18 @@ def test_is_spc_alternating(t: CompressionTester) { var frames = Array.new(n); for (i < n) { var is_spc = (i & 1) == 0; - frames[i] = t.make_frame_full(i, [Value.I32(u32.view(i))], 0uL, 0uL, is_spc); + frames[i] = t.make_frame_full(i, [Value.I32(u32.view(i))], 0uL, 0, is_spc); } var got = t.roundtrip(frames); t.assert_frames_eq(frames, got); } -// All peripherals varying together. Catches cross-field corruption like -// accidentally swapping the ret_addr and bytecode_ip slots. def test_peripherals_combined_fuzz(t: CompressionTester) { var n = 30; var frames = Array.new(n); for (i < n) { var ret_addr = u64.view(i) * 0xCAFEBABECAFEBABEuL; - var bytecode_ip = u64.view(~i) * 0xDEADBEEFDEADBEEFuL; + var bytecode_offset = ~i * 0x0DEADBEE; var is_spc = (i % 3) != 0; var n_vals = (i % 4) + 1; var vals = Array.new(n_vals); @@ -525,7 +533,7 @@ def test_peripherals_combined_fuzz(t: CompressionTester) { _ => vals[j] = Value.F64(u64.view(i) * 0x1111uL + u64.view(j)); } } - frames[i] = t.make_frame_full(i * 2, vals, ret_addr, bytecode_ip, is_spc); + frames[i] = t.make_frame_full(i * 2, vals, ret_addr, bytecode_offset, is_spc); } var got = t.roundtrip(frames); t.assert_frames_eq(frames, got); diff --git a/test/unittest/SidetableTest.v3 b/test/unittest/SidetableTest.v3 index 3a913d18d..7a3533e42 100644 --- a/test/unittest/SidetableTest.v3 +++ b/test/unittest/SidetableTest.v3 @@ -13,6 +13,7 @@ def X_ = void( T("br_table0", test_br_table0), T("try_table0", test_try_table0), T("br_loop_val", test_br_loop_val), + T("resume_entry_size", test_resume_entry_size), () ); @@ -267,4 +268,21 @@ def test_br_loop_val(t: SidetableTester) { BR, 0, END]); t.assert(5, -4, 0, 1, 0); -} \ No newline at end of file +} + +def test_resume_entry_size(t: SidetableTester) { + var min = Sidetable_ResumeEntry.size; + // A zero-handler immediate: continuation type index 0, then a handler count of 0. + var got_resume = Sidetables.computeEntrySize(Opcode.RESUME, CodePtr.new(Array.new(4))); + if (got_resume < min) { + t.t.fail2("computeEntrySize(resume): expected at least %d, got %d", min, got_resume); + } + var got_throw = Sidetables.computeEntrySize(Opcode.RESUME_THROW, CodePtr.new(Array.new(4))); + if (got_throw < min) { + t.t.fail2("computeEntrySize(resume_throw): expected at least %d, got %d", min, got_throw); + } + var got_throw_ref = Sidetables.computeEntrySize(Opcode.RESUME_THROW_REF, CodePtr.new(Array.new(4))); + if (got_throw_ref < min) { + t.t.fail2("computeEntrySize(resume_throw_ref): expected at least %d, got %d", min, got_throw_ref); + } +} diff --git a/test/unittest/x86-64-linux/X86_64CompressionTest.v3 b/test/unittest/x86-64-linux/X86_64CompressionTest.v3 new file mode 100644 index 000000000..cd6bb7ffd --- /dev/null +++ b/test/unittest/x86-64-linux/X86_64CompressionTest.v3 @@ -0,0 +1,566 @@ +// Copyright 2026 Wizard authors. All rights reserved. +// See LICENSE for details of Apache 2.0 license. + +def NAIVE = NaiveCompressionStrategy.new(); +def PACKED = PackedCompressionStrategy.new(); + +// A heap object for ref-typed value slots, built as in {CompressionTest.v3}. +def EMPTY_STRUCT = StructDecl.new(false, ValueTypes.NO_HEAPTYPES, []); +def newObject = ObjectModel.newStructFromValues(EMPTY_STRUCT, _); + +def newIntTester(t: Tester) -> X86_64CompressionTester { + return X86_64CompressionTester.new(t, X86_64InterpreterOnlyStrategy.new()); +} +def newSpcTester(t: Tester) -> X86_64CompressionTester { + return X86_64CompressionTester.new(t, X86_64SpcAotStrategy.new(false)); +} + +def T = UnitTests.registerT("x86compress:", _, newIntTester, _); +def TS = UnitTests.registerT("x86compress:spc:", _, newSpcTester, _); +def X_ = void( + T("empty_frames", test_empty_frames), + T("single_empty", test_single_empty), + T("single_val", test_single_val), + T("two_frames", test_two_frames), + T("three_frames", test_three_frames), + T("varying_vals", test_varying_vals), + T("many_frames", test_many_frames), + T("many_vals", test_many_vals), + T("pc_variety", test_pc_variety), + T("multi_func", test_multi_func), + T("offset_preserved", test_offset_preserved), + T("offset_survives_gc", test_offset_survives_gc), + T("val_i32", test_val_i32), + T("val_i64", test_val_i64), + T("val_f32", test_val_f32), + T("val_f64", test_val_f64), + T("val_v128", test_val_v128), + T("val_ref_null", test_val_ref_null), + T("val_ref_obj", test_val_ref_obj), + T("val_i31", test_val_i31), + T("val_cont", test_val_cont), + T("val_mixed", test_val_mixed), + T("rewrite_shrinks", test_rewrite_shrinks), + T("accessor_cleared", test_accessor_cleared), + T("root_params_state", test_root_params_state), + T("pipeline_naive", test_pipeline_naive), + T("pipeline_packed", test_pipeline_packed), + T("pipeline_stack", test_pipeline_stack), + T("cont_bottom_set", test_cont_bottom_set), + T("stp_at_end", test_stp_at_end), + T("write_requires_empty_stack", test_write_requires_empty_stack), + TS("single", test_spc_single), + TS("chain", test_spc_chain), + TS("mixed", test_spc_mixed), + TS("inlined_instance_cleared", test_spc_inlined_instance_cleared), + TS("vals", test_spc_vals), + TS("pc_is_derived", test_spc_pc_is_derived), + TS("curpc", test_spc_curpc), + () +); + +private class X86_64CompressionTester extends ExeTester { + var instance_: Instance; + var v_v_func: WasmFunction; + var stack_: X86_64Stack; + var stack2_: X86_64Stack; + + new(t: Tester, tiering: ExecutionStrategy) super(t, tiering) { } + + // Instantiate the module built so far; an SPC tier also compiles it, giving {spcRetAddr} a real address. + def instance() -> Instance { + if (instance_ == null) instance_ = makeInstance(); + return instance_; + } + def wasmFunc(index: int) -> WasmFunction { + return WasmFunction.!(instance().functions[index]); + } + def emptyFunc() -> WasmFunction { + if (v_v_func == null) { + sig(SigCache.v_v).codev([]); + v_v_func = wasmFunc(0); + } + return v_v_func; + } + // Two scratch stacks, reused within a test. + def stack() -> X86_64Stack { + if (stack_ == null) stack_ = X86_64StackManager.getFreshStack(); + return stack_; + } + def stack2() -> X86_64Stack { + if (stack2_ == null) stack2_ = X86_64StackManager.getFreshStack(); + return stack2_; + } + + def intRetAddr() -> u64 { + var ic = X86_64PreGenStubs.getInterpreterCode(); + return u64.view(ic.start + ic.header.deoptReentryOffset - Pointer.NULL); + } + def spcRetAddr(f: WasmFunction) -> u64 { + var entry = f.decl.target_code.spc_entry; + if (!X86_64SpcModuleCode.?(RiRuntime.findUserCode(entry))) { + t.fail("expected an SPC-compiled function; is this tester running under an SPC tier?"); + return 0; + } + return u64.view(entry - Pointer.NULL); + } + + // Build an interpreter frame with an explicit resume offset. + def makeIntFrameAt(f: WasmFunction, pc: int, offset: int, vals: Array) -> RelocatableFrame { + return RelocatableFrame(WasmFrameData(f, pc, intRetAddr(), vals), false, offset); + } + // Build an interpreter frame with the resume offset pinned to {pc + 1}. + def makeIntFrame(f: WasmFunction, pc: int, vals: Array) -> RelocatableFrame { + return makeIntFrameAt(f, pc, pc + 1, vals); + } + // Build an SPC frame with pc = -1. + def makeSpcFrame(f: WasmFunction, vals: Array) -> RelocatableFrame { + return RelocatableFrame(WasmFrameData(f, -1, spcRetAddr(f), vals), true, 0); + } + // A chain of {n} interpreter frames. + def intChain(n: int) -> Array { + var f = emptyFunc(); + var frames = Array.new(n); + for (i < n) { + var vals = Array.new(i); + for (j < i) vals[j] = Value.I32(u32.view(i * 100 + j)); + frames[i] = makeIntFrame(f, 1, vals); + } + return frames; + } + // Real instruction offsets in {f}, so tests do not invent pc values. + def validPcs(f: WasmFunction) -> Array { + var pcs = Vector.new(); + var bi = BytecodeIterator.new().reset(f.decl); + while (bi.more()) { + bi.current(); + pcs.put(bi.pc); + bi.next(); + } + return pcs.extract(); + } + + def roundtrip(frames: Array) -> Array { + var s = stack(); + X86_64Compression.writeFrames(s, frames); + return X86_64Compression.readFrames(s); + } + def assert_roundtrip(frames: Array) { + FrameAsserts.assert_frames_eq(t, frames, roundtrip(frames)); + } + // Roundtrip a single trivial frame holding {vals}. + def assert_roundtrip_vals(vals: Array) { + assert_roundtrip([makeIntFrame(emptyFunc(), 1, vals)]); + } + def assert_frames_eq(expected: Array, got: Array) { + FrameAsserts.assert_frames_eq(t, expected, got); + } + // The frame handle of the topmost frame of {s}; {s.rsp} is that frame's return-address slot. + def topHandle(s: X86_64Stack) -> X86_64FrameHandle { + return X86_64FrameHandles.from(s.rsp + Pointer.SIZE); + } +} + +// ===== Frame structure ===== + +def test_empty_frames(t: X86_64CompressionTester) { + var got = t.roundtrip([]); + FrameAsserts.assert_length(t.t, 0, got.length, "frames"); + if (t.stack().state() != StackState.EMPTY) t.t.fail1("expected an empty stack, got %s", t.stack().state().name); +} + +def test_single_empty(t: X86_64CompressionTester) { + t.assert_roundtrip([t.makeIntFrame(t.emptyFunc(), 1, [])]); +} + +def test_single_val(t: X86_64CompressionTester) { + t.assert_roundtrip([t.makeIntFrame(t.emptyFunc(), 1, [Value.I32(0xCAFEBABEu)])]); +} + +def test_two_frames(t: X86_64CompressionTester) { + t.assert_roundtrip(t.intChain(2)); +} + +def test_three_frames(t: X86_64CompressionTester) { + t.assert_roundtrip(t.intChain(3)); +} + +// Frames with 0..9 values, so no two adjacent value regions have the same length. +def test_varying_vals(t: X86_64CompressionTester) { + t.assert_roundtrip(t.intChain(10)); +} + +def test_many_frames(t: X86_64CompressionTester) { + var f = t.emptyFunc(); + var n = 200; // ~22KB of frames; the stack redzone sits at 256KB + var frames = Array.new(n); + for (i < n) frames[i] = t.makeIntFrame(f, 1, [Value.I64(u64.view(i))]); + t.assert_roundtrip(frames); +} + +def test_many_vals(t: X86_64CompressionTester) { + var n = 1000; // 32KB of value slots + var vals = Array.new(n); + for (i < n) vals[i] = Value.I32(u32.view(i * 7)); + t.assert_roundtrip_vals(vals); +} + +// One frame per real instruction offset in a non-trivial function body. +def test_pc_variety(t: X86_64CompressionTester) { + t.sig(SigCache.ii_i).codev([ + u8.!(Opcode.LOCAL_GET.code), 0, + u8.!(Opcode.LOCAL_GET.code), 1, + u8.!(Opcode.I32_ADD.code) + ]); + var f = t.wasmFunc(0); + var pcs = t.validPcs(f); + if (pcs.length < 3) return t.t.fail1("expected several instruction pcs, got %d", pcs.length); + var frames = Array.new(pcs.length); + for (i < pcs.length) frames[i] = t.makeIntFrame(f, pcs[i], [Value.I32(u32.view(i))]); + t.assert_roundtrip(frames); +} + +// Frames belonging to three different functions of the same instance. +def test_multi_func(t: X86_64CompressionTester) { + t.newFunction(SigCache.v_v, [u8.!(Opcode.NOP.code)]); + t.newFunction(SigCache.v_v, [u8.!(Opcode.NOP.code), u8.!(Opcode.NOP.code)]); + t.sig(SigCache.v_v).codev([]); + var frames = [ + t.makeIntFrame(t.wasmFunc(0), 1, [Value.I32(10)]), + t.makeIntFrame(t.wasmFunc(1), 1, [Value.I32(20), Value.I32(21)]), + t.makeIntFrame(t.wasmFunc(2), 1, []) + ]; + t.assert_roundtrip(frames); +} + +// ===== Value representation ===== + +def test_val_i32(t: X86_64CompressionTester) { + t.assert_roundtrip_vals([Value.I32(0), Value.I32(1), Value.I32(0x80000000u), Value.I32(0xFFFFFFFFu)]); +} + +def test_val_i64(t: X86_64CompressionTester) { + t.assert_roundtrip_vals([Value.I64(0), Value.I64(0x8000000000000000uL), Value.I64(0xFFFFFFFFFFFFFFFFuL)]); +} + +def test_val_f32(t: X86_64CompressionTester) { + t.assert_roundtrip_vals([Value.F32(0), Value.F32(0x3F800000u), Value.F32(0x7FC00000u), Value.F32(0xFF800000u)]); +} + +def test_val_f64(t: X86_64CompressionTester) { + t.assert_roundtrip_vals([Value.F64(0), Value.F64(0x3FF0000000000000uL), Value.F64(0x7FF8000000000000uL)]); +} + +def test_val_v128(t: X86_64CompressionTester) { + t.assert_roundtrip_vals([ + Value.V128(0, 0), + Value.V128(0xDEADBEEFCAFE0000uL, 0x0123456789ABCDEFuL), + Value.V128(0xFFFFFFFFFFFFFFFFuL, 0xFFFFFFFFFFFFFFFFuL) + ]); +} + +def test_val_ref_null(t: X86_64CompressionTester) { + t.assert_roundtrip_vals([Value.Ref(null), Value.Ref(null)]); +} + +// Reference identity survives, but the tag byte does not. +def test_val_ref_obj(t: X86_64CompressionTester) { + var a = newObject([]); + var b = HostObject.new(); + var vals: Array = [Value.Ref(a), Value.Ref(b), Value.Ref(null), Value.Ref(a)]; + var got = t.roundtrip([t.makeIntFrame(t.emptyFunc(), 1, vals)]); + FrameAsserts.assert_length(t.t, 1, got.length, "frames"); + FrameAsserts.assert_vals_eq(t.t, vals, got[0].data.vals); + match (got[0].data.vals[0]) { + Ref(obj) => if (obj != a) t.t.fail("ref identity not preserved"); + _ => t.t.fail("expected a Ref value"); + } +} + +def test_val_i31(t: X86_64CompressionTester) { + t.assert_roundtrip_vals([Value.I31(0), Value.I31(1), Value.I31(u31.!(0x3FFFFFFF)), Value.I31(u31.max)]); +} + +def test_val_cont(t: X86_64CompressionTester) { + var target = Target.newWasmStack(); + var cont = Continuations.makeContinuation(target); + var vals: Array = [Value.Cont(cont)]; + var got = t.roundtrip([t.makeIntFrame(t.emptyFunc(), 1, vals)]); + FrameAsserts.assert_length(t.t, 1, got.length, "frames"); + FrameAsserts.assert_vals_eq(t.t, vals, got[0].data.vals); + match (got[0].data.vals[0]) { + Cont(c) => { + if (Continuations.getStoredStack(c) != target) t.t.fail("cont stack not preserved"); + if (Continuations.getStoredVersion(c) != Continuations.getStoredVersion(cont)) { + t.t.fail("cont version not preserved"); + } + } + _ => t.t.fail("expected a Cont value"); + } +} + +// Every value kind adjacent in one frame, catching any slot-size or tag-offset mistake. +def test_val_mixed(t: X86_64CompressionTester) { + var obj = newObject([]); + t.assert_roundtrip_vals([ + Value.I32(1), Value.I64(2), Value.F32(3), Value.F64(4), + Value.V128(5, 6), Value.Ref(obj), Value.Ref(null), Value.I31(7), + Value.I32(0xFFFFFFFFu) + ]); +} + +// ===== Bytecode resume offset ===== + +def test_offset_preserved(t: X86_64CompressionTester) { + t.sig(SigCache.v_v).codev([ + u8.!(Opcode.I32_CONST.code), 0x80, 0x80, 0x01, // 3-byte LEB immediate + u8.!(Opcode.DROP.code) + ]); + var f = t.wasmFunc(0); + var pc = t.validPcs(f)[0]; + var offset = pc + 3; // last byte of the immediate, before the next instruction + var got = t.roundtrip([t.makeIntFrameAt(f, pc, offset, [])]); + FrameAsserts.assert_length(t.t, 1, got.length, "frames"); + if (got[0].bytecode_offset != offset) { + t.t.fail2("expected bytecode_offset=%d, got %d", offset, got[0].bytecode_offset); + } +} + +def test_offset_survives_gc(t: X86_64CompressionTester) { + var f = t.emptyFunc(); + var seed = t.intChain(3); + var src = t.stack2(); + X86_64Compression.writeFrames(src, seed); + var frames = X86_64Compression.readFrames(src); + + Target.forceGC(); + + var s = t.stack(); + X86_64Compression.writeFrames(s, frames); + var h = t.topHandle(s); + if (!X86_64FrameHandle.Interpreter.?(h)) return t.t.fail("expected an interpreter frame on top"); + var ih = X86_64FrameHandle.Interpreter.!(h); + var code = f.decl.cur_bytecode; // read after the collection + if (ih.code() != code) return t.t.fail("expected the frame to hold the current bytecode array"); + var expected = Pointer.atContents(code) + frames[frames.length - 1].bytecode_offset; + if (ih.ip() != expected) { + t.t.fail2("expected ip=0x%x, got ip=0x%x", + u64.view(expected - Pointer.NULL), u64.view(ih.ip() - Pointer.NULL)); + } + t.assert_frames_eq(frames, X86_64Compression.readFrames(s)); +} + +// ===== Stack hygiene ===== + +// Writing a shorter frame set over a longer one must not leave the tail of the old set reachable. +def test_rewrite_shrinks(t: X86_64CompressionTester) { + var long = t.intChain(6); + X86_64Compression.writeFrames(t.stack(), long); + t.stack().clear(); // {writeFrames} requires an EMPTY stack + var short = t.intChain(2); + t.assert_frames_eq(short, t.roundtrip(short)); +} + +// {writeFrames} clears the accessor slot, so a stale {X86_64FrameAccessor} cannot defeat {isUnwound}. +def test_accessor_cleared(t: X86_64CompressionTester) { + var frames = t.intChain(2); + var s = t.stack(); + X86_64Compression.writeFrames(s, frames); + var stale = X86_64Compression.readFrames(s); // installs accessors + if (stale.length != 2) return t.t.fail1("expected 2 frames, got %d", stale.length); + if (t.topHandle(s).accessor() == null) return t.t.fail("expected readFrames to install an accessor"); + + s.clear(); + X86_64Compression.writeFrames(s, frames); + if (t.topHandle(s).accessor() != null) t.t.fail("expected writeFrames to clear the accessor slot"); +} + +def test_root_params_state(t: X86_64CompressionTester) { + t.sig(SigCache.ii_i).codev([ + u8.!(Opcode.LOCAL_GET.code), 0, + u8.!(Opcode.LOCAL_GET.code), 1, + u8.!(Opcode.I32_ADD.code) + ]); + var f = t.wasmFunc(0); + var s = t.stack(); + X86_64Compression.writeFrames(s, [t.makeIntFrame(f, t.validPcs(f)[0], [Value.I32(1), Value.I32(2)])]); + if (s.params_arity != 0) t.t.fail1("expected no pending root parameters, got params_arity=%d", s.params_arity); + if (s.state() != StackState.RESUMABLE) t.t.fail1("expected a RESUMABLE stack, got %s", s.state().name); +} + +// ===== Full pipeline ===== + +def assert_pipeline(t: X86_64CompressionTester, roundtrip: Array -> Array) { + var frames = t.intChain(4); + X86_64Compression.writeFrames(t.stack(), frames); + var read = Target.readFramesFromStack(t.stack()); + var back = roundtrip(read); + Target.writeFramesToStack(t.stack2(), back); + t.assert_frames_eq(frames, X86_64Compression.readFrames(t.stack2())); +} + +def test_pipeline_naive(t: X86_64CompressionTester) { + assert_pipeline(t, fun (fs: Array) => NAIVE.decompress(NAIVE.compress(fs))); +} + +def test_pipeline_packed(t: X86_64CompressionTester) { + assert_pipeline(t, fun (fs: Array) => PACKED.decompress(PACKED.compress(fs))); +} + +// The same loop through the {StackCompression} entrypoints, covering its strategy dispatch. +def test_pipeline_stack(t: X86_64CompressionTester) { + var frames = t.intChain(4); + X86_64Compression.writeFrames(t.stack(), frames); + var compressed = StackCompression.compressStack(t.stack()); + StackCompression.decompressStack(t.stack2(), compressed); + t.assert_frames_eq(frames, X86_64Compression.readFrames(t.stack2())); +} + +// ===== SPC frames ===== + +def test_spc_single(t: X86_64CompressionTester) { + t.assert_roundtrip([t.makeSpcFrame(t.emptyFunc(), [Value.I32(42)])]); +} + +def test_spc_chain(t: X86_64CompressionTester) { + var f = t.emptyFunc(); + var frames = Array.new(5); + for (i < frames.length) { + var vals = Array.new(i); + for (j < i) vals[j] = Value.I64(u64.view(i * 10 + j)); + frames[i] = t.makeSpcFrame(f, vals); + } + t.assert_roundtrip(frames); +} + +// Interleaved interpreter and SPC frames: same 104-byte footprint, {is_spc} from the return address. +def test_spc_mixed(t: X86_64CompressionTester) { + var f = t.emptyFunc(); + var frames = Array.new(6); + for (i < frames.length) { + var vals: Array = [Value.I32(u32.view(i))]; + frames[i] = if((i & 1) == 0, t.makeSpcFrame(f, vals), t.makeIntFrame(f, 1, vals)); + } + t.assert_roundtrip(frames); +} + +// {inlined_instance} (+40) aliases the interpreter's {stp} slot, and must be cleared. +def test_spc_inlined_instance_cleared(t: X86_64CompressionTester) { + var f = t.emptyFunc(); + var s = t.stack(); + var n = 4; + X86_64Compression.writeFrames(s, t.intChain(n)); + var top = t.topHandle(s); + if (!X86_64FrameHandle.Interpreter.?(top)) return t.t.fail("setup: expected an interpreter frame"); + if (X86_64FrameHandle.Interpreter.!(top).stp() == Pointer.NULL) { + return t.t.fail("setup: expected the interpreter frame to store a sidetable pointer"); + } + + var spc = Array.new(n); + for (i < n) spc[i] = t.makeSpcFrame(f, []); + s.clear(); // {writeFrames} requires an EMPTY stack; the stale +40 slots stay in the mapping + X86_64Compression.writeFrames(s, spc); + + var sfp = s.rsp + Pointer.SIZE; + for (i < n) { + if (X86_64FrameHandle.Spc(sfp).inlined_instance() != null) { + return t.t.fail1("frame[%d]: expected inlined_instance to be cleared", i); + } + sfp += X86_64InterpreterFrame.size + Pointer.SIZE; + } + Target.forceGC(); // the stack is a GC root; a stale +40 would be scanned here +} + +def test_spc_vals(t: X86_64CompressionTester) { + var obj = newObject([]); + t.assert_roundtrip([t.makeSpcFrame(t.emptyFunc(), [ + Value.I32(1), Value.I64(2), Value.F32(3), Value.F64(4), + Value.V128(5, 6), Value.Ref(obj), Value.I31(7) + ])]); +} + +// An SPC frame's pc is derived from its return address. +def test_spc_pc_is_derived(t: X86_64CompressionTester) { + var f = t.emptyFunc(); + var frame = RelocatableFrame(WasmFrameData(f, 42, t.spcRetAddr(f), [Value.I32(1)]), true, 0); + var got = t.roundtrip([frame]); + FrameAsserts.assert_length(t.t, 1, got.length, "frames"); + if (got[0].data.pc != -1) t.t.fail1("expected the supplied pc to be discarded, got pc=%d", got[0].data.pc); +} + +def test_spc_curpc(t: X86_64CompressionTester) { + t.sig(SigCache.v_v).codev([ + u8.!(Opcode.NOP.code), u8.!(Opcode.NOP.code), u8.!(Opcode.NOP.code) + ]); + var f = t.wasmFunc(0); + var stale_pc = t.validPcs(f)[2]; // a real pc, so the frame's resume offset stays in bounds + var s = t.stack(); + X86_64Compression.writeFrames(s, [t.makeIntFrame(f, stale_pc, [Value.I32(1)])]); + if (t.topHandle(s).curpc() != stale_pc) return t.t.fail("setup: expected the interpreter frame to store its pc"); + + s.clear(); + X86_64Compression.writeFrames(s, [t.makeSpcFrame(f, [Value.I32(1)])]); + var got = t.topHandle(s).curpc(); + if (got != -1) t.t.fail1("expected the SPC frame to carry curpc=-1, got %d", got); +} + +def test_cont_bottom_set(t: X86_64CompressionTester) { + var s = t.stack(); + X86_64Compression.writeFrames(s, t.intChain(2)); + if (s.cont_bottom == null) { + t.t.fail("expected cont_bottom to be established on a reconstructed stack"); + } +} + +def test_stp_at_end(t: X86_64CompressionTester) { + // One `br` out of a block, so the sidetable holds exactly one 4-int entry. + t.sig(SigCache.v_v).codev([ + u8.!(Opcode.BLOCK.code), BpTypeCode.EmptyBlock.code, + u8.!(Opcode.BR.code), 0, + u8.!(Opcode.END.code) + ]); + var f = t.wasmFunc(0); + var entries = f.decl.sidetable.entries; + var map = SidetableMap.new(f.decl); + var pcs = t.validPcs(f); + var pc = pcs[pcs.length - 1]; // past the branch + var stp = map[pc]; + if (stp != entries.length) { + return t.t.fail2("setup: expected stp=%d at the last pc, got %d", entries.length, stp); + } + + var s = t.stack(); + X86_64Compression.writeFrames(s, [t.makeIntFrame(f, pc, [])]); + var h = t.topHandle(s); + if (!X86_64FrameHandle.Interpreter.?(h)) return t.t.fail("expected an interpreter frame on top"); + var ih = X86_64FrameHandle.Interpreter.!(h); + // Mirrors {X86_64FrameAccessor.stp}, which every consumer of this slot uses. + var got = int.!((ih.stp() - Pointer.atContents(entries)) / 4); + if (got != stp) t.t.fail2("expected sidetable position %d, got %d", stp, got); +} + +def test_write_requires_empty_stack(t: X86_64CompressionTester) { + t.sig(SigCache.ii_i).codev([ + u8.!(Opcode.LOCAL_GET.code), 0, + u8.!(Opcode.LOCAL_GET.code), 1, + u8.!(Opcode.I32_ADD.code) + ]); + var f = t.wasmFunc(0); + + // A stack awaiting its arguments, exactly as {cont.new} leaves one. + var s = X86_64StackManager.getFreshStack(); + s.reset(f); + if (s.params_arity != 2 || s.state() != StackState.SUSPENDED) { + return t.t.fail1("setup: expected a SUSPENDED stack awaiting 2 arguments, got %s", s.state().name); + } + + // Once its holder is done with it, an explicit {clear()} makes the stack writable again. + s.clear(); + if (s.state() != StackState.EMPTY) { + return t.t.fail1("setup: expected clear() to empty the stack, got %s", s.state().name); + } + X86_64Compression.writeFrames(s, [t.makeIntFrame(f, t.validPcs(f)[0], [Value.I32(1), Value.I32(2)])]); + if (s.params_arity != 0 || s.state() != StackState.RESUMABLE) { + t.t.fail1("expected a RESUMABLE stack with no pending parameters, got %s", s.state().name); + } +}