|
8 | 8 | //! |
9 | 9 | //! ## Architecture |
10 | 10 | //! |
11 | | -//! The builder maintains **two stacks** that together implement Wasm semantics: |
| 11 | +//! The builder maintains **three** key data structures that together implement Wasm semantics: |
12 | 12 | //! |
13 | | -//! ### Value Stack (`value_stack: Vec<VarId>`) |
| 13 | +//! ### Value Stack (`value_stack: Vec<UseVar>`) |
14 | 14 | //! |
15 | 15 | //! 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: |
17 | 17 | //! |
18 | 18 | //! ```text |
19 | 19 | //! i32.const 5 → emit: v0 = const 5; push(v0) |
|
28 | 28 | //! - `start_block`: jump target for loops (backward) |
29 | 29 | //! - `end_block`: jump target for block exit (forward/join) |
30 | 30 | //! - `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. |
32 | 54 | //! |
33 | 55 | //! ## Flow |
34 | 56 | //! |
|
39 | 61 | //! `value_stack`, allocates new variables, emits IR instructions, pushes results. |
40 | 62 | //! |
41 | 63 | //! 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()`. |
43 | 67 | //! |
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. |
45 | 74 | //! |
46 | 75 | //! ## Invariants |
47 | 76 | //! |
48 | 77 | //! - **Entry block**: First `new_block()` call returns `BlockId(0)` (function entry). |
49 | 78 | //! - **Value stack**: Operations pop N arguments, push ≤1 result. |
50 | 79 | //! - **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). |
52 | 83 | //! |
53 | | -//! ## Example 1 |
| 84 | +//! ## Example 1 — simple arithmetic with a local |
54 | 85 | //! |
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 | +//! ``` |
56 | 92 | //! |
57 | 93 | //! Generated IR: |
58 | 94 | //! ```text |
59 | 95 | //! 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) |
62 | 103 | //! ``` |
63 | | -//! where v0 = param $a, v1 = const 1 (allocated during init/translation). |
64 | 104 | //! |
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. |
66 | 108 | //! |
| 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): |
67 | 121 | //! ```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 |
74 | 130 | //! ``` |
75 | 131 | //! |
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 | +//! ``` |
77 | 146 | //! |
| 147 | +//! SSA IR after translation (simplified var names): |
78 | 148 | //! ```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 |
101 | 192 | //! ``` |
| 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). |
102 | 224 |
|
103 | 225 | use super::super::types::*; |
104 | 226 | use anyhow::{Context, Result}; |
|
0 commit comments