Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/engine/CodePtr.v3
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions src/engine/Sidetable.v3
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand Down
5 changes: 3 additions & 2 deletions src/engine/compression/Compression.v3
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
type WasmFrameData(func: WasmFunction, pc: int, ret_addr: u64, vals: Array<Value>) #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 { }

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use "pc" instead of "bytecode_offset"--that matches the rest of the code.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe bytecode_offset is different from pc here. bytecode_offset was initially referring to ip, which differs from pc because a non-top stack in the chain has pc pointed at the start of its resume call while ip is advanced forward. It was initially named bytecode_ip to store the partial ip as a pointer casted to a u64, but this will fail if the cur_bytecodes has been moved by the GC and the bytecode_ip will point to recycled memory after a GC call. Due to this I changed it to store the offset of ip from cur_bytecodes instead. Should I name this to ip_offset or something?


class CompressionStrategy<C> {
def compress(frames: Range<RelocatableFrame>) -> C;
Expand Down
6 changes: 3 additions & 3 deletions src/engine/compression/PackedCompressionStrategy.v3
Original file line number Diff line number Diff line change
@@ -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<PackedCompressedStack> {
def frames_builder = Vector<FrameHeader>.new();
Expand Down Expand Up @@ -32,7 +32,7 @@ class PackedCompressionStrategy extends CompressionStrategy<PackedCompressedStac

def compress(frames: Range<RelocatableFrame>) -> 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);
}
Expand Down Expand Up @@ -71,7 +71,7 @@ class PackedCompressionStrategy extends CompressionStrategy<PackedCompressedStac
for (header in comp.frames) {
var vals = Array<Value>.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();
}
Expand Down
8 changes: 5 additions & 3 deletions src/engine/x86-64/X86_64Frames.v3
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,7 @@ type X86_64FrameHandle #unboxed {
(p + X86_64InterpreterFrame.code.offset).store<Array<byte>>(bytecode);
(p + X86_64InterpreterFrame.ip.offset).store<Pointer>(Pointer.atElement(bytecode, new_pc));
(p + X86_64InterpreterFrame.eip.offset).store<Pointer>(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<Pointer>(st_ptr);
(p + X86_64InterpreterFrame.stp.offset).store<Pointer>(X86_64Frames.sidetablePtr(func.sidetable.entries, stp_val));
}

// Redirect return address to the interpreter's deopt reentry point.
Expand Down Expand Up @@ -186,6 +184,10 @@ component X86_64Frames {
def fromSfp(sfp: Pointer) -> Ref<X86_64InterpreterFrame> {
return Ref<X86_64InterpreterFrame>.of(CiRuntime.forgeRange<byte>(sfp, X86_64InterpreterFrame.size));
}
// Compute the address of sidetable entry {stp}, counted in {int}s from the first entry.
def sidetablePtr(entries: Array<int>, stp: int) -> Pointer {
return Pointer.atContents(entries) + stp * 4;
}
}

// Native frame states used in the implementation of {FrameStateAccessor}. Since a frame
Expand Down
20 changes: 14 additions & 6 deletions src/engine/x86-64/X86_64Stack.v3
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Comment thread
linxuanm marked this conversation as resolved.
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.
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -860,18 +868,18 @@ 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,
MasmAddr(r_stack, masm.offsets.X86_64Stack_next_stack),
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
Expand Down
142 changes: 142 additions & 0 deletions src/engine/x86-64/X86_64StackCompression.v3
Original file line number Diff line number Diff line change
@@ -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<RelocatableFrame> {
var collector = StackFrameCollector.new();
stack.walk<void>(collector.visitFrame, void, stack.rsp, false);

var result = Vector<RelocatableFrame>.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<RelocatableFrame>) {
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>(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<Pointer>();
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<Value>.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<TargetFrame>.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;
}
}
9 changes: 6 additions & 3 deletions src/engine/x86-64/X86_64Target.v3
Original file line number Diff line number Diff line change
Expand Up @@ -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<RelocatableFrame>;
def readFramesFromStack(from: WasmStack) -> Array<RelocatableFrame> {
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<RelocatableFrame>);
def writeFramesToStack(dest: WasmStack, frames: Array<RelocatableFrame>) {
X86_64Compression.writeFrames(X86_64Stack.!(dest), frames);
}
}

type TargetOsrInfo(spc_entry: Pointer, osr_entries: List<(int, int)>) #unboxed { }
Expand Down
Loading
Loading