Skip to content

Commit e8f54be

Browse files
committed
[stack-switching] Stack compression
1 parent fa0cd7c commit e8f54be

20 files changed

Lines changed: 594 additions & 12 deletions

src/engine/Compression.v3

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
// Copyright 2025 Wizard authors. All rights reserved.
2+
// See LICENSE for details of Apache 2.0 license.
3+
4+
// Generalized- relocatable representation of a stack frame that can be serialized.
5+
type RelocatableFrame(func: WasmFunction, pc: int, reentry: TargetReentryLabel, bytecode_ip: TargetReentryLabel, vals: Array<Value>) #unboxed {
6+
7+
def render(buf: StringBuilder) -> StringBuilder {
8+
// Frame header.
9+
buf.put2(" Frame(func=%q, pc=%d):", func.decl.render(null, _), pc).ln();
10+
buf.put1(" Values(count=%d):", vals.length).ln();
11+
12+
// Frame values.
13+
if (vals.length > 0) {
14+
buf.puts(" ");
15+
for (v in vals) Values.render(v, buf).puts(" ");
16+
}
17+
return buf;
18+
}
19+
}
20+
21+
class CompressionStrategy<C> {
22+
def compress(frames: Range<RelocatableFrame>) -> C;
23+
def decompress(comp: C) -> Array<RelocatableFrame>;
24+
}
25+
26+
class NaiveCompressionStrategy extends CompressionStrategy<NaiveCompressedStack> {
27+
def compress(frames: Range<RelocatableFrame>) -> NaiveCompressedStack {
28+
var frames_builder = Vector<FrameHeader>.new();
29+
var values_builder = Vector<Value>.new();
30+
for (frame in frames) {
31+
var header = FrameHeader(frame.func, frame.pc, frame.reentry, frame.bytecode_ip, frame.vals.length);
32+
frames_builder.put(header);
33+
values_builder.puta(frame.vals);
34+
}
35+
36+
// XXX: reuse stack (maybe pass one in?)
37+
var result = NaiveCompressedStack.new();
38+
result.frames = frames_builder.extract();
39+
result.values = values_builder.extract();
40+
return result;
41+
}
42+
43+
def decompress(comp: NaiveCompressedStack) -> Array<RelocatableFrame> {
44+
var builder = Vector<RelocatableFrame>.new();
45+
var val_offset = 0;
46+
for (header in comp.frames) {
47+
var vals = Arrays.range(comp.values, val_offset, val_offset + header.n_vals);
48+
builder.put(RelocatableFrame(header.func, header.pc, header.reentry, header.bytecode_ip, vals));
49+
val_offset += header.n_vals;
50+
}
51+
52+
return builder.extract();
53+
}
54+
}
55+
56+
class PackedCompressionStrategy extends CompressionStrategy<PackedCompressedStack> {
57+
def frames_builder = Vector<FrameHeader>.new();
58+
def refs_builder = Vector<Object>.new();
59+
def w = DataWriter.new();
60+
def r = DataReader.new(null);
61+
62+
def reset() { frames_builder.clear(); refs_builder.clear(); w.clear(); }
63+
64+
def writeValue(value: Value) {
65+
match (value) {
66+
Ref(v) => { w.putb(Value.Ref.tag).put_uleb32(u32.!(refs_builder.length)); refs_builder.put(v); }
67+
I31(v) => w.putb(Value.I31.tag).put_uleb32(v);
68+
I32(v) => w.putb(Value.I32.tag).put_uleb32(v);
69+
I64(v) => w.putb(Value.I64.tag).put_sleb64(i64.view(v));
70+
F32(v) => w.putb(Value.F32.tag).put_uleb32(v);
71+
F64(v) => w.putb(Value.F64.tag).put_sleb64(i64.view(v));
72+
V128(l, h) => w.putb(Value.V128.tag).put_sleb64(i64.view(l)).put_sleb64(i64.view(h));
73+
Cont(c) => {
74+
// TODO: omit {version} if boxed-continuation is enabled
75+
var obj = Continuations.getStoredObject(c);
76+
var version = Continuations.getStoredVersion(c);
77+
w.putb(Value.Cont.tag).put_uleb32(u32.!(refs_builder.length)).put_sleb64(i64.view(version));
78+
refs_builder.put(obj);
79+
}
80+
}
81+
}
82+
83+
def compress(frames: Range<RelocatableFrame>) -> PackedCompressedStack {
84+
for (frame in frames) {
85+
var header = FrameHeader(frame.func, frame.pc, frame.reentry, frame.bytecode_ip, frame.vals.length);
86+
frames_builder.put(header);
87+
for (v in frame.vals) writeValue(v);
88+
}
89+
90+
// XXX: reuse stack (maybe pass one in?)
91+
var result = PackedCompressedStack.new();
92+
result.frames = frames_builder.extract();
93+
result.packed = w.extract();
94+
result.refs = refs_builder.extract();
95+
return result;
96+
}
97+
98+
def readValue(comp: PackedCompressedStack) -> Value {
99+
var tag = r.read1();
100+
match (tag) {
101+
Value.Ref.tag => return Value.Ref(comp.refs[r.read_uleb32()]);
102+
Value.I31.tag => return Value.I31(u31.!(r.read_uleb32()));
103+
Value.I32.tag => return Value.I32(r.read_uleb32());
104+
Value.I64.tag => return Value.I64(u64.view(r.read_sleb64()));
105+
Value.F32.tag => return Value.F32(r.read_uleb32());
106+
Value.F64.tag => return Value.F64(u64.view(r.read_sleb64()));
107+
Value.V128.tag => return Value.V128(u64.view(r.read_sleb64()), u64.view(r.read_sleb64()));
108+
Value.Cont.tag => {
109+
var obj = comp.refs[r.read_uleb32()];
110+
var version = u64.view(r.read_sleb64());
111+
return Value.Cont(Continuations.fromStoredObject(obj, version));
112+
}
113+
_ => return Value.I32(0);
114+
}
115+
}
116+
117+
def decompress(comp: PackedCompressedStack) -> Array<RelocatableFrame> {
118+
r.reset(comp.packed, 0, comp.packed.length);
119+
120+
var builder = Vector<RelocatableFrame>.new();
121+
for (header in comp.frames) {
122+
var vals = Array<Value>.new(header.n_vals);
123+
for (i < header.n_vals) vals[i] = readValue(comp);
124+
builder.put(RelocatableFrame(header.func, header.pc, header.reentry, header.bytecode_ip, vals));
125+
}
126+
return builder.extract();
127+
}
128+
}
129+
130+
// Compressed stack representation.
131+
132+
type FrameHeader(func: WasmFunction, pc: int, reentry: TargetReentryLabel, bytecode_ip: TargetReentryLabel, n_vals: int) #unboxed {
133+
134+
def render(buf: StringBuilder) -> StringBuilder {
135+
return buf.put3("Frame(func=%d, pc=%d, n_vals=%d)", func.decl.func_index, pc, n_vals);
136+
}
137+
}
138+
139+
class CompressedStack {
140+
def size() -> u64;
141+
def render(buf: StringBuilder) -> StringBuilder;
142+
}
143+
144+
class NaiveCompressedStack extends CompressedStack {
145+
var frames: Array<FrameHeader>;
146+
var values: Array<Value>;
147+
148+
def render(buf: StringBuilder) -> StringBuilder {
149+
buf.put1("NaiveCompressedStack(n_frames=%d):", frames.length).ln();
150+
for (frame in frames) buf.put1(" %q", frame.render).ln();
151+
return buf;
152+
}
153+
}
154+
155+
class PackedCompressedStack extends CompressedStack {
156+
var frames: Array<FrameHeader>;
157+
158+
// Packed value stack representation.
159+
var packed: Array<byte>;
160+
var refs: Array<Object>;
161+
162+
def render(buf: StringBuilder) -> StringBuilder {
163+
buf.put1("PackedCompressedStack(n_frames=%d):", frames.length).ln();
164+
for (frame in frames) buf.put1(" %q", frame.render).ln();
165+
return buf;
166+
}
167+
}
168+
169+
component StackCompression {
170+
def naive = NaiveCompressionStrategy.new();
171+
def packed = PackedCompressionStrategy.new();
172+
173+
def compress(stack: WasmStack) -> CompressedStack {
174+
var frames = Target.readFramesFromStack(stack);
175+
var compressed = packed.compress(frames);
176+
return compressed;
177+
}
178+
179+
def decompress(to: WasmStack, from: CompressedStack) {
180+
var frames: Array<RelocatableFrame>;
181+
match (from) {
182+
x: NaiveCompressedStack => frames = naive.decompress(x);
183+
x: PackedCompressedStack => frames = packed.decompress(x);
184+
}
185+
Target.writeFramesToStack(to, frames);
186+
}
187+
}

src/engine/Debug.v3

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ component Debug {
1313
def stack = false;
1414
def memory = false;
1515
def diagnostic = false;
16+
def compression = false;
1617

1718
// Prevents arguments from being dead-code-eliminated.
1819
def keepAlive<T>(x: T) { }

src/engine/Sidetable.v3

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,11 @@ component Sidetables {
8282
BR => size = Sidetable_BrEntry.size;
8383
BR_IF => size = Sidetable_BrEntry.size;
8484
BR_TABLE => { var count = immptr.skip_labels(); size = (1 + count) * Sidetable_BrEntry.size; }
85-
TRY_TABLE => { var count = immptr.skip_catches(); size = count * Sidetable_CatchEntry.size; }
85+
TRY_TABLE => {
86+
immptr.read_BlockTypeCode();
87+
var count = immptr.skip_catches();
88+
size = count * Sidetable_CatchEntry.size;
89+
}
8690
BR_ON_NULL => size = Sidetable_BrEntry.size;
8791
BR_ON_NON_NULL => size = Sidetable_BrEntry.size;
8892
BR_ON_CAST => size = Sidetable_BrEntry.size;

src/engine/Value.v3

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ component Values {
7171
var id = if(x.decl == null, -1, x.decl.heaptype_index);
7272
buf.put1("<ref array #%d>", id);
7373
}
74-
x: Object => x.render(buf);
74+
x: Object => { x.render(buf); }
7575
null => buf.puts("<ref null>");
7676
}
7777
I31(val) => buf.put1("i31:%d", u32.view(val));

src/engine/WasmStack.v3

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// See LICENSE for details of Apache 2.0 license.
33

44
// An execution stack.
5-
class ExecStack {
5+
class ExecStack extends Object {
66
def popV(t: ValueType) -> Value;
77
def popi() -> i32;
88
def popu() -> u32;
@@ -51,10 +51,14 @@ class ExecStack {
5151
}
5252
}
5353

54+
// Represents a suspendable stack that can be used to instantiate a continuation.
55+
class VersionedStack extends ExecStack {
56+
var version: u64;
57+
}
58+
5459
// Represents a stack on which Wasm code can be executed.
55-
class WasmStack extends ExecStack {
60+
class WasmStack extends VersionedStack {
5661
var parent: WasmStack;
57-
var version: u64;
5862

5963
// ext:stack-switching
6064
// Denotes the bottom stack of a suspended continuation (with {this} as the top stack).

src/engine/continuation/BoxedContinuation.v3

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,11 @@ component Continuations {
2929

3030
def getStoredStack(cont: Continuation) -> WasmStack { return cont.stack; }
3131
def getStoredVersion(cont: Continuation) -> u64 { return 0; } // boxed cont does not store version
32+
33+
def objectIsContinuation(obj: Object) -> bool { return Continuation.?(obj); }
34+
def objectToContinuation(obj: Object) -> Continuation { return Continuation.!(obj); }
35+
36+
// Stack compression.
37+
def getStoredObject(cont: Continuation) -> Object { return cont; }
38+
def fromStoredObject(obj: Object, version: u64) -> Continuation { return Continuation.!(obj); }
3239
}

src/engine/continuation/UnboxedContinuation.v3

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,11 @@ component Continuations {
2525

2626
def getStoredStack(cont: Continuation) -> WasmStack { return cont.stack; }
2727
def getStoredVersion(cont: Continuation) -> u64 { return cont.version; }
28+
29+
def objectIsContinuation(obj: Object) -> bool { return false; }
30+
def objectToContinuation(obj: Object) -> Continuation;
31+
32+
// Stack compression.
33+
def getStoredObject(cont: Continuation) -> Object { return cont.stack; }
34+
def fromStoredObject(obj: Object, version: u64) -> Continuation { return Continuation(WasmStack.!(obj), version); }
2835
}

src/engine/v3/V3Target.v3

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ component Target {
4242
def fastMemFill(dst: Range<byte>, val: byte) {
4343
for (i < dst.length) dst[i] = val;
4444
}
45+
46+
// TODO[sc]: empty function stubs for stack compression (not needed for now).
47+
// Stack compression: read the frames of a stack into an array of target independent frames representation.
48+
def readFramesFromStack(from: WasmStack) -> Array<RelocatableFrame>;
49+
// Stack compression: overwrite the destination stack with the content of the relocatable frames.
50+
def writeFramesToStack(dest: WasmStack, frames: Array<RelocatableFrame>);
4551
}
4652

4753
// A one-element cache for recycling storage of Wasm stacks (interpreters).
@@ -101,3 +107,6 @@ type TargetFrame(frame: V3Frame) #unboxed {
101107
}
102108
}
103109
class TargetHandlerDest(is_dummy: bool) { }
110+
111+
// Stack compression specialization.
112+
type TargetReentryLabel(ret_addr: int) #unboxed;

src/engine/x86-64/Mmap.v3

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,8 @@ class MemoryRange {
4949
def range(offset: int, length: int) -> Range<byte> {
5050
return CiRuntime.forgeRange(this.start + offset, length);
5151
}
52+
53+
def render(buf: StringBuilder) -> StringBuilder {
54+
return buf.put2("MemRange[0x%x, 0x%x)", start - Pointer.NULL, end - Pointer.NULL);
55+
}
5256
}

src/engine/x86-64/X86_64Interpreter.v3

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4230,6 +4230,7 @@ class X86_64InterpreterGen(ic: X86_64InterpreterCode, w: DataWriter) {
42304230
}
42314231
// generates cleanup code for after a child stack returns
42324232
private def genOnResumeFinish(skip_tag: bool) {
4233+
restoreCurPcFromFrame();
42334234
restoreCallerIVars();
42344235
restoreDispatchTableReg();
42354236
var r_stack = r_tmp1;

0 commit comments

Comments
 (0)