Skip to content

Commit e46ceaf

Browse files
author
Arnaud Riess
committed
refactor: update documentation for IR builder and enhance phi-node tracking details
1 parent f4a8b71 commit e46ceaf

2 files changed

Lines changed: 352 additions & 64 deletions

File tree

crates/herkos/src/ir/builder/core.rs

Lines changed: 164 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88
//!
99
//! ## Architecture
1010
//!
11-
//! The builder maintains **two stacks** that together implement Wasm semantics:
11+
//! The builder maintains **three** key data structures that together implement Wasm semantics:
1212
//!
13-
//! ### Value Stack (`value_stack: Vec<VarId>`)
13+
//! ### Value Stack (`value_stack: Vec<UseVar>`)
1414
//!
1515
//! Replaces Wasm's implicit evaluation stack. Instead of pushing raw values, we push
16-
//! variable IDs. Example:
16+
//! [`UseVar`] tokens for already-defined SSA variables. Example:
1717
//!
1818
//! ```text
1919
//! i32.const 5 → emit: v0 = const 5; push(v0)
@@ -28,7 +28,29 @@
2828
//! - `start_block`: jump target for loops (backward)
2929
//! - `end_block`: jump target for block exit (forward/join)
3030
//! - `else_block`: false branch for If
31-
//! - `result_var`: PHI node if block has a result type
31+
//! - `result_var`: phi convergence slot if the block has a result type
32+
//! - `locals_at_entry`: snapshot of `local_vars` at the time this frame was pushed
33+
//! - `branch_incoming`: predecessor snapshots from forward `br`/`br_if`/`br_table`
34+
//! - `loop_phi_vars` / `phi_patches`: phi source collection for loop back-edges
35+
//!
36+
//! ### Local Variable Map (`local_vars: Vec<UseVar>`)
37+
//!
38+
//! Maps each Wasm local index to the **current** SSA variable holding its value.
39+
//! This map is the key to SSA construction for mutable locals:
40+
//!
41+
//! - At function entry, each local (param or declared) gets a fresh variable via
42+
//! `new_pre_alloc_var()`. The map starts as `[v0, v1, ..., vN]`.
43+
//! - `local.get i` → emit `vX = Assign(local_vars[i])` and push vX. A fresh copy
44+
//! is emitted (rather than re-using `local_vars[i]`) so that a later `local.set`
45+
//! cannot retroactively change what was already pushed onto the value stack.
46+
//! - `local.set i` → allocate `vY`, emit `vY = Assign(popped_value)`, set
47+
//! `local_vars[i] = vY`. Subsequent `local.get i` will see vY.
48+
//! - `local.tee i` → same as `local.set`, but the value is also left on the stack.
49+
//!
50+
//! When control flow merges (at `End` of block/loop/if), different predecessors may
51+
//! hold different variables for the same local. The builder compares predecessor
52+
//! snapshots and inserts `IrInstr::Phi` nodes for diverging locals, then updates
53+
//! `local_vars` to the phi dest variables. See `translate.rs` for the full protocol.
3254
//!
3355
//! ## Flow
3456
//!
@@ -39,66 +61,166 @@
3961
//! `value_stack`, allocates new variables, emits IR instructions, pushes results.
4062
//!
4163
//! 3. Control flow instructions (`block`, `loop`, `if`, `br`, etc.) manipulate the control stack
42-
//! and create new basic blocks as needed.
64+
//! and create new basic blocks as needed. Branch instructions (`br`, `br_if`, `br_table`)
65+
//! additionally snapshot `local_vars` and record it as a phi predecessor via
66+
//! `record_forward_branch()` or `record_loop_back_branch()`.
4367
//!
44-
//! 4. Return: `IrFunction` with all blocks, variables typed, and control flow explicit.
68+
//! 4. At each `End`: predecessor snapshots are collected, `IrInstr::Phi` nodes are inserted for
69+
//! locals with differing values, and `local_vars` is updated to the phi dest variables.
70+
//!
71+
//! 5. Return: `IrFunction` with all blocks, variables typed, control flow explicit, and phi
72+
//! nodes at every join point. The `lower_phis` pass then destructs these phis into
73+
//! predecessor-block assignments before codegen.
4574
//!
4675
//! ## Invariants
4776
//!
4877
//! - **Entry block**: First `new_block()` call returns `BlockId(0)` (function entry).
4978
//! - **Value stack**: Operations pop N arguments, push ≤1 result.
5079
//! - **Control stack**: Push/pop balanced; each frame has a unique start/end pair.
51-
//! - **Local variables**: `local_vars[i]` = VarId for Wasm local index i (set once at start).
80+
//! - **Local variables**: `local_vars[i]` always holds the *current* SSA variable for Wasm
81+
//! local `i`. It is updated on every `local.set`/`local.tee` and at every join point
82+
//! (where it may be replaced by a phi dest variable).
5283
//!
53-
//! ## Example 1
84+
//! ## Example 1 — simple arithmetic with a local
5485
//!
55-
//! Wasm: `(func (param $a i32) → i32 (local.get $a) (i32.const 1) (i32.add))`
86+
//! ```wasm
87+
//! (func (param $a i32) (result i32)
88+
//! local.get $a
89+
//! i32.const 1
90+
//! i32.add)
91+
//! ```
5692
//!
5793
//! Generated IR:
5894
//! ```text
5995
//! block_0:
60-
//! v2 = i32_add(v0, v1)
61-
//! return v2
96+
//! v2 = Assign(v0) ;; local.get $a → fresh SSA copy of param
97+
//! v3 = Const(1) ;; i32.const 1
98+
//! v4 = I32Add(v2, v3) ;; i32.add
99+
//! return v4
100+
//!
101+
//! ;; v0 = param $a (implicitly defined at function entry)
102+
//! ;; v1 = result convergence slot allocated by push_control (internal)
62103
//! ```
63-
//! where v0 = param $a, v1 = const 1 (allocated during init/translation).
64104
//!
65-
//! ## Example 2 (if-else)
105+
//! Note: `local.get` emits a fresh `Assign` copy (v2) rather than pushing v0 directly.
106+
//! This ensures a later `local.set $a` cannot retroactively change values already
107+
//! pushed on the value stack.
66108
//!
109+
//! ## Example 2 — local.set creates fresh SSA variables
110+
//!
111+
//! ```wasm
112+
//! (func (param $n i32) (result i32)
113+
//! local.get $n ;; read $n
114+
//! i32.const 1
115+
//! i32.add
116+
//! local.set $n ;; overwrite $n with $n+1
117+
//! local.get $n) ;; read updated $n
118+
//! ```
119+
//!
120+
//! Generated IR (local_vars evolution shown inline):
67121
//! ```text
68-
//! Wasm:
69-
//! if i32
70-
//! (const 1)
71-
//! else
72-
//! (const 2)
73-
//! end
122+
//! block_0:
123+
//! ;; local_vars = [v0] (v0 = param $n)
124+
//! v2 = Assign(v0) ;; local.get $n → copy; local_vars = [v0] (unchanged)
125+
//! v3 = Const(1)
126+
//! v4 = I32Add(v2, v3)
127+
//! v5 = Assign(v4) ;; local.set $n → fresh var; local_vars = [v5]
128+
//! v6 = Assign(v5) ;; local.get $n → copy of v5; local_vars = [v5] (unchanged)
129+
//! return v6
74130
//! ```
75131
//!
76-
//! Generated blocks:
132+
//! ## Example 3 — if/else with phi at join
133+
//!
134+
//! ```wasm
135+
//! ;; (func (param $cond i32) (param $n i32) (result i32)
136+
//! ;; local.get $cond
137+
//! ;; if
138+
//! ;; i32.const 10
139+
//! ;; local.set $n ;; then-branch: $n ← 10
140+
//! ;; else
141+
//! ;; i32.const 20
142+
//! ;; local.set $n ;; else-branch: $n ← 20
143+
//! ;; end
144+
//! ;; local.get $n) ;; which value?
145+
//! ```
77146
//!
147+
//! SSA IR after translation (simplified var names):
78148
//! ```text
79-
//! ┌──────────────────┐
80-
//! │ block_0 │ Entry
81-
//! │ (condition) │
82-
//! │ br_if block_2 │
83-
//! │ br block_3 │
84-
//! └────────┬─────────┘
85-
//! │
86-
//! ┌────┴────┐
87-
//! ▼ ▼
88-
//! ┌─────────┐ ┌─────────┐
89-
//! │ block_1 │ │ block_2 │ True and False branches
90-
//! │ v1=1 │ │ v2=2 │
91-
//! │ br block│ │ br block│
92-
//! │ 3 │ │ 3 │
93-
//! └────┬────┘ └────┬────┘
94-
//! │ │
95-
//! └────┬──────┘
96-
//! ▼
97-
//! ┌──────────────┐
98-
//! │ block_3 │ Join point
99-
//! │ v0=phi(v1,v2)│
100-
//! └──────────────┘
149+
//! block_0 (entry):
150+
//! ;; local_vars = [v_cond, v_n]
151+
//! v_cv = Assign(v_cond) ;; local.get $cond
152+
//! BranchIf(v_cv, then=block_1, else=block_2)
153+
//!
154+
//! block_1 (then):
155+
//! ;; At If push: locals_at_entry = [v_cond, v_n]
156+
//! ;; local_vars = [v_cond, v_n] ← snapshot preserved
157+
//! v_ten = Const(10)
158+
//! v_nt = Assign(v_ten) ;; local.set $n → local_vars = [v_cond, v_nt]
159+
//! jump block_3
160+
//! ;; At Else: then_pred_info = (block_1, [v_cond, v_nt])
161+
//!
162+
//! block_2 (else):
163+
//! ;; local_vars restored to [v_cond, v_n] ← locals_at_entry
164+
//! v_twenty = Const(20)
165+
//! v_ne = Assign(v_twenty) ;; local.set $n → local_vars = [v_cond, v_ne]
166+
//! jump block_3
167+
//!
168+
//! block_3 (join):
169+
//! ;; Predecessors: (block_1, [v_cond, v_nt]) and (block_2, [v_cond, v_ne])
170+
//! ;; $n differs → insert phi; $cond same → no phi
171+
//! v_phi_n = Phi [(block_1, v_nt), (block_2, v_ne)]
172+
//! ;; local_vars = [v_cond, v_phi_n]
173+
//! v_r = Assign(v_phi_n) ;; local.get $n
174+
//! return v_r
175+
//! ```
176+
//!
177+
//! ## Example 4 — loop with phi vars for the back-edge
178+
//!
179+
//! ```wasm
180+
//! ;; (func (param $n i32) (result i32)
181+
//! ;; (local $i i32) ;; zero-initialized
182+
//! ;; loop $L
183+
//! ;; local.get $i
184+
//! ;; i32.const 1
185+
//! ;; i32.add
186+
//! ;; local.set $i ;; $i++
187+
//! ;; local.get $i
188+
//! ;; local.get $n
189+
//! ;; i32.lt_s
190+
//! ;; br_if $L) ;; keep looping while $i < $n
191+
//! ;; local.get $i) ;; return final $i
101192
//! ```
193+
//!
194+
//! SSA IR after translation (simplified):
195+
//! ```text
196+
//! block_0 (pre-loop):
197+
//! ;; local_vars = [v_n, v_i0] (v_n=param, v_i0=0-init local)
198+
//! ;; push_control(Loop) snapshots locals_at_entry=[v_n, v_i0]
199+
//! ;; and substitutes local_vars = [p_n, p_i] ← phi vars
200+
//! jump block_1
201+
//!
202+
//! block_1 (loop header): ← br_if backward target
203+
//! ;; Phi instructions inserted here by emit_loop_phis at End:
204+
//! p_n = Phi [(block_0, v_n), (block_2, v_n2)] ;; $n unchanged → trivial
205+
//! p_i = Phi [(block_0, v_i0), (block_2, v_inew)] ;; $i modified → real phi
206+
//! v_ic = Assign(p_i) ;; local.get $i
207+
//! v_ip = I32Add(v_ic, Const(1))
208+
//! v_inew = Assign(v_ip) ;; local.set $i → local_vars = [p_n, v_inew]
209+
//! v_ic2 = Assign(v_inew) ;; local.get $i
210+
//! v_nc = Assign(p_n) ;; local.get $n
211+
//! v_cmp = I32LtS(v_ic2, v_nc)
212+
//! ;; br_if 0: record_loop_back_branch → phi_patches += (p_i, block_1, v_inew)
213+
//! BranchIf(v_cmp, if_true=block_1, if_false=block_2)
214+
//!
215+
//! block_2 (loop exit):
216+
//! ;; local_vars = [p_n, v_inew] (fall-through from br_if false path)
217+
//! v_ret = Assign(v_inew) ;; local.get $i
218+
//! return v_ret
219+
//! ```
220+
//!
221+
//! After `lower_phis`, the trivial `p_n = Phi[..., v_n]` becomes `p_n = Assign(v_n)`
222+
//! and the real `p_i` phi is rewritten as predecessor-block assignments:
223+
//! `block_0` gets `p_i = v_i0`, `block_1` gets `p_i = v_inew` (before its terminator).
102224
103225
use super::super::types::*;
104226
use anyhow::{Context, Result};

0 commit comments

Comments
 (0)