diff --git a/src/engine/Compression.v3 b/src/engine/Compression.v3 new file mode 100644 index 000000000..3bc5e5e34 --- /dev/null +++ b/src/engine/Compression.v3 @@ -0,0 +1,187 @@ +// Copyright 2025 Wizard authors. All rights reserved. +// See LICENSE for details of Apache 2.0 license. + +// Generalized- relocatable representation of a stack frame that can be serialized. +type RelocatableFrame(func: WasmFunction, pc: int, reentry: TargetReentryLabel, bytecode_ip: TargetReentryLabel, vals: Array) #unboxed { + + def render(buf: StringBuilder) -> StringBuilder { + // Frame header. + buf.put2(" Frame(func=%q, pc=%d):", func.decl.render(null, _), pc).ln(); + buf.put1(" Values(count=%d):", vals.length).ln(); + + // Frame values. + if (vals.length > 0) { + buf.puts(" "); + for (v in vals) Values.render(v, buf).puts(" "); + } + return buf; + } +} + +class CompressionStrategy { + def compress(frames: Range) -> C; + def decompress(comp: C) -> Array; +} + +class NaiveCompressionStrategy extends CompressionStrategy { + def compress(frames: Range) -> NaiveCompressedStack { + var frames_builder = Vector.new(); + var values_builder = Vector.new(); + for (frame in frames) { + var header = FrameHeader(frame.func, frame.pc, frame.reentry, frame.bytecode_ip, frame.vals.length); + frames_builder.put(header); + values_builder.puta(frame.vals); + } + + // XXX: reuse stack (maybe pass one in?) + var result = NaiveCompressedStack.new(); + result.frames = frames_builder.extract(); + result.values = values_builder.extract(); + return result; + } + + def decompress(comp: NaiveCompressedStack) -> Array { + var builder = Vector.new(); + var val_offset = 0; + for (header in comp.frames) { + var vals = Arrays.range(comp.values, val_offset, val_offset + header.n_vals); + builder.put(RelocatableFrame(header.func, header.pc, header.reentry, header.bytecode_ip, vals)); + val_offset += header.n_vals; + } + + return builder.extract(); + } +} + +class PackedCompressionStrategy extends CompressionStrategy { + def frames_builder = Vector.new(); + def refs_builder = Vector.new(); + def w = DataWriter.new(); + def r = DataReader.new(null); + + def reset() { frames_builder.clear(); refs_builder.clear(); w.clear(); } + + def writeValue(value: Value) { + match (value) { + Ref(v) => { w.putb(Value.Ref.tag).put_uleb32(u32.!(refs_builder.length)); refs_builder.put(v); } + I31(v) => w.putb(Value.I31.tag).put_uleb32(v); + I32(v) => w.putb(Value.I32.tag).put_uleb32(v); + I64(v) => w.putb(Value.I64.tag).put_sleb64(i64.view(v)); + F32(v) => w.putb(Value.F32.tag).put_uleb32(v); + F64(v) => w.putb(Value.F64.tag).put_sleb64(i64.view(v)); + V128(l, h) => w.putb(Value.V128.tag).put_sleb64(i64.view(l)).put_sleb64(i64.view(h)); + Cont(c) => { + // TODO: omit {version} if boxed-continuation is enabled + var obj = Continuations.getStoredObject(c); + var version = Continuations.getStoredVersion(c); + w.putb(Value.Cont.tag).put_uleb32(u32.!(refs_builder.length)).put_sleb64(i64.view(version)); + refs_builder.put(obj); + } + } + } + + def compress(frames: Range) -> PackedCompressedStack { + for (frame in frames) { + var header = FrameHeader(frame.func, frame.pc, frame.reentry, frame.bytecode_ip, frame.vals.length); + frames_builder.put(header); + for (v in frame.vals) writeValue(v); + } + + // XXX: reuse stack (maybe pass one in?) + var result = PackedCompressedStack.new(); + result.frames = frames_builder.extract(); + result.packed = w.extract(); + result.refs = refs_builder.extract(); + return result; + } + + def readValue(comp: PackedCompressedStack) -> Value { + var tag = r.read1(); + match (tag) { + Value.Ref.tag => return Value.Ref(comp.refs[r.read_uleb32()]); + Value.I31.tag => return Value.I31(u31.!(r.read_uleb32())); + Value.I32.tag => return Value.I32(r.read_uleb32()); + Value.I64.tag => return Value.I64(u64.view(r.read_sleb64())); + Value.F32.tag => return Value.F32(r.read_uleb32()); + Value.F64.tag => return Value.F64(u64.view(r.read_sleb64())); + Value.V128.tag => return Value.V128(u64.view(r.read_sleb64()), u64.view(r.read_sleb64())); + Value.Cont.tag => { + var obj = comp.refs[r.read_uleb32()]; + var version = u64.view(r.read_sleb64()); + return Value.Cont(Continuations.fromStoredObject(obj, version)); + } + _ => return Value.I32(0); + } + } + + def decompress(comp: PackedCompressedStack) -> Array { + r.reset(comp.packed, 0, comp.packed.length); + + var builder = Vector.new(); + for (header in comp.frames) { + var vals = Array.new(header.n_vals); + for (i < header.n_vals) vals[i] = readValue(comp); + builder.put(RelocatableFrame(header.func, header.pc, header.reentry, header.bytecode_ip, vals)); + } + return builder.extract(); + } +} + +// Compressed stack representation. + +type FrameHeader(func: WasmFunction, pc: int, reentry: TargetReentryLabel, bytecode_ip: TargetReentryLabel, n_vals: int) #unboxed { + + def render(buf: StringBuilder) -> StringBuilder { + return buf.put3("Frame(func=%d, pc=%d, n_vals=%d)", func.decl.func_index, pc, n_vals); + } +} + +class CompressedStack { + def size() -> u64; + def render(buf: StringBuilder) -> StringBuilder; +} + +class NaiveCompressedStack extends CompressedStack { + var frames: Array; + var values: Array; + + def render(buf: StringBuilder) -> StringBuilder { + buf.put1("NaiveCompressedStack(n_frames=%d):", frames.length).ln(); + for (frame in frames) buf.put1(" %q", frame.render).ln(); + return buf; + } +} + +class PackedCompressedStack extends CompressedStack { + var frames: Array; + + // Packed value stack representation. + var packed: Array; + var refs: Array; + + def render(buf: StringBuilder) -> StringBuilder { + buf.put1("PackedCompressedStack(n_frames=%d):", frames.length).ln(); + for (frame in frames) buf.put1(" %q", frame.render).ln(); + return buf; + } +} + +component StackCompression { + def naive = NaiveCompressionStrategy.new(); + def packed = PackedCompressionStrategy.new(); + + def compress(stack: WasmStack) -> CompressedStack { + var frames = Target.readFramesFromStack(stack); + var compressed = packed.compress(frames); + return compressed; + } + + def decompress(to: WasmStack, from: CompressedStack) { + var frames: Array; + match (from) { + x: NaiveCompressedStack => frames = naive.decompress(x); + x: PackedCompressedStack => frames = packed.decompress(x); + } + Target.writeFramesToStack(to, frames); + } +} diff --git a/src/engine/Debug.v3 b/src/engine/Debug.v3 index 29b91af78..1bd18875f 100644 --- a/src/engine/Debug.v3 +++ b/src/engine/Debug.v3 @@ -13,6 +13,7 @@ component Debug { def stack = false; def memory = false; def diagnostic = false; + def compression = false; // Prevents arguments from being dead-code-eliminated. def keepAlive(x: T) { } diff --git a/src/engine/Sidetable.v3 b/src/engine/Sidetable.v3 index a9017a2a2..05b4075ac 100644 --- a/src/engine/Sidetable.v3 +++ b/src/engine/Sidetable.v3 @@ -82,7 +82,11 @@ component Sidetables { BR => size = Sidetable_BrEntry.size; BR_IF => size = Sidetable_BrEntry.size; BR_TABLE => { var count = immptr.skip_labels(); size = (1 + count) * Sidetable_BrEntry.size; } - TRY_TABLE => { var count = immptr.skip_catches(); size = count * Sidetable_CatchEntry.size; } + TRY_TABLE => { + immptr.read_BlockTypeCode(); + var count = immptr.skip_catches(); + size = count * Sidetable_CatchEntry.size; + } BR_ON_NULL => size = Sidetable_BrEntry.size; BR_ON_NON_NULL => size = Sidetable_BrEntry.size; BR_ON_CAST => size = Sidetable_BrEntry.size; diff --git a/src/engine/Value.v3 b/src/engine/Value.v3 index e7031b337..e77bb927a 100644 --- a/src/engine/Value.v3 +++ b/src/engine/Value.v3 @@ -71,7 +71,7 @@ component Values { var id = if(x.decl == null, -1, x.decl.heaptype_index); buf.put1("", id); } - x: Object => x.render(buf); + x: Object => { x.render(buf); } null => buf.puts(""); } I31(val) => buf.put1("i31:%d", u32.view(val)); diff --git a/src/engine/WasmStack.v3 b/src/engine/WasmStack.v3 index a15f2ae38..42905605b 100644 --- a/src/engine/WasmStack.v3 +++ b/src/engine/WasmStack.v3 @@ -2,7 +2,7 @@ // See LICENSE for details of Apache 2.0 license. // An execution stack. -class ExecStack { +class ExecStack extends Object { def popV(t: ValueType) -> Value; def popi() -> i32; def popu() -> u32; @@ -51,10 +51,14 @@ class ExecStack { } } +// Represents a suspendable stack that can be used to instantiate a continuation. +class VersionedStack extends ExecStack { + var version: u64; +} + // Represents a stack on which Wasm code can be executed. -class WasmStack extends ExecStack { +class WasmStack extends VersionedStack { var parent: WasmStack; - var version: u64; // ext:stack-switching // Denotes the bottom stack of a suspended continuation (with {this} as the top stack). diff --git a/src/engine/continuation/BoxedContinuation.v3 b/src/engine/continuation/BoxedContinuation.v3 index 1bed91b0b..3737ca13b 100644 --- a/src/engine/continuation/BoxedContinuation.v3 +++ b/src/engine/continuation/BoxedContinuation.v3 @@ -29,4 +29,11 @@ component Continuations { def getStoredStack(cont: Continuation) -> WasmStack { return cont.stack; } def getStoredVersion(cont: Continuation) -> u64 { return 0; } // boxed cont does not store version + + def objectIsContinuation(obj: Object) -> bool { return Continuation.?(obj); } + def objectToContinuation(obj: Object) -> Continuation { return Continuation.!(obj); } + + // Stack compression. + def getStoredObject(cont: Continuation) -> Object { return cont; } + def fromStoredObject(obj: Object, version: u64) -> Continuation { return Continuation.!(obj); } } diff --git a/src/engine/continuation/UnboxedContinuation.v3 b/src/engine/continuation/UnboxedContinuation.v3 index f67cee887..226484e8e 100644 --- a/src/engine/continuation/UnboxedContinuation.v3 +++ b/src/engine/continuation/UnboxedContinuation.v3 @@ -25,4 +25,11 @@ component Continuations { def getStoredStack(cont: Continuation) -> WasmStack { return cont.stack; } def getStoredVersion(cont: Continuation) -> u64 { return cont.version; } + + def objectIsContinuation(obj: Object) -> bool { return false; } + def objectToContinuation(obj: Object) -> Continuation; + + // Stack compression. + def getStoredObject(cont: Continuation) -> Object { return cont.stack; } + def fromStoredObject(obj: Object, version: u64) -> Continuation { return Continuation(WasmStack.!(obj), version); } } diff --git a/src/engine/v3/V3Target.v3 b/src/engine/v3/V3Target.v3 index 870f4be7e..2bb1062bb 100644 --- a/src/engine/v3/V3Target.v3 +++ b/src/engine/v3/V3Target.v3 @@ -42,6 +42,12 @@ component Target { def fastMemFill(dst: Range, val: byte) { for (i < dst.length) dst[i] = val; } + + // TODO[sc]: empty function stubs for stack compression (not needed for now). + // Stack compression: read the frames of a stack into an array of target independent frames representation. + def readFramesFromStack(from: WasmStack) -> Array; + // Stack compression: overwrite the destination stack with the content of the relocatable frames. + def writeFramesToStack(dest: WasmStack, frames: Array); } // A one-element cache for recycling storage of Wasm stacks (interpreters). @@ -101,3 +107,6 @@ type TargetFrame(frame: V3Frame) #unboxed { } } class TargetHandlerDest(is_dummy: bool) { } + +// Stack compression specialization. +type TargetReentryLabel(ret_addr: int) #unboxed; diff --git a/src/engine/x86-64/X86_64Interpreter.v3 b/src/engine/x86-64/X86_64Interpreter.v3 index 29307ca98..c0096c7c0 100644 --- a/src/engine/x86-64/X86_64Interpreter.v3 +++ b/src/engine/x86-64/X86_64Interpreter.v3 @@ -4230,6 +4230,7 @@ class X86_64InterpreterGen(ic: X86_64InterpreterCode, w: DataWriter) { } // generates cleanup code for after a child stack returns private def genOnResumeFinish(skip_tag: bool) { + restoreCurPcFromFrame(); restoreCallerIVars(); restoreDispatchTableReg(); var r_stack = r_tmp1; diff --git a/src/engine/x86-64/X86_64Runtime.v3 b/src/engine/x86-64/X86_64Runtime.v3 index ccecc0377..4b7bd887e 100644 --- a/src/engine/x86-64/X86_64Runtime.v3 +++ b/src/engine/x86-64/X86_64Runtime.v3 @@ -136,6 +136,13 @@ component X86_64Runtime { // is found). Then, the tag parameters and the continuation is pushed onto // the handler stack. def runtime_handle_suspend(stack: X86_64Stack, instance: Instance, tag_id: u31) -> Throwable { + // TODO[sc]: remove this check + var old_parent = stack.parent; + var compressed = StackCompression.compress(stack); + StackCompression.decompress(stack, compressed); + stack.parent = old_parent; + if (old_parent != null) stack.parent_rsp_ptr.store(X86_64Stack.!(old_parent).rsp); + var tag = instance.tags[tag_id]; var vals = stack.popN(tag.sig.params); var cont = Runtime.unwindStackChain(stack, instance, tag_id, WasmStack.tryHandleSuspension); diff --git a/src/engine/x86-64/X86_64Stack.v3 b/src/engine/x86-64/X86_64Stack.v3 index 8c5ec701f..2d0765cad 100644 --- a/src/engine/x86-64/X86_64Stack.v3 +++ b/src/engine/x86-64/X86_64Stack.v3 @@ -36,7 +36,7 @@ class X86_64Stack extends WasmStack { } clear(); if (valuerep.tagged) RiGc.registerScanner(this, X86_64Stack.scan); - if (Trace.stack && Debug.stack) Trace.OUT.put2("newStack start=0x%x, end=0x%x", mapping.range.start - Pointer.NULL, mapping.range.end - Pointer.NULL).ln(); + if (Debug.stack) Trace.OUT.put2("newStack start=0x%x, end=0x%x", mapping.range.start - Pointer.NULL, mapping.range.end - Pointer.NULL).ln(); } // Gets the state of this stack. def state() -> StackState { @@ -123,7 +123,7 @@ class X86_64Stack extends WasmStack { // stop the stackwalk. Otherwise stop at the return-parent stub. In any case, return the last // valid stack pointer, and a boolean indicating if the walk stopped early (due to {f} returning {false}). // (XXX: takes a function {f} with an additional parameter, and the parameter, to avoid a closure). - private def walk

(f: (Pointer, RiUserCode, StackFramePos, P) -> bool, param: P, start_sp: Pointer, continue_to_parent: bool) -> (bool, StackFramePos) { + def walk

(f: (Pointer, RiUserCode, StackFramePos, P) -> bool, param: P, start_sp: Pointer, continue_to_parent: bool) -> (bool, StackFramePos) { var stack = this; var sp = start_sp; if (Trace.stack && Debug.stack) { @@ -189,7 +189,7 @@ class X86_64Stack extends WasmStack { } // Increment by {Pointer.SIZE} to skip the return address pushed by {resume}. var frame = TargetFrame(rsp + Pointer.SIZE); - var accessor = frame.getFrameAccessor(); + var accessor = frame.getFrameAccessorOfStack(this); var func = accessor.func(); var handler = func.decl.findSuspensionHandler(func.instance, tag, accessor.pc()); if (handler.handler_pc < 0) { // not found @@ -473,9 +473,15 @@ class X86_64Stack extends WasmStack { code.scanFrame(ip, pos.frame.sp); return true; } + // XXX: If you ever get a nonsensical stacktrace (i.e., non-contiguous, untraceble + // call stack) where a virtual call on a value returned by {X86_64Stack.readValue} + // shows up somewhere in it, it's almost certainly the REF <=> REF_U64 tag coercion. def readValue(base: Pointer, offset: int) -> Value { + // TODO: correctly tag the REF_U64 storage kind (requires typed REF_NULL loads). if (!valuerep.tagged) fatal("untyped frame access requires value tagging to be enabled"); var tp = base + offset * valuerep.slot_size; + + if (!mapping.range.contains(base)) System.error("FrameAccessError", "base out of bounds"); if (!mapping.range.contains(tp)) System.error("FrameAccessError", "out of bounds"); var vp = tp + valuerep.tag_size; var tag = tp.load() & '\x7F'; @@ -488,6 +494,7 @@ class X86_64Stack extends WasmStack { BpTypeCode.NULLEXTERNREF.code, BpTypeCode.I31REF.code => return readI31OrObject(vp); + // TODO: check if nullref needs REF_U64 coercion BpTypeCode.STRUCTREF.code, BpTypeCode.NULLREF.code, BpTypeCode.ARRAYREF.code, @@ -576,6 +583,22 @@ class X86_64Stack extends WasmStack { if (bits == 0) return Values.REF_NULL; if ((bits & 1) == 1) return Value.I31(u31.view(bits >> 1)); var obj = vp.load(); + // TODO[sc]: revisit this check + if (FeatureDisable.unboxedConts) { + if (Continuations.objectIsContinuation(obj)) { + return Value.Cont(Continuations.objectToContinuation(obj)); + } + } else { + if (WasmStack.?(obj)) { + var version = (vp + 8).load(); + return Value.Cont(Continuations.continuationWithVersion(WasmStack.!(obj), version)); + } + } + + if (ExecStack.?(obj)) { + // {vp} points to an unboxed continuation. + var x = 1 / 0; + } return Value.Ref(obj); } def popResult(rt: Array) -> Result { @@ -593,7 +616,7 @@ def G = X86_64MasmRegs.toGpr, X = X86_64MasmRegs.toXmmr; component X86_64Stacks { var RESUME_STUB_POINTER: Pointer; - def getFrameAccessor(sp: Pointer) -> X86_64FrameAccessor { + def getFrameAccessorOfStack(sp: Pointer, stack: X86_64Stack) -> X86_64FrameAccessor { var retip = (sp + -RETADDR_SIZE).load(); var code = RiRuntime.findUserCode(retip); match (code) { @@ -602,7 +625,7 @@ component X86_64Stacks { if (prev != null) return prev; // Interpreter frames store the {WasmFunction} _and_ {FuncDecl}. var decl = (sp + X86_64InterpreterFrame.func_decl.offset).load(); - var n = X86_64FrameAccessor.new(X86_64Runtime.curStack, sp, decl); + var n = X86_64FrameAccessor.new(stack, sp, decl); (sp + X86_64InterpreterFrame.accessor.offset).store(n); return n; } @@ -612,13 +635,19 @@ component X86_64Stacks { // SPC frames only store the {WasmFunction}. var wf = (sp + X86_64InterpreterFrame.wasm_func.offset).load(); // TODO: assert wf.decl == x.decl() - var n = X86_64FrameAccessor.new(X86_64Runtime.curStack, sp, wf.decl); + var n = X86_64FrameAccessor.new(stack, sp, wf.decl); (sp + X86_64InterpreterFrame.accessor.offset).store(n); return n; } } return null; } + + // TODO[cleanup]: defaulting to {curStack} should be deprecated with stack-switching + def getFrameAccessor(sp: Pointer) -> X86_64FrameAccessor { + return getFrameAccessorOfStack(sp, X86_64Runtime.curStack); + } + def traceIpAndSp(ip: Pointer, sp: Pointer, out: Range -> void) { var buf = X86_64Runtime.globalFrameDescriptionBuf; buf.put2("\t@[ip=0x%x, sp=0x%x] ", ip - Pointer.NULL, sp - Pointer.NULL).send(out); @@ -1041,6 +1070,13 @@ class X86_64FrameAccessor(stack: X86_64Stack, sp: Pointer, decl: FuncDecl) exten var vfp = (sp + X86_64InterpreterFrame.vfp.offset).load(); return stack.readValue(vfp, i); } + // Get the value at {vfp + i}. + // XXX: refactor + def getValue(i: int) -> Value { + checkNotUnwound(); + var vfp = (sp + X86_64InterpreterFrame.vfp.offset).load(); + return stack.readValue(vfp, i); + } // Get the value of frame variable {i}. def getFrameVar(i: int) -> Value { checkNotUnwound(); @@ -1132,7 +1168,7 @@ class X86_64FrameAccessor(stack: X86_64Stack, sp: Pointer, decl: FuncDecl) exten var st_ptr = if(stp == st_entries.length, Pointer.atContents(st_entries), Pointer.atElement(func.sidetable.entries, stp)); (sp + X86_64InterpreterFrame.stp.offset) .store(st_ptr); } - private def vfp() -> Pointer { + def vfp() -> Pointer { return (sp + X86_64InterpreterFrame.vfp.offset).load(); } private def set_vsp(p: Pointer) { diff --git a/src/engine/x86-64/X86_64StackCompression.v3 b/src/engine/x86-64/X86_64StackCompression.v3 new file mode 100644 index 000000000..9ec811504 --- /dev/null +++ b/src/engine/x86-64/X86_64StackCompression.v3 @@ -0,0 +1,138 @@ +// Copyright 2025 Wizard authors. All rights reserved. +// See LICENSE for details of Apache 2.0 license. + +def valuerep = Target.tagging; + +component X86_64Compression { + def collector = StackFrameCollector.new(); + + def loadTargetFrame(frame: TargetFrame, next_vfp: Pointer) -> RelocatableFrame { + var accessor = frame.getFrameAccessor(); + var func = accessor.func(); + var pc = accessor.pc(); + var values = Vector.new(); + var bytecode_ip = (accessor.sp + X86_64InterpreterFrame.ip.offset).load(); + + // XXX: a neater way to access ret_addr pointer? + var ret_addr = (frame.sp + (-Pointer.SIZE)).load(); + + if (Debug.compression) { + var vfp_ptr = accessor.sp + X86_64InterpreterFrame.vfp.offset - Pointer.NULL; + var vfp_val = accessor.vfp() - Pointer.NULL; + Trace.OUT.put2("vfp (at compression) *(0x%x) = 0x%x", vfp_ptr, vfp_val).ln(); + } + + var offset = 0; + for (this_vfp = accessor.vfp(); this_vfp < next_vfp; this_vfp += valuerep.slot_size) { + var value = accessor.getValue(offset); + values.put(value); + offset++; + } + + return RelocatableFrame(func, pc, TargetReentryLabel(ret_addr), TargetReentryLabel(bytecode_ip), values.extract()); + } + + def readFrames(stack: X86_64Stack) -> Array { + if (Debug.compression) Trace.OUT.put1("Reading stack info:\n%q", renderStackBounds(stack, _)).ln(); + var collector = StackFrameCollector.new(); + var result = Vector.new(); + stack.walk(collector.visitFrame, void, stack.rsp, false); + + for (i = collector.frames.length - 1; i >= 0; i--) { + var next_vfp = stack.vsp; + if (i != 0) { + next_vfp = collector.frames[i-1].getFrameAccessor().vfp(); + } + + result.put(loadTargetFrame(collector.frames[i], next_vfp)); + } + + return result.extract(); + } + + def writeFrames(to: X86_64Stack, frames: Array) { + to.clear(); + var root_func = frames[0].func; + var instance = root_func.instance; + to.reset(instance.functions[root_func.decl.func_index]); + to.rsp += Pointer.SIZE; // Pop the enter-func-stub off (only needed for first entry). + if (Debug.compression) Trace.OUT.put1( + " Root function: %q", root_func.decl.render(null, _) + ).ln(); + + for (f in frames) { + var map = SidetableMap.new(f.func.decl); + var stp = map[f.pc]; + if (Debug.compression) Trace.OUT.put2("pc[%d] -> stp[%d]", f.pc, stp).ln(); + + // Allocate interpreter stack frame. + to.rsp += -X86_64InterpreterFrame.size; + setFrameContext(to.rsp, f.func); + setNewProgramLocation(to.rsp, f.func.decl, f.pc, stp, f.bytecode_ip.ret_addr); + + // Load values. + (to.rsp + X86_64InterpreterFrame.vfp.offset).store(to.vsp); // Store %vfp. + for (val in f.vals) to.push(val); + (to.rsp + X86_64InterpreterFrame.vsp.offset).store(to.vsp); // Store %vsp. + + if (Debug.compression) { + var vfp_ptr = to.rsp + X86_64InterpreterFrame.vfp.offset - Pointer.NULL; + var vfp_val = (to.rsp + X86_64InterpreterFrame.vfp.offset).load() - Pointer.NULL; + Trace.OUT.put2("vfp (at decompression) *(0x%x) = 0x%x", vfp_ptr, vfp_val).ln(); + } + + // Store recorded machine address as retip. + to.rsp += -Pointer.SIZE; + to.rsp.store(f.reentry.ret_addr); + } + + if (Debug.compression) Trace.OUT.put1("Stack info:\n%q", renderStackBounds(to, _)).ln(); + } + + private def renderStackBounds(stack: X86_64Stack, buf: StringBuilder) -> StringBuilder { + buf.put1(" vsp: 0x%x", stack.vsp - Pointer.NULL).ln(); + buf.put1(" rsp: 0x%x", stack.rsp - Pointer.NULL).ln(); + return buf; + } + + def setFrameContext(sp: Pointer, wf: WasmFunction) { + var module = wf.instance.module; + (sp + X86_64InterpreterFrame.wasm_func.offset).store(wf); + (sp + X86_64InterpreterFrame.instance.offset).store(wf.instance); + (sp + X86_64InterpreterFrame.sidetable.offset).store>(wf.decl.sidetable.entries); + (sp + X86_64InterpreterFrame.accessor.offset).store(null); + + // Load instance.memories[0].start into MEM0_BASE + if (module.memories.length > 0) { + var memory = NativeWasmMemory.!(wf.instance.memories[0]); + (sp + X86_64InterpreterFrame.mem0_base.offset).store(memory.start); + } + } + + // XXX: merge with functionality in FrameAccessor + // TODO: forgeRange + def setNewProgramLocation(sp: Pointer, func: FuncDecl, pc: int, stp: int, bytecode_ip: Pointer) { + var code = func.cur_bytecode; + + (sp + X86_64InterpreterFrame.func_decl.offset) .store(func); + (sp + X86_64InterpreterFrame.curpc.offset) .store(pc); + (sp + X86_64InterpreterFrame.code.offset) .store>(code); + (sp + X86_64InterpreterFrame.ip.offset) .store(bytecode_ip); + (sp + X86_64InterpreterFrame.eip.offset) .store(Pointer.atContents(code) + code.length); + var st_entries = func.sidetable.entries; + var st_ptr = if(stp == st_entries.length, Pointer.atContents(st_entries), Pointer.atElement(func.sidetable.entries, stp)); + (sp + X86_64InterpreterFrame.stp.offset) .store(st_ptr); + } +} + +// Class for collecting and organizing the frames of a {X86_64Stack} during its +// compression by walking its stack frames. +class StackFrameCollector { + def frames = Vector.new(); + + def reset() { frames.clear(); } + def visitFrame(p: Pointer, c: RiUserCode, pos: StackFramePos, v: void) -> bool { + 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 015db9508..43649e5e8 100644 --- a/src/engine/x86-64/X86_64Target.v3 +++ b/src/engine/x86-64/X86_64Target.v3 @@ -161,6 +161,26 @@ component Target { } return RedZones.addRedZone(mapping, offset, size); } + + // Stack compression: read the frames of a stack into an array of target independent frames representation. + def readFramesFromStack(from: WasmStack) -> Array { + var stack = X86_64Stack.!(from); + if (Debug.compression) { + Trace.OUT.put1( + "Compressing X86-64Stack @ 0x%x", Pointer.atObject(stack) - Pointer.NULL + ).ln(); + } + return X86_64Compression.readFrames(stack); + } + + // Stack compression: overwrite the destination stack with the content of the relocatable frames. + def writeFramesToStack(dest: WasmStack, frames: Array) { + var stack = X86_64Stack.!(dest); + if (Debug.compression) Trace.OUT.put1( + "Decompressing into X86-64Stack @ 0x%x", Pointer.atObject(stack) - Pointer.NULL + ).ln(); + X86_64Compression.writeFrames(stack, frames); + } } type TargetOsrInfo(spc_entry: Pointer, osr_entries: List<(int, int)>) #unboxed { } @@ -171,6 +191,11 @@ type TargetFrame(sp: Pointer) #unboxed { def getFrameAccessor() -> X86_64FrameAccessor { return X86_64Stacks.getFrameAccessor(sp); } + + // TODO[cleanup]: a frame accessor should be able to obtain its corresponding stack + def getFrameAccessorOfStack(stack: X86_64Stack) -> X86_64FrameAccessor { + return X86_64Stacks.getFrameAccessorOfStack(sp, stack); + } } type SpcResultForStub(wf: WasmFunction, entrypoint: Pointer, thrown: Throwable) #unboxed { } class TargetHandlerDest(is_dummy: bool) { @@ -477,3 +502,6 @@ def genRdtsc(ic: X86_64InterpreterCode, w: DataWriter) { asm.q.or_r_r(X86_64Regs.RAX, X86_64Regs.RDX); // XXX: use V3 return register symbolic constant asm.ret(); } + +// Stack compression specialization. +type TargetReentryLabel(ret_addr: Pointer) #unboxed; diff --git a/test/regress/ext:stack-switching/suspend15_a.bin.wast b/test/regress/ext:stack-switching/suspend15_a.bin.wast new file mode 100644 index 000000000..1200d534b --- /dev/null +++ b/test/regress/ext:stack-switching/suspend15_a.bin.wast @@ -0,0 +1,14 @@ +(module definition binary + "\00\61\73\6d\01\00\00\00\01\97\80\80\80\00\06\60" + "\01\7f\00\60\00\00\5d\00\5d\01\60\00\02\7f\63\03" + "\60\01\7f\01\7f\03\83\80\80\80\00\02\00\05\0d\83" + "\80\80\80\00\01\00\00\07\88\80\80\80\00\01\04\6d" + "\61\69\6e\00\01\09\85\80\80\80\00\01\03\00\01\00" + "\0a\ca\80\80\80\00\02\a9\80\80\80\00\00\20\00\45" + "\04\40\41\00\e2\00\05\02\04\20\00\41\01\6b\d2\00" + "\e0\02\e3\02\01\00\00\00\41\7f\d0\03\0b\1a\41\01" + "\6a\e2\00\0b\0b\96\80\80\80\00\00\02\04\20\00\d2" + "\00\e0\02\e3\02\01\00\00\00\41\7f\d0\03\0b\1a\0b" +) +(module instance) +(assert_return (invoke "main" (i32.const 0x1)) (i32.const 0x1)) diff --git a/test/regress/ext:stack-switching/suspend15_a.wast b/test/regress/ext:stack-switching/suspend15_a.wast new file mode 100644 index 000000000..418a7865c --- /dev/null +++ b/test/regress/ext:stack-switching/suspend15_a.wast @@ -0,0 +1,38 @@ +(module + (type $f1 (func (param i32))) + (type $f2 (func)) + (type $c1 (cont $f1)) + (type $c2 (cont $f2)) + (tag $ts (param i32)) + (func $foo (param i32) + (if (i32.eqz (local.get 0)) + (then + (suspend $ts (i32.const 0)) + ) + (else + (block (result i32 (ref null $c2)) + (resume $c1 (on $ts 0) + (i32.sub (local.get 0) (i32.const 1)) + (cont.new $c1 (ref.func $foo)) + ) + (i32.const -1) + (ref.null $c2) + ) + (drop) + (i32.add (i32.const 1)) + (suspend $ts) + ) + ) + ) + (elem declare func $foo) + (func (export "main") (param i32) (result i32) + (block (result i32 (ref null $c2)) + (resume $c1 (on $ts 0) (local.get 0) (cont.new $c1 (ref.func $foo))) + (i32.const -1) + (ref.null $c2) + ) + (drop) + ) +) + +(assert_return (invoke "main" (i32.const 1)) (i32.const 1)) diff --git a/test/regress/ext:stack-switching/suspend21.bin.wast b/test/regress/ext:stack-switching/suspend21.bin.wast new file mode 100644 index 000000000..11d0bd7e6 --- /dev/null +++ b/test/regress/ext:stack-switching/suspend21.bin.wast @@ -0,0 +1,15 @@ +(module definition binary + "\00\61\73\6d\01\00\00\00\01\92\80\80\80\00\04\60" + "\00\01\7f\5d\00\60\00\00\60\04\7d\7c\7f\7e\01\7c" + "\03\84\80\80\80\00\03\03\00\00\0d\83\80\80\80\00" + "\01\00\02\07\88\80\80\80\00\01\04\6d\61\69\6e\00" + "\02\09\85\80\80\80\00\01\03\00\01\01\0a\d0\80\80" + "\80\00\03\8e\80\80\80\00\00\e2\00\44\cd\cc\cc\cc" + "\cc\ec\58\c0\0f\0b\9c\80\80\80\00\00\41\c3\00\43" + "\00\00\90\40\44\9a\99\99\99\99\99\b9\3f\41\01\42" + "\d0\00\10\00\1a\0f\0b\96\80\80\80\00\00\02\63\01" + "\d2\01\e0\01\e3\01\01\00\00\00\1a\d0\01\0b\e3\01" + "\00\0b" +) +(module instance) +(assert_return (invoke "main") (i32.const 0x43)) diff --git a/test/regress/ext:stack-switching/suspend21.wast b/test/regress/ext:stack-switching/suspend21.wast new file mode 100644 index 000000000..420489bd7 --- /dev/null +++ b/test/regress/ext:stack-switching/suspend21.wast @@ -0,0 +1,28 @@ +;; Tests for parameter overlaps during stack compression. +(module + (type $f1 (func (result i32))) + (type $c1 (cont $f1)) + (tag $ts) + + (func $top (param f32 f64 i32 i64) (result f64) + (suspend $ts) + (return (f64.const -99.7)) + ) + (func $bot (result i32) + (i32.const 67) + (call $top (f32.const 4.5) (f64.const 0.1) (i32.const 1) (i64.const 80)) + (drop) + (return) + ) + (elem declare func $bot) + (func (export "main") (result i32) + (block (result (ref null $c1)) + (resume $c1 (on $ts 0) (cont.new $c1 (ref.func $bot))) + (drop) + (ref.null $c1) + ) + (resume $c1) + ) +) + +(assert_return (invoke "main") (i32.const 67)) diff --git a/test/regress/ext:stack-switching/suspend22.bin.wast b/test/regress/ext:stack-switching/suspend22.bin.wast new file mode 100644 index 000000000..2c31ca201 --- /dev/null +++ b/test/regress/ext:stack-switching/suspend22.bin.wast @@ -0,0 +1,14 @@ +(module definition binary + "\00\61\73\6d\01\00\00\00\01\94\80\80\80\00\05\60" + "\00\01\7f\5d\00\60\01\7f\01\7f\5d\02\60\00\02\7f" + "\63\03\03\84\80\80\80\00\03\02\00\00\0d\83\80\80" + "\80\00\01\00\02\07\88\80\80\80\00\01\04\6d\61\69" + "\6e\00\02\09\85\80\80\80\00\01\03\00\01\01\0a\cb" + "\80\80\80\00\03\97\80\80\80\00\00\20\00\45\04\7f" + "\41\00\e2\00\05\20\00\41\01\6b\10\00\41\01\6a\0b" + "\0b\87\80\80\80\00\00\41\c8\01\10\00\0b\9d\80\80" + "\80\00\01\01\63\03\02\04\d2\01\e0\01\e3\01\01\00" + "\00\00\0f\0b\21\00\1a\41\00\20\00\e3\03\00\0b" +) +(module instance) +(assert_return (invoke "main") (i32.const 0xc8)) diff --git a/test/regress/ext:stack-switching/suspend22.wast b/test/regress/ext:stack-switching/suspend22.wast new file mode 100644 index 000000000..feda3f26f --- /dev/null +++ b/test/regress/ext:stack-switching/suspend22.wast @@ -0,0 +1,44 @@ +;; Deep recursion (200-deep), suspend at base, resume. +;; Tests 201 identical frames compressed/decompressed. +(module + (type $f_start (func (result i32))) + (type $c_start (cont $f_start)) + (type $f_resume (func (param i32) (result i32))) + (type $c_resume (cont $f_resume)) + (tag $ts (param i32) (result i32)) + + (func $foo (param i32) (result i32) + (if (result i32) (i32.eqz (local.get 0)) + (then + (suspend $ts (i32.const 0)) + ) + (else + (i32.add + (call $foo (i32.sub (local.get 0) (i32.const 1))) + (i32.const 1)) + ) + ) + ) + + (func $start (result i32) + (call $foo (i32.const 200)) + ) + (elem declare func $start) + + ;; main: catches suspension (payload=0), resumes with 0 + ;; Each recursive frame adds 1 on return: result = 200 + (func (export "main") (result i32) + (local $k (ref null $c_resume)) + (block (result i32 (ref null $c_resume)) + (resume $c_start (on $ts 0) (cont.new $c_start (ref.func $start))) + (return) + ) + (local.set $k) + (drop) ;; drop tag payload (0) + (i32.const 0) + (local.get $k) + (resume $c_resume) + ) +) + +(assert_return (invoke "main") (i32.const 200)) diff --git a/test/regress/ext:stack-switching/suspend23.bin.wast b/test/regress/ext:stack-switching/suspend23.bin.wast new file mode 100644 index 000000000..bbdb4223c --- /dev/null +++ b/test/regress/ext:stack-switching/suspend23.bin.wast @@ -0,0 +1,16 @@ +(module definition binary + "\00\61\73\6d\01\00\00\00\01\99\80\80\80\00\06\60" + "\00\01\7f\5d\00\60\01\7f\01\7f\5d\02\60\01\7c\01" + "\7f\60\02\7e\7d\01\7f\03\86\80\80\80\00\05\04\05" + "\02\00\00\0d\83\80\80\80\00\01\00\00\07\88\80\80" + "\80\00\01\04\6d\61\69\6e\00\04\09\85\80\80\80\00" + "\01\03\00\01\03\0a\d9\80\80\80\00\05\88\80\80\80" + "\00\00\20\00\aa\e2\00\6a\0b\8b\80\80\80\00\00\20" + "\00\b9\10\00\20\01\a8\6a\0b\90\80\80\80\00\00\20" + "\00\ac\43\00\00\00\40\10\01\41\e4\00\6a\0b\86\80" + "\80\80\00\00\41\05\10\02\0b\96\80\80\80\00\00\41" + "\0a\02\63\03\d2\03\e0\01\e3\01\01\00\00\00\0f\0b" + "\e3\03\00\0b" +) +(module instance) +(assert_return (invoke "main") (i32.const 0x75)) diff --git a/test/regress/ext:stack-switching/suspend23.wast b/test/regress/ext:stack-switching/suspend23.wast new file mode 100644 index 000000000..2f6bce158 --- /dev/null +++ b/test/regress/ext:stack-switching/suspend23.wast @@ -0,0 +1,48 @@ +;; 3-function chain with mixed types. +;; Tests 3 heterogeneous frames with i32/i64/f32/f64. +(module + (type $f (func (result i32))) + (type $c (cont $f)) + (type $fb (func (param i32) (result i32))) + (type $cb (cont $fb)) + (tag $fetch (result i32)) + + ;; func_c: suspends, gets fetched value, adds to trunc(param) + (func $func_c (param f64) (result i32) + (i32.add + (i32.trunc_f64_s (local.get 0)) + (suspend $fetch)) + ) + + ;; func_b: calls func_c with converted params, adds trunc(f32 param) + (func $func_b (param i64 f32) (result i32) + (i32.add + (call $func_c (f64.convert_i64_s (local.get 0))) + (i32.trunc_f32_s (local.get 1))) + ) + + ;; func_a: calls func_b, adds 100 + (func $func_a (param i32) (result i32) + (i32.add + (call $func_b (i64.extend_i32_s (local.get 0)) (f32.const 2.0)) + (i32.const 100)) + ) + + (func $start (result i32) + (call $func_a (i32.const 5)) + ) + (elem declare func $start) + + ;; main: catches suspension, resumes with 10 + ;; Result: trunc(5.0)+10=15, +trunc(2.0)=17, +100=117 + (func (export "main") (result i32) + (i32.const 10) + (block (result (ref null $cb)) + (resume $c (on $fetch 0) (cont.new $c (ref.func $start))) + (return) + ) + (resume $cb) + ) +) + +(assert_return (invoke "main") (i32.const 117)) diff --git a/test/regress/ext:stack-switching/suspend24.bin.wast b/test/regress/ext:stack-switching/suspend24.bin.wast new file mode 100644 index 000000000..fe1e1be0b --- /dev/null +++ b/test/regress/ext:stack-switching/suspend24.bin.wast @@ -0,0 +1,17 @@ +(module definition binary + "\00\61\73\6d\01\00\00\00\01\9a\80\80\80\00\05\60" + "\00\01\7e\5d\00\60\01\7e\01\7e\5d\02\60\08\7f\7e" + "\7d\7c\7f\7e\7d\7c\01\7e\03\84\80\80\80\00\03\04" + "\00\00\0d\83\80\80\80\00\01\00\00\07\88\80\80\80" + "\00\01\04\6d\61\69\6e\00\02\09\85\80\80\80\00\01" + "\03\00\01\01\0a\f0\80\80\80\00\03\9f\80\80\80\00" + "\00\20\00\ac\20\01\7c\20\02\ae\20\03\b0\7c\7c\20" + "\04\ac\20\05\7c\20\06\ae\20\07\b0\7c\7c\7c\0b\ab" + "\80\80\80\00\00\e2\00\1a\41\01\42\02\43\00\00\40" + "\40\44\00\00\00\00\00\00\10\40\41\05\42\06\43\00" + "\00\e0\40\44\00\00\00\00\00\00\20\40\10\00\0b\96" + "\80\80\80\00\00\42\00\02\63\03\d2\01\e0\01\e3\01" + "\01\00\00\00\0f\0b\e3\03\00\0b" +) +(module instance) +(assert_return (invoke "main") (i64.const 0x24)) diff --git a/test/regress/ext:stack-switching/suspend24.wast b/test/regress/ext:stack-switching/suspend24.wast new file mode 100644 index 000000000..842843823 --- /dev/null +++ b/test/regress/ext:stack-switching/suspend24.wast @@ -0,0 +1,40 @@ +;; 8 parameters of mixed types. +;; Tests frame with 8 mixed-type values packed in compression. +(module + (type $f (func (result i64))) + (type $c (cont $f)) + (type $fb (func (param i64) (result i64))) + (type $cb (cont $fb)) + (tag $ts (result i64)) + + (func $top (param i32 i64 f32 f64 i32 i64 f32 f64) (result i64) + (i64.add + (i64.add + (i64.add (i64.extend_i32_s (local.get 0)) (local.get 1)) + (i64.add (i64.trunc_f32_s (local.get 2)) (i64.trunc_f64_s (local.get 3)))) + (i64.add + (i64.add (i64.extend_i32_s (local.get 4)) (local.get 5)) + (i64.add (i64.trunc_f32_s (local.get 6)) (i64.trunc_f64_s (local.get 7))))) + ) + + (func $bot (result i64) + (suspend $ts) + (drop) + (call $top + (i32.const 1) (i64.const 2) (f32.const 3.0) (f64.const 4.0) + (i32.const 5) (i64.const 6) (f32.const 7.0) (f64.const 8.0)) + ) + (elem declare func $bot) + + ;; main: catches suspension, resumes; result = 1+2+3+4+5+6+7+8 = 36 + (func (export "main") (result i64) + (i64.const 0) + (block (result (ref null $cb)) + (resume $c (on $ts 0) (cont.new $c (ref.func $bot))) + (return) + ) + (resume $cb) + ) +) + +(assert_return (invoke "main") (i64.const 36)) diff --git a/test/regress/ext:stack-switching/suspend25.bin.wast b/test/regress/ext:stack-switching/suspend25.bin.wast new file mode 100644 index 000000000..00b797501 --- /dev/null +++ b/test/regress/ext:stack-switching/suspend25.bin.wast @@ -0,0 +1,17 @@ +(module definition binary + "\00\61\73\6d\01\00\00\00\01\94\80\80\80\00\05\60" + "\00\01\7f\5d\00\60\01\7f\01\7f\5d\02\60\00\02\7f" + "\63\03\03\83\80\80\80\00\02\00\00\0d\83\80\80\80" + "\00\01\00\02\07\88\80\80\80\00\01\04\6d\61\69\6e" + "\00\01\09\85\80\80\80\00\01\03\00\01\00\0a\f1\80" + "\80\80\00\02\a7\80\80\80\00\01\01\7f\41\01\41\01" + "\e2\00\6a\21\00\20\00\20\00\41\01\6a\e2\00\6a\21" + "\00\20\00\20\00\41\01\6a\e2\00\6a\21\00\20\00\0b" + "\bf\80\80\80\00\01\01\63\03\02\04\d2\00\e0\01\e3" + "\01\01\00\00\00\0f\0b\21\00\1a\02\04\41\0a\20\00" + "\e3\03\01\00\00\00\0f\0b\21\00\1a\02\04\41\14\20" + "\00\e3\03\01\00\00\00\0f\0b\21\00\1a\41\03\20\00" + "\e3\03\00\0b" +) +(module instance) +(assert_return (invoke "main") (i32.const 0x22)) diff --git a/test/regress/ext:stack-switching/suspend25.wast b/test/regress/ext:stack-switching/suspend25.wast new file mode 100644 index 000000000..89eaec50c --- /dev/null +++ b/test/regress/ext:stack-switching/suspend25.wast @@ -0,0 +1,55 @@ +;; Triple suspend/resume on same continuation. +;; Tests repeated compress/decompress cycles, varying PC each time. +;; Flow: yield(1)->10, yield(12)->20, yield(32)->3, return 34 +(module + (type $f (func (result i32))) + (type $c (cont $f)) + (type $fb (func (param i32) (result i32))) + (type $cb (cont $fb)) + (tag $yield (param i32) (result i32)) + + (func $worker (result i32) + (local $acc i32) + ;; yield 1, receive 10: acc = 1 + 10 = 11 + (local.set $acc (i32.add (i32.const 1) (suspend $yield (i32.const 1)))) + ;; yield 12, receive 20: acc = 11 + 20 = 31 + (local.set $acc (i32.add (local.get $acc) + (suspend $yield (i32.add (local.get $acc) (i32.const 1))))) + ;; yield 32, receive 3: acc = 31 + 3 = 34 + (local.set $acc (i32.add (local.get $acc) + (suspend $yield (i32.add (local.get $acc) (i32.const 1))))) + (local.get $acc) + ) + (elem declare func $worker) + + (func (export "main") (result i32) + (local $k (ref null $cb)) + ;; Start worker, catch first yield (value 1) + (block (result i32 (ref null $cb)) + (resume $c (on $yield 0) (cont.new $c (ref.func $worker))) + (return) + ) + (local.set $k) + (drop) + ;; Resume with 10, catch second yield (value 12) + (block (result i32 (ref null $cb)) + (resume $cb (on $yield 0) (i32.const 10) (local.get $k)) + (return) + ) + (local.set $k) + (drop) + ;; Resume with 20, catch third yield (value 32) + (block (result i32 (ref null $cb)) + (resume $cb (on $yield 0) (i32.const 20) (local.get $k)) + (return) + ) + (local.set $k) + (drop) + ;; Final resume with 3, worker returns 34 + (i32.const 3) + (local.get $k) + (resume $cb) + ) +) + +(assert_return (invoke "main") (i32.const 34)) diff --git a/test/regress/ext:stack-switching/suspend26.bin.wast b/test/regress/ext:stack-switching/suspend26.bin.wast new file mode 100644 index 000000000..2bebda823 --- /dev/null +++ b/test/regress/ext:stack-switching/suspend26.bin.wast @@ -0,0 +1,19 @@ +(module definition binary + "\00\61\73\6d\01\00\00\00\01\96\80\80\80\00\05\60" + "\00\03\7f\7e\7d\5d\00\60\01\7f\03\7f\7e\7d\5d\02" + "\60\00\01\7f\03\84\80\80\80\00\03\02\00\00\0d\83" + "\80\80\80\00\01\00\04\07\88\80\80\80\00\01\04\6d" + "\61\69\6e\00\02\09\85\80\80\80\00\01\03\00\01\01" + "\0a\c6\80\80\80\00\03\99\80\80\80\00\01\01\7f\e2" + "\00\21\01\20\00\20\01\6a\20\00\41\02\6c\ac\20\00" + "\41\01\6a\b2\0b\86\80\80\80\00\00\41\05\10\00\0b" + "\97\80\80\80\00\00\41\e4\00\02\63\03\d2\01\e0\01" + "\e3\01\01\00\00\00\0f\0b\e3\03\00\0b" +) +(module instance) +(assert_return + (invoke "main") + (i32.const 0x69) + (i64.const 0xa) + (f32.const 0x1.8p+2) +) diff --git a/test/regress/ext:stack-switching/suspend26.wast b/test/regress/ext:stack-switching/suspend26.wast new file mode 100644 index 000000000..322ad398b --- /dev/null +++ b/test/regress/ext:stack-switching/suspend26.wast @@ -0,0 +1,35 @@ +;; Multi-value return (i32, i64, f32) through suspended stack. +(module + (type $f (func (result i32 i64 f32))) + (type $c (cont $f)) + (type $fb (func (param i32) (result i32 i64 f32))) + (type $cb (cont $fb)) + (tag $ts (result i32)) + + (func $compute (param i32) (result i32 i64 f32) + (local $received i32) + (local.set $received (suspend $ts)) + ;; return (param + received, i64(param * 2), f32(param + 1)) + (i32.add (local.get 0) (local.get $received)) + (i64.extend_i32_s (i32.mul (local.get 0) (i32.const 2))) + (f32.convert_i32_s (i32.add (local.get 0) (i32.const 1))) + ) + + (func $start (result i32 i64 f32) + (call $compute (i32.const 5)) + ) + (elem declare func $start) + + ;; main: catches suspension, resumes with 100 + ;; With input 5: returns (5+100=105, i64(5*2)=10, f32(5+1)=6.0) + (func (export "main") (result i32 i64 f32) + (i32.const 100) + (block (result (ref null $cb)) + (resume $c (on $ts 0) (cont.new $c (ref.func $start))) + (return) + ) + (resume $cb) + ) +) + +(assert_return (invoke "main") (i32.const 105) (i64.const 10) (f32.const 6.0)) diff --git a/test/regress/ext:stack-switching/suspend27.bin.wast b/test/regress/ext:stack-switching/suspend27.bin.wast new file mode 100644 index 000000000..a44757cc0 --- /dev/null +++ b/test/regress/ext:stack-switching/suspend27.bin.wast @@ -0,0 +1,15 @@ +(module definition binary + "\00\61\73\6d\01\00\00\00\01\8e\80\80\80\00\04\60" + "\00\01\7f\5d\00\60\01\7f\01\7f\5d\02\03\87\80\80" + "\80\00\06\00\00\00\00\00\00\0d\83\80\80\80\00\01" + "\00\00\07\88\80\80\80\00\01\04\6d\61\69\6e\00\05" + "\09\85\80\80\80\00\01\03\00\01\04\0a\d5\80\80\80" + "\00\06\84\80\80\80\00\00\e2\00\0b\87\80\80\80\00" + "\00\41\0a\10\00\6a\0b\87\80\80\80\00\00\41\14\10" + "\01\6a\0b\87\80\80\80\00\00\41\1e\10\02\6a\0b\87" + "\80\80\80\00\00\41\28\10\03\6a\0b\96\80\80\80\00" + "\00\41\05\02\63\03\d2\04\e0\01\e3\01\01\00\00\00" + "\0f\0b\e3\03\00\0b" +) +(module instance) +(assert_return (invoke "main") (i32.const 0x69)) diff --git a/test/regress/ext:stack-switching/suspend27.wast b/test/regress/ext:stack-switching/suspend27.wast new file mode 100644 index 000000000..613a9ffbc --- /dev/null +++ b/test/regress/ext:stack-switching/suspend27.wast @@ -0,0 +1,45 @@ +;; 5-function call chain: func_a -> func_b -> func_c -> func_d -> func_e. +;; func_e suspends; each function holds an i32 on the operand stack and adds on return. +;; Tests 5 distinct non-recursive frames with pending operand stack values. +(module + (type $f (func (result i32))) + (type $c (cont $f)) + (type $fb (func (param i32) (result i32))) + (type $cb (cont $fb)) + (tag $fetch (result i32)) + + (func $func_e (result i32) + (suspend $fetch) + ) + + (func $func_d (result i32) + (i32.add (i32.const 10) (call $func_e)) + ) + + (func $func_c (result i32) + (i32.add (i32.const 20) (call $func_d)) + ) + + (func $func_b (result i32) + (i32.add (i32.const 30) (call $func_c)) + ) + + (func $func_a (result i32) + (i32.add (i32.const 40) (call $func_b)) + ) + + (elem declare func $func_a) + + ;; main: catches suspension, resumes with 5 + ;; Result: 5 + 10 + 20 + 30 + 40 = 105 + (func (export "main") (result i32) + (i32.const 5) + (block (result (ref null $cb)) + (resume $c (on $fetch 0) (cont.new $c (ref.func $func_a))) + (return) + ) + (resume $cb) + ) +) + +(assert_return (invoke "main") (i32.const 105)) diff --git a/test/regress/ext:stack-switching/suspend28.bin.wast b/test/regress/ext:stack-switching/suspend28.bin.wast new file mode 100644 index 000000000..1b9cd49d3 --- /dev/null +++ b/test/regress/ext:stack-switching/suspend28.bin.wast @@ -0,0 +1,15 @@ +(module definition binary + "\00\61\73\6d\01\00\00\00\01\9b\80\80\80\00\06\60" + "\00\01\7e\5d\00\60\01\7e\01\7e\5d\02\60\03\7f\7e" + "\7d\01\7e\60\00\02\7e\63\03\03\84\80\80\80\00\03" + "\04\00\00\0d\83\80\80\80\00\01\00\02\07\88\80\80" + "\80\00\01\04\6d\61\69\6e\00\02\09\85\80\80\80\00" + "\01\03\00\01\01\0a\d2\80\80\80\00\03\a2\80\80\80" + "\00\00\20\00\45\04\7e\20\01\e2\00\05\20\00\41\01" + "\6b\20\01\20\00\ac\7c\20\02\43\00\00\00\3f\92\10" + "\00\0b\0b\8d\80\80\80\00\00\41\32\42\00\43\00\00" + "\80\3f\10\00\0b\93\80\80\80\00\00\02\05\d2\01\e0" + "\01\e3\01\01\00\00\00\0f\0b\e3\03\00\0b" +) +(module instance) +(assert_return (invoke "main") (i64.const 0x4fb)) diff --git a/test/regress/ext:stack-switching/suspend28.wast b/test/regress/ext:stack-switching/suspend28.wast new file mode 100644 index 000000000..b115ef793 --- /dev/null +++ b/test/regress/ext:stack-switching/suspend28.wast @@ -0,0 +1,42 @@ +;; 50-deep recursion with 3 locals per frame (i32, i64, f32). +;; Accumulates i32 counter as i64 sum: 1+2+...+50 = 1275. +;; f32 param is preserved but unused in result (exercises f32 compression). +;; Tests: 51 frames * 3 values each = 150+ mixed-type values. +(module + (type $f (func (result i64))) + (type $c (cont $f)) + (type $fb (func (param i64) (result i64))) + (type $cb (cont $fb)) + (tag $ts (param i64) (result i64)) + + (func $foo (param i32 i64 f32) (result i64) + (if (result i64) (i32.eqz (local.get 0)) + (then + (suspend $ts (local.get 1)) + ) + (else + (call $foo + (i32.sub (local.get 0) (i32.const 1)) + (i64.add (local.get 1) (i64.extend_i32_s (local.get 0))) + (f32.add (local.get 2) (f32.const 0.5))) + ) + ) + ) + + (func $start (result i64) + (call $foo (i32.const 50) (i64.const 0) (f32.const 1.0)) + ) + (elem declare func $start) + + ;; main: catches suspension with accumulated sum (1275), resumes with it + (func (export "main") (result i64) + (block (result i64 (ref null $cb)) + (resume $c (on $ts 0) (cont.new $c (ref.func $start))) + (return) + ) + ;; stack: [i64=1275, ref $cb] — resume with the payload + (resume $cb) + ) +) + +(assert_return (invoke "main") (i64.const 1275)) diff --git a/test/regress/ext:stack-switching/suspend29.bin.wast b/test/regress/ext:stack-switching/suspend29.bin.wast new file mode 100644 index 000000000..015cbc78e --- /dev/null +++ b/test/regress/ext:stack-switching/suspend29.bin.wast @@ -0,0 +1,16 @@ +(module definition binary + "\00\61\73\6d\01\00\00\00\01\a6\80\80\80\00\09\60" + "\00\01\7f\5d\00\60\01\7f\01\7f\5d\02\60\01\7e\01" + "\7f\5d\04\60\01\7e\01\7e\60\00\02\7e\63\05\60\00" + "\02\7f\63\03\03\83\80\80\80\00\02\00\00\0d\85\80" + "\80\80\00\02\00\02\00\06\07\88\80\80\80\00\01\04" + "\6d\61\69\6e\00\01\09\85\80\80\80\00\01\03\00\01" + "\00\0a\d5\80\80\80\00\02\99\80\80\80\00\02\01\7f" + "\01\7e\41\01\e2\00\21\00\20\00\ac\e2\01\21\01\20" + "\00\20\01\a7\6a\0b\b1\80\80\80\00\02\01\63\03\01" + "\63\05\02\08\d2\00\e0\01\e3\01\01\00\00\00\0f\0b" + "\21\00\1a\02\07\41\0a\20\00\e3\03\01\00\01\00\0f" + "\0b\21\01\1a\42\14\20\01\e3\05\00\0b" +) +(module instance) +(assert_return (invoke "main") (i32.const 0x1e)) diff --git a/test/regress/ext:stack-switching/suspend29.wast b/test/regress/ext:stack-switching/suspend29.wast new file mode 100644 index 000000000..bf8f1abd5 --- /dev/null +++ b/test/regress/ext:stack-switching/suspend29.wast @@ -0,0 +1,53 @@ +;; Two different tags, two suspend/resume cycles. +;; Worker first suspends with $tag_a (i32->i32), then with $tag_b (i64->i64). +;; Tests: continuation type changes between resumes. +(module + (type $f (func (result i32))) + (type $c (cont $f)) + ;; After first suspend (tag_a): continuation takes i32, returns i32 + (type $fa (func (param i32) (result i32))) + (type $ca (cont $fa)) + ;; After second suspend (tag_b): continuation takes i64, returns i32 + (type $fb (func (param i64) (result i32))) + (type $cb (cont $fb)) + + (tag $tag_a (param i32) (result i32)) + (tag $tag_b (param i64) (result i64)) + + (func $worker (result i32) + (local $a i32) + (local $b i64) + ;; First suspend via tag_a: send 1, receive i32 back + (local.set $a (suspend $tag_a (i32.const 1))) + ;; Second suspend via tag_b: send i64(a), receive i64 back + (local.set $b (suspend $tag_b (i64.extend_i32_s (local.get $a)))) + ;; Return a + i32.wrap(b) + (i32.add (local.get $a) (i32.wrap_i64 (local.get $b))) + ) + (elem declare func $worker) + + (func (export "main") (result i32) + (local $ka (ref null $ca)) + (local $kb (ref null $cb)) + ;; Start worker, catch tag_a suspension (payload=1) + (block (result i32 (ref null $ca)) + (resume $c (on $tag_a 0) (cont.new $c (ref.func $worker))) + (return) + ) + (local.set $ka) + (drop) ;; drop tag_a payload (1) + ;; Resume with 10, catch tag_b suspension (payload=i64(10)) + (block (result i64 (ref null $cb)) + (resume $ca (on $tag_b 0) (i32.const 10) (local.get $ka)) + (return) + ) + (local.set $kb) + (drop) ;; drop tag_b payload (i64(10)) + ;; Resume with i64(20), worker returns 10 + 20 = 30 + (i64.const 20) + (local.get $kb) + (resume $cb) + ) +) + +(assert_return (invoke "main") (i32.const 30)) diff --git a/test/regress/ext:stack-switching/suspend30.bin.wast b/test/regress/ext:stack-switching/suspend30.bin.wast new file mode 100644 index 000000000..887b6be67 --- /dev/null +++ b/test/regress/ext:stack-switching/suspend30.bin.wast @@ -0,0 +1,19 @@ +(module definition binary + "\00\61\73\6d\01\00\00\00\01\9a\80\80\80\00\06\60" + "\00\01\7f\5d\00\60\01\7f\01\7f\5d\02\60\02\7f\7f" + "\01\7f\60\00\02\7f\63\03\03\87\80\80\80\00\06\02" + "\04\00\00\00\00\0d\83\80\80\80\00\01\00\02\07\91" + "\80\80\80\00\02\05\74\65\73\74\33\00\04\05\74\65" + "\73\74\35\00\05\09\89\80\80\80\00\02\03\00\01\02" + "\03\00\01\03\0a\f4\80\80\80\00\06\86\80\80\80\00" + "\00\20\00\e2\00\0b\99\80\80\80\00\00\20\00\45\04" + "\7f\20\01\10\00\05\20\00\41\01\6b\20\01\20\00\6a" + "\10\01\0b\0b\88\80\80\80\00\00\41\03\41\00\10\01" + "\0b\88\80\80\80\00\00\41\05\41\00\10\01\0b\93\80" + "\80\80\00\00\02\05\d2\02\e0\01\e3\01\01\00\00\00" + "\0f\0b\e3\03\00\0b\93\80\80\80\00\00\02\05\d2\03" + "\e0\01\e3\01\01\00\00\00\0f\0b\e3\03\00\0b" +) +(module instance) +(assert_return (invoke "test3") (i32.const 0x6)) +(assert_return (invoke "test5") (i32.const 0xf)) diff --git a/test/regress/ext:stack-switching/suspend30.wast b/test/regress/ext:stack-switching/suspend30.wast new file mode 100644 index 000000000..cd6dd5a26 --- /dev/null +++ b/test/regress/ext:stack-switching/suspend30.wast @@ -0,0 +1,61 @@ +;; Recursion + helper function chain. +;; $recurse calls itself to depth 0, then calls $helper which suspends. +;; Creates interleaved frame pattern: recurse, recurse, ..., recurse, helper. +;; Accumulates triangular number: depth N -> N*(N+1)/2. +(module + (type $f (func (result i32))) + (type $c (cont $f)) + (type $fb (func (param i32) (result i32))) + (type $cb (cont $fb)) + (tag $ts (param i32) (result i32)) + + (func $helper (param i32) (result i32) + (suspend $ts (local.get 0)) + ) + + ;; recurse(n, acc): if n==0, call helper(acc); else recurse(n-1, acc+n) + (func $recurse (param i32 i32) (result i32) + (if (result i32) (i32.eqz (local.get 0)) + (then + (call $helper (local.get 1)) + ) + (else + (call $recurse + (i32.sub (local.get 0) (i32.const 1)) + (i32.add (local.get 1) (local.get 0))) + ) + ) + ) + + (func $start3 (result i32) + (call $recurse (i32.const 3) (i32.const 0)) + ) + (elem declare func $start3) + + (func $start5 (result i32) + (call $recurse (i32.const 5) (i32.const 0)) + ) + (elem declare func $start5) + + ;; depth 3: acc = 3+2+1 = 6, helper suspends with 6, resume with 6 -> returns 6 + (func (export "test3") (result i32) + (block (result i32 (ref null $cb)) + (resume $c (on $ts 0) (cont.new $c (ref.func $start3))) + (return) + ) + ;; stack: [i32=6, ref $cb] — resume with the payload + (resume $cb) + ) + + ;; depth 5: acc = 5+4+3+2+1 = 15, helper suspends with 15, resume with 15 -> returns 15 + (func (export "test5") (result i32) + (block (result i32 (ref null $cb)) + (resume $c (on $ts 0) (cont.new $c (ref.func $start5))) + (return) + ) + (resume $cb) + ) +) + +(assert_return (invoke "test3") (i32.const 6)) +(assert_return (invoke "test5") (i32.const 15)) diff --git a/test/unittest/CompressionTest.v3 b/test/unittest/CompressionTest.v3 new file mode 100644 index 000000000..3e7ebf7dc --- /dev/null +++ b/test/unittest/CompressionTest.v3 @@ -0,0 +1,452 @@ +// Copyright 2025 Wizard authors. All rights reserved. +// See LICENSE for details of Apache 2.0 license. + +// Zero-initialized reentry label used as a placeholder in test frames. +// TODO: fuzz this for testing +var DUMMY_REENTRY: TargetReentryLabel; + +// An adapter that erases the compressed stack type to perform strategy-agnostic roundtrip tests. +class CompressionStrategyAdapter { + def roundtrip: Array -> Array; + new(roundtrip) { } +} + +def strategies = [ + ("naive", CompressionStrategyAdapter.new(naiveRoundtrip)), + ("packed", CompressionStrategyAdapter.new(packedRoundtrip)) +]; + +def naive = NaiveCompressionStrategy.new(); +def packed = PackedCompressionStrategy.new(); + +def naiveRoundtrip(frames: Array) -> Array { + return naive.decompress(naive.compress(frames)); +} +def packedRoundtrip(frames: Array) -> Array { + return packed.decompress(packed.compress(frames)); +} + +// Register all tests across all strategies: for each test, register it once for every strategy. +def X_ = registerAll([ + ("i32", test_i32), + ("i64", test_i64), + ("f32", test_f32), + ("f64", test_f64), + ("v128", test_v128), + ("ref", test_ref), + ("empty_frames", test_empty_frames), + ("single_empty_frame", test_single_empty_frame), + ("multi_empty_frames", test_multi_empty_frames), + ("multi_vals", test_multi_vals), + ("multi_frames", test_multi_frames), + ("single_val_per_frame", test_single_val_per_frame), + ("cont", test_cont), + ("frame_metadata", test_frame_metadata), + ("repeated_vals", test_repeated_vals), + ("repeated_across_frames", test_repeated_across_frames), + ("many_diverse_vals", test_many_diverse_vals), + ("many_frames_diverse", test_many_frames_diverse), + ("refs_mixed", test_refs_mixed), + ("refs_shared_across_frames", test_refs_shared_across_frames), + ("large_uniform_frames", test_large_uniform_frames) +]); + +def registerAll(tests: Array<(string, CompressionTester -> void)>) { + for (t in tests) { + for (s in strategies) { + var name = Strings.format2("compression:%s:%s", s.0, t.0); + var adapter = s.1; + UnitTests.registerT(name, "", CompressionTester.new(_, adapter), t.1); + } + } +} + +// Test helper that wraps a Tester with compression-specific utilities. +class CompressionTester(t: Tester, adapter: CompressionStrategyAdapter) { + def mb = ModuleBuilder.new(); + var dummy_func: WasmFunction; + + def getDummyFunc() -> WasmFunction { + if (dummy_func == null) { + mb.sig(SigCache.v_v).code([]); + var err = ErrorGen.new("CompressionTest"); + var instance = Instantiator.new(Extension.set.all, mb.module, [], err).run(); + if (!err.ok()) { t.fail(err.error_msg); return null; } + dummy_func = WasmFunction.!(instance.functions[0]); + } + return dummy_func; + } + + def make_frame(pc: int, vals: Array) -> RelocatableFrame { + var func = getDummyFunc(); + return RelocatableFrame(func, pc, DUMMY_REENTRY, DUMMY_REENTRY, vals); + } + + def roundtrip(frames: Array) -> Array { + return adapter.roundtrip(frames); + } + + def assert_roundtrip_vals(vals: Array) { + var frames = [make_frame(0, vals)]; + var got = roundtrip(frames); + assert_length(1, got.length, "frames"); + assert_vals_eq(vals, got[0].vals); + } + + def assert_length(expected: int, got: int, what: string) { + if (expected != got) t.fail3("expected %d %s, got %d", expected, what, got); + } + + def assert_vals_eq(expected: Array, got: Array) { + assert_length(expected.length, got.length, "values"); + for (i < expected.length) { + if (expected[i] != got[i]) { + t.fail3("value[%d]: expected %q, got %q", + i, Values.render(expected[i], _), Values.render(got[i], _)); + } + } + } + + def assert_frames_eq(expected: Array, got: Array) { + assert_length(expected.length, got.length, "frames"); + for (i < expected.length) { + var e = expected[i], g = got[i]; + if (e.func != g.func) t.fail1("frame[%d]: func mismatch", i); + if (e.pc != g.pc) t.fail3("frame[%d]: expected pc=%d, got pc=%d", i, e.pc, g.pc); + assert_vals_eq(e.vals, g.vals); + } + } +} + +// ===== Compression roundtrip tests ===== + +def test_i32(t: CompressionTester) { + t.assert_roundtrip_vals([Value.I32(0)]); + t.assert_roundtrip_vals([Value.I32(1)]); + t.assert_roundtrip_vals([Value.I32(0xFFFFFFFFu)]); + t.assert_roundtrip_vals([Value.I32(0x80000000u)]); + t.assert_roundtrip_vals([Value.I32(42), Value.I32(0), Value.I32(0xDEADBEEFu)]); +} + +def test_i64(t: CompressionTester) { + t.assert_roundtrip_vals([Value.I64(0)]); + t.assert_roundtrip_vals([Value.I64(1)]); + t.assert_roundtrip_vals([Value.I64(0xFFFFFFFFFFFFFFFFuL)]); + t.assert_roundtrip_vals([Value.I64(0x8000000000000000uL)]); + t.assert_roundtrip_vals([Value.I64(42), Value.I64(0xDEADCAFEBEEF0000uL)]); +} + +def test_f32(t: CompressionTester) { + t.assert_roundtrip_vals([Value.F32(0)]); + t.assert_roundtrip_vals([Value.F32(0x3F800000u)]); // 1.0f + t.assert_roundtrip_vals([Value.F32(0x7FC00000u)]); // NaN + t.assert_roundtrip_vals([Value.F32(0xFF800000u)]); // -Inf +} + +def test_f64(t: CompressionTester) { + t.assert_roundtrip_vals([Value.F64(0)]); + t.assert_roundtrip_vals([Value.F64(0x3FF0000000000000uL)]); // 1.0 + t.assert_roundtrip_vals([Value.F64(0x7FF8000000000000uL)]); // NaN + t.assert_roundtrip_vals([Value.F64(0xFFF0000000000000uL)]); // -Inf +} + +def test_v128(t: CompressionTester) { + t.assert_roundtrip_vals([Value.V128(0, 0)]); + t.assert_roundtrip_vals([Value.V128(1, 2)]); + t.assert_roundtrip_vals([Value.V128(0xFFFFFFFFFFFFFFFFuL, 0xFFFFFFFFFFFFFFFFuL)]); + t.assert_roundtrip_vals([Value.V128(0xDEADBEEFCAFE0000uL, 0x0123456789ABCDEFuL)]); +} + +def test_ref(t: CompressionTester) { + t.assert_roundtrip_vals([Value.Ref(null)]); + var obj = HeapObject.new(null, []); + t.assert_roundtrip_vals([Value.Ref(obj)]); + // Verify identity is preserved. + var frames = [t.make_frame(0, [Value.Ref(obj)])]; + var got = t.roundtrip(frames); + match (got[0].vals[0]) { + Ref(v) => if (v != obj) t.t.fail("ref identity not preserved"); + _ => t.t.fail("expected Ref value"); + } +} + +def test_empty_frames(t: CompressionTester) { + var got = t.roundtrip([]); + t.assert_length(0, got.length, "frames"); +} + +def test_single_empty_frame(t: CompressionTester) { + var frames = [t.make_frame(7, [])]; + var got = t.roundtrip(frames); + t.assert_length(1, got.length, "frames"); + t.assert_length(0, got[0].vals.length, "values"); + if (got[0].pc != 7) t.t.fail1("expected pc=7, got pc=%d", got[0].pc); +} + +def test_multi_empty_frames(t: CompressionTester) { + var frames = [t.make_frame(0, []), t.make_frame(1, []), t.make_frame(2, [])]; + var got = t.roundtrip(frames); + t.assert_length(3, got.length, "frames"); + for (i < 3) { + t.assert_length(0, got[i].vals.length, "values"); + if (got[i].pc != i) t.t.fail1("expected pc=%d, got wrong pc", i); + } +} + +def test_multi_vals(t: CompressionTester) { + var vals: Array = [ + Value.I32(1), Value.I64(2), Value.F32(3), Value.F64(4), + Value.V128(5, 6), Value.Ref(null), Value.I32(0xFFFFFFFFu) + ]; + t.assert_roundtrip_vals(vals); +} + +def test_multi_frames(t: CompressionTester) { + var f0 = t.make_frame(0, [Value.I32(10), Value.I32(20)]); + var f1 = t.make_frame(5, [Value.I64(30)]); + var f2 = t.make_frame(10, [Value.F32(40), Value.F64(50), Value.V128(60, 70)]); + var frames = [f0, f1, f2]; + var got = t.roundtrip(frames); + t.assert_frames_eq(frames, got); +} + +def test_single_val_per_frame(t: CompressionTester) { + var frames = [ + t.make_frame(0, [Value.I32(100)]), + t.make_frame(1, [Value.I64(200)]), + t.make_frame(2, [Value.F32(300)]), + t.make_frame(3, [Value.F64(400)]), + t.make_frame(4, [Value.V128(500, 600)]) + ]; + var got = t.roundtrip(frames); + t.assert_frames_eq(frames, got); +} + +def test_cont(t: CompressionTester) { + var stack = Target.newWasmStack(); + var cont = Continuations.makeContinuation(stack); + t.assert_roundtrip_vals([Value.Cont(cont)]); + // Verify stored object and version are preserved. + var frames = [t.make_frame(0, [Value.Cont(cont)])]; + var got = t.roundtrip(frames); + match (got[0].vals[0]) { + Cont(got_cont) => { + if (Continuations.getStoredStack(got_cont) != stack) t.t.fail("cont stack not preserved"); + if (Continuations.getStoredVersion(got_cont) != Continuations.getStoredVersion(cont)) t.t.fail("cont version not preserved"); + } + _ => t.t.fail("expected Cont value"); + } +} + +def test_frame_metadata(t: CompressionTester) { + var func = t.getDummyFunc(); + var frame = RelocatableFrame(func, 42, DUMMY_REENTRY, DUMMY_REENTRY, [Value.I32(99)]); + var got = t.roundtrip([frame]); + t.assert_length(1, got.length, "frames"); + if (got[0].func != func) t.t.fail("func not preserved"); + if (got[0].pc != 42) t.t.fail1("expected pc=42, got pc=%d", got[0].pc); + t.assert_vals_eq([Value.I32(99)], got[0].vals); +} + +// Many identical values within a single frame. +def test_repeated_vals(t: CompressionTester) { + var vals = Array.new(100); + for (i < vals.length) vals[i] = Value.I32(0xCAFEBABEu); + t.assert_roundtrip_vals(vals); + + // Repeat with i64. + for (i < vals.length) vals[i] = Value.I64(0xDEADBEEF00000000uL); + t.assert_roundtrip_vals(vals); + + // Repeat with the same ref object. + var obj = HeapObject.new(null, []); + for (i < vals.length) vals[i] = Value.Ref(obj); + t.assert_roundtrip_vals(vals); +} + +// Same repeating value pattern across multiple frames. +def test_repeated_across_frames(t: CompressionTester) { + var frames = Array.new(20); + var shared_vals: Array = [Value.I32(42), Value.I64(42), Value.F32(42)]; + for (i < frames.length) frames[i] = t.make_frame(i, shared_vals); + var got = t.roundtrip(frames); + t.assert_frames_eq(frames, got); +} + +// Single frame with many values of varying types and magnitudes. +def test_many_diverse_vals(t: CompressionTester) { + var vals = Vector.new(); + for (i < 50) { + vals.put(Value.I32(u32.view(i * 7))); + vals.put(Value.I64(u64.view(i) * 0x100000001uL)); + vals.put(Value.F32(u32.view(i * 31))); + vals.put(Value.F64(u64.view(i) * 0x123456789uL)); + } + t.assert_roundtrip_vals(vals.extract()); +} + +// Many frames, each with distinct values and varying sizes. +def test_many_frames_diverse(t: CompressionTester) { + var frames = Vector.new(); + for (i < 30) { + var n_vals = (i % 5) + 1; // 1 to 5 values per frame + var vals = Array.new(n_vals); + for (j < n_vals) { + var k = i * 5 + j; + match (k % 4) { + 0 => vals[j] = Value.I32(u32.view(k * 13)); + 1 => vals[j] = Value.I64(u64.view(k) * 0xABCDuL); + 2 => vals[j] = Value.F32(u32.view(k * 97)); + _ => vals[j] = Value.F64(u64.view(k) * 0x1111111111uL); + } + } + frames.put(t.make_frame(i * 3, vals)); + } + var input = frames.extract(); + var got = t.roundtrip(input); + t.assert_frames_eq(input, got); +} + +// Multiple distinct reference types in a single frame. +def test_refs_mixed(t: CompressionTester) { + var heap_obj = HeapObject.new(null, []); + var host_obj = HostObject.new(); + var host_fn = HostFunction.new("test_fn", SigCache.v_v, nop_host); + var vals: Array = [ + Value.Ref(heap_obj), + Value.Ref(host_obj), + Value.Ref(host_fn), + Value.Ref(null), + Value.Ref(heap_obj), // same object again + Value.Ref(host_obj) // same object again + ]; + var frames = [t.make_frame(0, vals)]; + var got = t.roundtrip(frames); + t.assert_length(1, got.length, "frames"); + t.assert_length(vals.length, got[0].vals.length, "values"); + // Verify identity for each ref. + for (i < vals.length) { + match (vals[i]) { + Ref(expected) => match (got[0].vals[i]) { + Ref(actual) => if (expected != actual) t.t.fail1("ref[%d]: identity not preserved", i); + _ => t.t.fail1("ref[%d]: expected Ref", i); + } + _ => ; + } + } +} + +def nop_host(args: Range) -> HostResult { return HostResult.Value0; } + +// Shared reference objects across multiple frames. +def test_refs_shared_across_frames(t: CompressionTester) { + var obj_a = HeapObject.new(null, [Value.I32(1)]); + var obj_b = HostObject.new(); + var frames = [ + t.make_frame(0, [Value.Ref(obj_a), Value.I32(10)]), + t.make_frame(1, [Value.Ref(obj_b), Value.Ref(obj_a)]), + t.make_frame(2, [Value.Ref(obj_a), Value.Ref(obj_b), Value.Ref(null)]) + ]; + var got = t.roundtrip(frames); + t.assert_frames_eq(frames, got); + // Verify cross-frame identity: obj_a in frame 0 == obj_a in frame 1 and 2. + var get_ref = get_ref_val; + var a0 = get_ref(got[0].vals[0]); + var a1 = get_ref(got[1].vals[1]); + var a2 = get_ref(got[2].vals[0]); + if (a0 != a1) t.t.fail("obj_a identity not preserved across frames 0 and 1"); + if (a0 != a2) t.t.fail("obj_a identity not preserved across frames 0 and 2"); + var b1 = get_ref(got[1].vals[0]); + var b2 = get_ref(got[2].vals[1]); + if (b1 != b2) t.t.fail("obj_b identity not preserved across frames 1 and 2"); +} + +def get_ref_val(v: Value) -> Object { + match (v) { + Ref(obj) => return obj; + _ => return null; + } +} + +// Many frames with uniform large value counts. +def test_large_uniform_frames(t: CompressionTester) { + var n_frames = 20; + var n_vals = 50; + var frames = Array.new(n_frames); + for (i < n_frames) { + var vals = Array.new(n_vals); + for (j < n_vals) vals[j] = Value.I64(u64.view(i * n_vals + j)); + frames[i] = t.make_frame(i, vals); + } + var got = t.roundtrip(frames); + t.assert_frames_eq(frames, got); +} + +// ===== Stack capture tests ===== + +// Host function callback. Set {stack} before resuming; {invoke} serves as the imported host function body. +class FrameCapturer { + var stack: WasmStack; + + def invoke(args: Range) -> HostResult { + return HostResult.Value0; + } +} + +// Tester that builds Wasm functions and captures stack frames during execution. +class StackCaptureTester extends ExeTester { + var capturer = FrameCapturer.new(); + var import_index: int; + + new(t: Tester, tiering: ExecutionStrategy) super(t, tiering) { } + + // Adds an imported v_v host function to the module and sets up the import binding. + // Must be called before codev/makeFunc. + def addHostImport() { + var import_sig = addSig(SigCache.v_v); + var import_decl = FuncDecl.new(import_sig.heaptype_index); + module.addImport("test", "capture", import_decl); + import_index = import_decl.func_index; + imports = [HostFunction.new("capture", import_sig, capturer.invoke)]; + } + + // Builds a validated function that pushes all params onto the value stack and calls the imported v_v host function, + // This ensures the value stack holds exactly the bound param values during the host call. + def buildCaptureFunc(param_types: Array) -> Function { + addHostImport(); + var main_sig = SigDecl.new(true, ValueTypes.NO_HEAPTYPES, param_types, SigCache.arr_v); + sig(main_sig); + var bc = BinBuilder.new(); + for (i < param_types.length) { + bc.put(u8.!(Opcode.LOCAL_GET.code)); + bc.put_u32leb(u32.!(i)); + } + bc.put(u8.!(Opcode.CALL.code)); + bc.put_u32leb(u32.!(import_index)); + for (i < param_types.length) { + bc.put(u8.!(Opcode.DROP.code)); + } + codev(bc.extract()); + return makeFunc(); + } +} + +// TODO: add more tests, after stack capture utils are done +// TODO: fix using {Execute.tiering} for {ExeTester} +def S = StackCaptureTester.new; +// def unused_stack_ = TestTiers.addTests2([ +// ("stack_capture_smoke", S, test_stack_capture_smoke) +// ]); + +def test_stack_capture_smoke(t: StackCaptureTester) { + var f = t.buildCaptureFunc([ValueType.I32, ValueType.I64]); + if (f == null) return; + + var s = Target.newWasmStack(); + s.reset(f); + s.bind([Value.I32(42), Value.I64(99)]); + var r = s.resume(); + + // The function should complete normally with no return values. + t.assert_req(r, Result.Value(Values.NONE)); +}