Skip to main content

logicaffeine_compile/codegen_sva/
fabric_rtl.rs

1//! fabric_rtl — Futamura's first projection in silicon: "the specialized residual IS the
2//! circuit," end-to-end from the REAL compiler.
3//!
4//! `PE(interp, program) = compiled_program`: our compiler specializes the evaluator to a
5//! program, producing register bytecode with no interpreter dispatch (`count_dispatch == 0`,
6//! Jones-optimal). For a straight-line function that residual is a dispatch-free arithmetic
7//! trace. This module takes that **actual compiled residual**, lowers it register→SSA to a
8//! **fixed spatial dataflow circuit** — one functional unit per op, wired SSA-style, with NO
9//! program counter, NO fetch, NO opcode dispatch — and proves it THREE ways: bit-identically
10//! to the RUNNING VM under iverilog, across a BATTERY of distinct compiled programs directly
11//! against the VM, and — where the residual is multiply-free — EXHAUSTIVELY by our OWN
12//! certified SAT against an independent closed form. The circuit does not *interpret* the
13//! program; it *is* the program.
14//!
15//! Width is **64-bit signed wrapping**. The VM's `Int` is arbitrary-precision, so the circuit
16//! is its **fixed-width projection**: it computes the VM's EXACT result reduced mod 2^64, and
17//! that equality holds for EVERY input, overflow included (no range restriction — the low 64
18//! bits always agree). Unbounded precision is the BigInt coprocessor's domain (`bignum_rtl`).
19//! Straight-line residuals (`+ - *`) lower here; COUNTED LOOPS lower to a sequential CGRA — a
20//! fixed spatial body datapath streamed by a loop counter — in the `LoopFabric` section below.
21
22/// The 64-bit datapath width — the VM's `Int` is `i64`, so the circuit wraps identically.
23pub const W: u32 = 64;
24
25/// The straight-line ALU ops a dispatch-free residual is built from (the `alu.v` ops, each
26/// already proven ≡ the VM). 64-bit signed, wrapping — bit-identical to the VM's `Int`.
27#[derive(
28    Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
29)]
30pub enum AluOp {
31    Add,
32    Sub,
33    Mul,
34    /// Bitwise AND / OR / XOR — combinational, bit-identical to the VM's `Int` bitwise ops (and, for 0/1
35    /// Bool operands, to the logical spellings, since `&`/`|`/`^` agree with `&&`/`||`/xor on {0,1}).
36    And,
37    Or,
38    Xor,
39    /// Logical shift left `a << (b & 63)` (the VM's `wrapping_shl`, count masked mod 64).
40    Shl,
41    /// ARITHMETIC shift right `a >>> (b & 63)` (the VM's `wrapping_shr` on the signed `Int`).
42    Shr,
43    /// LOGICAL (unsigned) shift right `(a as u64) >> (b & 63)` — zero-fill, the Word64/Keccak `word64Shr`
44    /// (`wrapping_shr` on the `u64` value). Distinct from [`AluOp::Shr`], which sign-extends: they agree only
45    /// while the high bit is clear (why the Word32 lane, always in `0..2³²`, reuses `Shr`), but a Word64 value
46    /// with bit 63 set needs this zero-fill form.
47    UShr,
48}
49
50impl AluOp {
51    pub fn verilog(&self, a: &str, b: &str) -> String {
52        match self {
53            AluOp::Add => format!("$signed({a}) + $signed({b})"),
54            AluOp::Sub => format!("$signed({a}) - $signed({b})"),
55            AluOp::Mul => format!("$signed({a}) * $signed({b})"),
56            AluOp::And => format!("({a}) & ({b})"),
57            AluOp::Or => format!("({a}) | ({b})"),
58            AluOp::Xor => format!("({a}) ^ ({b})"),
59            // Shift counts mask mod 64 (the wrapping spec); `>>>` on a $signed value is arithmetic.
60            AluOp::Shl => format!("(({a}) << (({b}) & 64'd63))"),
61            AluOp::Shr => format!("($signed({a}) >>> (({b}) & 64'd63))"),
62            // LOGICAL right shift — `>>` on an $unsigned value zero-fills (Word64 `wrapping_shr` on `u64`).
63            AluOp::UShr => format!("($unsigned({a}) >> (({b}) & 64'd63))"),
64        }
65    }
66    pub fn eval(&self, a: i64, b: i64) -> i64 {
67        match self {
68            AluOp::Add => a.wrapping_add(b),
69            AluOp::Sub => a.wrapping_sub(b),
70            AluOp::Mul => a.wrapping_mul(b),
71            AluOp::And => a & b,
72            AluOp::Or => a | b,
73            AluOp::Xor => a ^ b,
74            AluOp::Shl => a.wrapping_shl(b as u32),
75            AluOp::Shr => a.wrapping_shr(b as u32),
76            AluOp::UShr => (a as u64).wrapping_shr(b as u32) as i64,
77        }
78    }
79}
80
81/// A SEQUENTIAL functional unit — signed division and remainder. Unlike an [`AluOp`], division is NOT a
82/// combinational tile: on silicon it is the multi-cycle `shared_divider` (already proven ≡ the VM by
83/// `rtl.rs::the_signed_divider_is_proven_correct_by_our_sat`), issued as a functional unit. Keeping it a
84/// distinct fabric cell lets the emitter wire that sequential unit rather than synthesize a giant
85/// combinational divider. Semantics mirror the soft-core's `shared_divider` exactly, INCLUDING the
86/// divide-by-zero trap (`b == 0 → 0`).
87#[derive(
88    Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
89)]
90pub enum FuncUnit {
91    /// Truncated signed quotient (`Op::Div` / `Op::ExactDiv`, `a / b` rounding toward zero).
92    DivS,
93    /// Signed remainder taking the dividend's sign (`Op::Mod`, `a % b`).
94    ModS,
95    /// Floor division (`Op::FloorDiv`, `a // b` rounding toward negative infinity).
96    FloorDivS,
97    /// Exponentiation (`Op::Pow`, `a ** b`) — a SEQUENTIAL multiplier (b multiplies) at the RTL level; the
98    /// eval is the closed-form wrapping power. The VM promotes to bigint on overflow, so a fabric witness
99    /// keeps `a**b` inside i64 (a small const exponent over the bounded fuzz range).
100    Pow,
101    /// UNSIGNED high-half multiply `(a·b) >> 64` (both operands as `u64`) — the reciprocal step of
102    /// `Op::MagicDivU`'s magic division. On silicon it is the high word of the 64×64 multiplier; the eval
103    /// takes the top 64 bits of the 128-bit unsigned product. No divider trap applies (it is a multiply).
104    MulHiU,
105    /// APPROXIMATE equality (`Op::ApproxEq`, `a is approximately b`) over NUMERIC operands: both converted to
106    /// `f64`, compared by the relative+absolute tolerance `logos_approx_eq`. Yields 0/1. (Float-typed operands
107    /// need the float lane; the fabric refuses them at the `Constant::Float` load, so only the numeric-int
108    /// case reaches here.) No divider trap — it is a comparison.
109    ApproxEqF,
110    /// The FLOAT LANE — IEEE-754 `f64` operating on the bit-pattern each operand carries (`f64::from_bits`).
111    /// Binary `FAddF`/`FSubF`/`FMulF`/`FDivF` return the result's bits; unary `FSqrtF` returns bits; `FFloorF`
112    /// returns the floored value as an `Int` (the float→int bridge, matching the `floor` builtin). No divider
113    /// trap (float `/0.0` is `±inf`, not the integer trap).
114    FAddF,
115    FSubF,
116    FMulF,
117    FDivF,
118    FSqrtF,
119    FFloorF,
120    /// Signed 128-bit MULTIPLY OVERFLOW flag: `1` iff `(a as i128) * (b as i128)` does NOT fit `i64` (the
121    /// `BigIntWide` lane's "did this product promote to a BigInt?" test — the VM's `Value::mul` returns an
122    /// `Int` when it fits and a `BigInt` when it does not). A comparison, so no divider trap.
123    MulOvfS,
124    /// INT→FLOAT promotion: reinterpret the operand as a signed `i64` value and return the bit pattern of its
125    /// `f64` widening (`(a as f64).to_bits()`). This is the fabric's static realization of the VM's dynamic
126    /// `Int ∘ Float → Float` coercion (`arith::multiply` etc. do `*a as f64` when one side is a `Float`): when a
127    /// float op has an integer operand, the fabric promotes that operand through this unit first so the float
128    /// functional unit sees two genuine `f64` bit patterns. Unary (`b` ignored); no divider trap.
129    IntToF,
130    /// FLOAT COMPARISONS over two `f64` bit patterns, yielding the `0/1` predicate the terminator / a `Select`
131    /// consumes. A genuine IEEE-754 compare (`f64::from_bits(a) < f64::from_bits(b)` etc.) — NOT the integer
132    /// bit-pattern ordering a plain `Select` would use, which is only sound for same-sign operands and silently
133    /// wrong once a negative float appears. `FEqF`/`FNeF` follow IEEE equality (so `NaN != NaN`). No divider trap.
134    FLtF,
135    FGtF,
136    FLeF,
137    FGeF,
138    FEqF,
139    FNeF,
140    /// POPCOUNT (`count_ones`): the number of set bits in the operand read as a `u64`. Unary (`b` ignored),
141    /// combinational (a bit-adder tree), yields an `Int` in `0..=64` — no divider trap. The static realization
142    /// of the VM's `count_ones` builtin / the `popcount_leaf` hot path.
143    PopCount,
144}
145
146impl FuncUnit {
147    pub fn eval(&self, a: i64, b: i64) -> i64 {
148        // Pow is handled BEFORE the divider trap: `a ** 0 == 1`, not 0. Negative exponents clamp to 0 (the
149        // fabric models only the integer-power domain the witness exercises).
150        if let FuncUnit::Pow = self {
151            return a.wrapping_pow(b.max(0) as u32);
152        }
153        // Unsigned high-half multiply — a multiply, not a divider, so no `b == 0` trap (and `mulhi(x, 0)`
154        // is naturally 0). Both operands reinterpreted as `u64` (the magic reciprocal is a `u64` constant).
155        if let FuncUnit::MulHiU = self {
156            return (((a as u64 as u128) * (b as u64 as u128)) >> 64) as i64;
157        }
158        // Approximate equality over numeric operands — reuses the kernel's exact tolerance, so the fabric
159        // result is bit-identical to the VM's `is approximately`. A comparison, so no `b == 0` trap.
160        if let FuncUnit::ApproxEqF = self {
161            return i64::from(logicaffeine_data::ops::logos_approx_eq(a as f64, b as f64));
162        }
163        // The float lane — operands are f64 BIT PATTERNS. Binary ops return the result's bits; `FSqrtF`
164        // returns bits; `FFloorF` returns the floored value as an integer (the float→int bridge).
165        match self {
166            FuncUnit::FAddF => {
167                return (f64::from_bits(a as u64) + f64::from_bits(b as u64)).to_bits() as i64
168            }
169            FuncUnit::FSubF => {
170                return (f64::from_bits(a as u64) - f64::from_bits(b as u64)).to_bits() as i64
171            }
172            FuncUnit::FMulF => {
173                return (f64::from_bits(a as u64) * f64::from_bits(b as u64)).to_bits() as i64
174            }
175            FuncUnit::FDivF => {
176                return (f64::from_bits(a as u64) / f64::from_bits(b as u64)).to_bits() as i64
177            }
178            FuncUnit::FSqrtF => return f64::from_bits(a as u64).sqrt().to_bits() as i64,
179            FuncUnit::FFloorF => return f64::from_bits(a as u64).floor() as i64,
180            FuncUnit::MulOvfS => {
181                let p = (a as i128) * (b as i128);
182                return i64::from(p < i64::MIN as i128 || p > i64::MAX as i128);
183            }
184            // INT→FLOAT promotion: widen the signed integer operand to its `f64` bit pattern (the VM's implicit
185            // `*a as f64` when an Int meets a Float). `b` is ignored (unary).
186            FuncUnit::IntToF => return (a as f64).to_bits() as i64,
187            // FLOAT comparisons — decode both operands to `f64` and apply the genuine IEEE-754 relation.
188            FuncUnit::FLtF => {
189                return i64::from(f64::from_bits(a as u64) < f64::from_bits(b as u64))
190            }
191            FuncUnit::FGtF => {
192                return i64::from(f64::from_bits(a as u64) > f64::from_bits(b as u64))
193            }
194            FuncUnit::FLeF => {
195                return i64::from(f64::from_bits(a as u64) <= f64::from_bits(b as u64))
196            }
197            FuncUnit::FGeF => {
198                return i64::from(f64::from_bits(a as u64) >= f64::from_bits(b as u64))
199            }
200            FuncUnit::FEqF => {
201                return i64::from(f64::from_bits(a as u64) == f64::from_bits(b as u64))
202            }
203            FuncUnit::FNeF => {
204                return i64::from(f64::from_bits(a as u64) != f64::from_bits(b as u64))
205            }
206            // POPCOUNT — the set-bit count of the operand read as a `u64` (unary, `b` ignored).
207            FuncUnit::PopCount => return (a as u64).count_ones() as i64,
208            _ => {}
209        }
210        if b == 0 {
211            return 0; // the divider's hardware trap — matches `soft_core_divider` (b == 0 → 0)
212        }
213        match self {
214            FuncUnit::DivS => a.wrapping_div(b),
215            FuncUnit::ModS => a.wrapping_rem(b),
216            FuncUnit::FloorDivS => {
217                let q = a.wrapping_div(b);
218                if a.wrapping_rem(b) != 0 && ((a < 0) != (b < 0)) {
219                    q - 1
220                } else {
221                    q
222                }
223            }
224            FuncUnit::Pow => unreachable!("Pow handled above"),
225            FuncUnit::MulHiU => unreachable!("MulHiU handled above"),
226            FuncUnit::ApproxEqF
227            | FuncUnit::FAddF
228            | FuncUnit::FSubF
229            | FuncUnit::FMulF
230            | FuncUnit::FDivF
231            | FuncUnit::FSqrtF
232            | FuncUnit::FFloorF
233            | FuncUnit::MulOvfS
234            | FuncUnit::IntToF
235            | FuncUnit::FLtF
236            | FuncUnit::FGtF
237            | FuncUnit::FLeF
238            | FuncUnit::FGeF
239            | FuncUnit::FEqF
240            | FuncUnit::FNeF
241            | FuncUnit::PopCount => unreachable!("float/approx/overflow/popcount handled above"),
242        }
243    }
244    /// A COMBINATIONAL-correct Verilog lowering with the `b == 0 → 0` guard — semantically identical to
245    /// the sequential `shared_divider` (whose FSM instantiation is the area refinement). 64-bit signed,
246    /// so iverilog's truncated division/remainder is bit-identical to the VM's `Int`.
247    pub fn verilog(&self, a: &str, b: &str) -> String {
248        match self {
249            FuncUnit::DivS => format!("(($signed({b}) == 0) ? 64'sd0 : $signed({a}) / $signed({b}))"),
250            FuncUnit::ModS => format!("(($signed({b}) == 0) ? 64'sd0 : $signed({a}) % $signed({b}))"),
251            FuncUnit::FloorDivS => format!(
252                "(($signed({b}) == 0) ? 64'sd0 : (($signed({a}) % $signed({b}) != 0 && \
253                 (($signed({a}) < 0) != ($signed({b}) < 0))) ? ($signed({a}) / $signed({b})) - 64'sd1 \
254                 : $signed({a}) / $signed({b})))"
255            ),
256            FuncUnit::Pow => panic!(
257                "Pow RTL is a sequential-multiplier functional unit (a `b`-step multiply FSM), not a \
258                 combinational wire — it is emitted by `emit_pow_datapath_verilog` (the sequenced power \
259                 lane) and iverilog-exercised by fabric_pow_lock; the combinational tape does not emit it"
260            ),
261            // High-half of the 64×64 UNSIGNED product: zero-extend both operands to 128 bits (so the
262            // multiply is full-width, not truncated to 64), then take bits [127:64].
263            FuncUnit::MulHiU => format!("(({{64'h0, {a}}} * {{64'h0, {b}}}) >> 64)"),
264            // Signed 128-bit multiply OVERFLOW flag: `1` iff `(a·b)` does NOT fit `i64`. Sign-extend both
265            // operands to 128 bits (so the product is full-width, not truncated), then range-check against
266            // `[i64::MIN, i64::MAX]`. Bit-identical to `FuncUnit::eval`'s i128 test (`p < MIN || p > MAX`).
267            FuncUnit::MulOvfS => format!(
268                "((($signed({{{{64{{{a}[63]}}}}, {a}}}) * $signed({{{{64{{{b}[63]}}}}, {b}}})) \
269                 <= 128'sh0000_0000_0000_0000_7FFF_FFFF_FFFF_FFFF && \
270                 ($signed({{{{64{{{a}[63]}}}}, {a}}}) * $signed({{{{64{{{b}[63]}}}}, {b}}})) \
271                 >= -128'sh0000_0000_0000_0000_8000_0000_0000_0000) ? 64'sd0 : 64'sd1)"
272            ),
273            // APPROXIMATE equality over NUMERIC (integer) operands, bit-identical to `logos_approx_eq(a,b)`:
274            // `a == b`, OR `|a-b| ≤ max(1e-9·max(|a|,|b|), 1e-12)`. Both operands are integers, so `|a-b|` and
275            // `max(|a|,|b|)` are integers and the `1e-12` floor never dominates (`max(|a|,|b|) ≥ 1` whenever
276            // `a ≠ b`), leaving the exact-rational test `|a-b|·10^9 ≤ max(|a|,|b|)` (evaluated in 128 bits so
277            // the ×10^9 scale cannot overflow). Yields 0/1 — no divider trap (it is a comparison).
278            FuncUnit::ApproxEqF => format!(
279                "((($signed({a}) == $signed({b})) || \
280                 ($signed(((($signed({a}) - $signed({b})) < 0) ? -($signed({a}) - $signed({b})) \
281                 : ($signed({a}) - $signed({b}))) * 128'sd1000000000) <= \
282                 $signed((((($signed({a}) < 0) ? -$signed({a}) : $signed({a})) > \
283                 (($signed({b}) < 0) ? -$signed({b}) : $signed({b}))) ? \
284                 ((($signed({a}) < 0) ? -$signed({a}) : $signed({a}))) : \
285                 ((($signed({b}) < 0) ? -$signed({b}) : $signed({b}))))))) ? 64'sd1 : 64'sd0)"
286            ),
287            // The float→int FLOOR bridge (`FuncUnit::FFloorF`): `floor(f64_bits(a)) as i64`, bit-identical to
288            // the eval (`f64::from_bits(a).floor() as i64`). Combinational IEEE-754 decode: unbias the exponent,
289            // rebuild the 53-bit significand (implicit leading bit), then case on the exponent — `|a| < 1 → 0`;
290            // `0 ≤ exp < 52 →` shift the significand right by `52-exp` (dropping the fraction); `exp ≥ 52 →`
291            // shift left (already integral); inf/nan/`exp ≥ 63 →` the saturating `as i64` cast. For a negative
292            // value, `floor = -(truncated magnitude) - (had a fraction ? 1 : 0)`. The operand `a` must be a
293            // plain 64-bit wire/const (a float bit-pattern), never a wrapped expression (it is bit-indexed).
294            FuncUnit::FFloorF => ffloor_verilog(a),
295            // POPCOUNT — a synthesizable bit-adder tree (the 64-bit zero-extended sum of the operand's bits),
296            // bit-identical to the eval `(a as u64).count_ones()`.
297            FuncUnit::PopCount => popcount_verilog(a),
298            FuncUnit::FAddF
299            | FuncUnit::FSubF
300            | FuncUnit::FMulF
301            | FuncUnit::FDivF
302            | FuncUnit::FSqrtF
303            | FuncUnit::IntToF
304            | FuncUnit::FLtF
305            | FuncUnit::FGtF
306            | FuncUnit::FLeF
307            | FuncUnit::FGeF
308            | FuncUnit::FEqF
309            | FuncUnit::FNeF => panic!(
310                "the float-lane RTL (an FPU functional unit binding fpu.v) is emitted by \
311                 `emit_float_datapath_verilog` (the sequenced float lane), not the combinational tape — its \
312                 semantics are pinned by the eval + differential + iverilog (float_soc_lock)"
313            ),
314        }
315    }
316}
317
318/// Why a cell's fixed-width value is not the value the VM computes for the same operands. The fabric's
319/// arithmetic is the 64-bit projection of the VM's; the VM is exact and total in a way no fixed-width datapath
320/// is, so every point where the two part is named here, and a checked evaluation reports it instead of
321/// producing the wrapped, zeroed or saturated word.
322#[derive(
323    Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
324)]
325pub enum Divergence {
326    /// The exact integer result leaves `i64`: the VM promotes it to a `BigInt`.
327    IntOverflow,
328    /// An integer division or remainder by zero: the VM fails `Division by zero` / `Modulo by zero`.
329    DivideByZero,
330    /// An integer base raised to a negative exponent: the VM fails.
331    NegativeExponent,
332    /// An integer exponent beyond `u32`: the VM fails `exponent too large`.
333    ExponentTooLarge,
334    /// A float divided by zero of either sign: the VM fails `Division by zero`.
335    FloatDivideByZero,
336}
337
338impl Divergence {
339    pub const ALL: [Divergence; 5] = [
340        Divergence::IntOverflow,
341        Divergence::DivideByZero,
342        Divergence::NegativeExponent,
343        Divergence::ExponentTooLarge,
344        Divergence::FloatDivideByZero,
345    ];
346
347    /// The stable code a device reports for this divergence (0 is reserved for "none").
348    pub fn code(self) -> u32 {
349        match self {
350            Divergence::IntOverflow => 1,
351            Divergence::DivideByZero => 2,
352            Divergence::NegativeExponent => 3,
353            Divergence::ExponentTooLarge => 4,
354            Divergence::FloatDivideByZero => 5,
355        }
356    }
357
358    /// The divergence a device code names.
359    pub fn from_code(code: u32) -> Option<Divergence> {
360        Divergence::ALL.into_iter().find(|d| d.code() == code)
361    }
362
363    /// The stable name used in reports and accounting.
364    pub fn name(self) -> &'static str {
365        match self {
366            Divergence::IntOverflow => "int-overflow",
367            Divergence::DivideByZero => "divide-by-zero",
368            Divergence::NegativeExponent => "negative-exponent",
369            Divergence::ExponentTooLarge => "exponent-too-large",
370            Divergence::FloatDivideByZero => "float-divide-by-zero",
371        }
372    }
373}
374
375impl std::fmt::Display for Divergence {
376    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
377        f.write_str(self.name())
378    }
379}
380
381impl AluOp {
382    /// The VM's value for this op on two `Int`s, or the divergence that keeps the word from being it. `+ − ×`
383    /// are exact in the VM (they promote); the bitwise ops and the masked shifts are the VM's own `i64` ops.
384    pub fn eval_checked(&self, a: i64, b: i64) -> Result<i64, Divergence> {
385        match self {
386            AluOp::Add => a.checked_add(b).ok_or(Divergence::IntOverflow),
387            AluOp::Sub => a.checked_sub(b).ok_or(Divergence::IntOverflow),
388            AluOp::Mul => a.checked_mul(b).ok_or(Divergence::IntOverflow),
389            AluOp::And | AluOp::Or | AluOp::Xor | AluOp::Shl | AluOp::Shr | AluOp::UShr => {
390                Ok(self.eval(a, b))
391            }
392        }
393    }
394}
395
396impl FuncUnit {
397    /// The VM's value for this unit, or the divergence that keeps the word from being it. Operands follow the
398    /// unit's convention (integers by value, floats as `f64` bit patterns); so does the result.
399    pub fn eval_checked(&self, a: i64, b: i64) -> Result<i64, Divergence> {
400        match self {
401            FuncUnit::DivS => {
402                if b == 0 {
403                    Err(Divergence::DivideByZero)
404                } else {
405                    a.checked_div(b).ok_or(Divergence::IntOverflow)
406                }
407            }
408            FuncUnit::ModS => {
409                if b == 0 {
410                    Err(Divergence::DivideByZero)
411                } else {
412                    Ok(a.wrapping_rem(b))
413                }
414            }
415            FuncUnit::FloorDivS => {
416                if b == 0 {
417                    Err(Divergence::DivideByZero)
418                } else if a == i64::MIN && b == -1 {
419                    Err(Divergence::IntOverflow)
420                } else {
421                    Ok(self.eval(a, b))
422                }
423            }
424            FuncUnit::Pow => {
425                if b < 0 {
426                    Err(Divergence::NegativeExponent)
427                } else {
428                    let e = u32::try_from(b).map_err(|_| Divergence::ExponentTooLarge)?;
429                    a.checked_pow(e).ok_or(Divergence::IntOverflow)
430                }
431            }
432            FuncUnit::FDivF => {
433                if f64::from_bits(b as u64) == 0.0 {
434                    Err(Divergence::FloatDivideByZero)
435                } else {
436                    Ok(self.eval(a, b))
437                }
438            }
439            FuncUnit::MulHiU
440            | FuncUnit::ApproxEqF
441            | FuncUnit::FAddF
442            | FuncUnit::FSubF
443            | FuncUnit::FMulF
444            | FuncUnit::FSqrtF
445            | FuncUnit::FFloorF
446            | FuncUnit::MulOvfS
447            | FuncUnit::IntToF
448            | FuncUnit::FLtF
449            | FuncUnit::FGtF
450            | FuncUnit::FLeF
451            | FuncUnit::FGeF
452            | FuncUnit::FEqF
453            | FuncUnit::FNeF
454            | FuncUnit::PopCount => Ok(self.eval(a, b)),
455        }
456    }
457}
458
459/// The combinational IEEE-754 `floor(f64) → i64` expression for [`FuncUnit::FFloorF`], fully inlined over the
460/// operand `a` (a plain 64-bit float bit-pattern wire). Kept a free function so the ~15 repetitions of `a`
461/// don't clutter [`FuncUnit::verilog`]. Bit-exact to `f64::from_bits(a).floor() as i64` (validated by iverilog
462/// in `fabric_ffloor_lock` over a battery spanning integers, fractions, negatives, and the saturating cast).
463fn ffloor_verilog(a: &str) -> String {
464    // Reused sub-expressions (inlined because the emit site wants ONE expression, no helper wires).
465    let exp = format!("($signed({{2'b0, {a}[62:52]}}) - 13'sd1023)"); // unbiased exponent
466    let mant = format!("({a}[62:52]==11'd0 ? {{1'b0,{a}[51:0]}} : {{1'b1,{a}[51:0]}})"); // 53-bit significand
467                                                                                         // truncated-toward-zero magnitude of floor(|a|):
468    let tmag = format!(
469        "({a}[62:52] == 11'd2047 ? 64'h7FFFFFFFFFFFFFFF : \
470         ({exp} < 0 ? 64'd0 : \
471         ({exp} >= 63 ? 64'hFFFFFFFFFFFFFFFF : \
472         ({exp} >= 52 ? ({{11'd0, {mant}}} << ({exp} - 52)) \
473         : ({{11'd0, {mant}}} >> (52 - {exp}))))))"
474    );
475    // whether the truncation dropped any fraction bits (only in the 0 ≤ exp < 52 shift-right branch):
476    let had_frac = format!(
477        "({a}[62:52] == 11'd2047 ? 1'b0 : \
478         ({exp} < 0 ? ({mant} != 53'd0) : \
479         ({exp} >= 52 ? 1'b0 : \
480         ((({{11'd0, {mant}}}) & ((64'd1 << (52 - {exp})) - 64'd1)) != 64'd0))))"
481    );
482    // floor: positive → truncation; negative → -(magnitude) - (had_frac ? 1 : 0).
483    format!("({a}[63] ? (-$signed({tmag}) - ({had_frac} ? 64'sd1 : 64'sd0)) : $signed({tmag}))")
484}
485
486/// The combinational POPCOUNT expression for [`FuncUnit::PopCount`]: the 64-bit zero-extended sum of the
487/// operand's individual bits (`{63'd0, a[0]} + {63'd0, a[1]} + …`). A plain bit-adder tree — synthesizable
488/// (yosys maps it to LUT adders) and bit-identical to `(a as u64).count_ones()`. The operand `a` must be a
489/// plain 64-bit wire/const (it is bit-indexed).
490fn popcount_verilog(a: &str) -> String {
491    let terms = (0..64)
492        .map(|i| format!("{{63'd0, {a}[{i}]}}"))
493        .collect::<Vec<_>>()
494        .join(" + ");
495    format!("({terms})")
496}
497
498/// An operand of a residual op: a circuit input (a function parameter), a prior SSA value, or
499/// a compile-time constant (already specialized in by the partial evaluator).
500#[derive(Clone, Copy, Debug, PartialEq, Hash, serde::Serialize, serde::Deserialize, Eq)]
501pub enum Src {
502    In(usize),
503    Val(usize),
504    Const(i64),
505}
506
507impl Src {
508    fn verilog(&self) -> String {
509        match self {
510            Src::In(k) => format!("in{k}"),
511            Src::Val(k) => format!("v{k}"),
512            Src::Const(c) => {
513                if *c < 0 {
514                    format!("-64'sd{}", (*c as i128).unsigned_abs())
515                } else {
516                    format!("64'sd{c}")
517                }
518            }
519        }
520    }
521    pub fn eval(&self, inputs: &[i64], vals: &[i64]) -> i64 {
522        match self {
523            Src::In(k) => inputs[*k],
524            Src::Val(k) => vals[*k],
525            Src::Const(c) => *c,
526        }
527    }
528}
529
530/// One residual operation: `v_i = a op b`.
531#[derive(Clone, Copy, Debug, PartialEq, Hash, serde::Serialize, serde::Deserialize, Eq)]
532pub struct FabricOp {
533    pub op: AluOp,
534    pub a: Src,
535    pub b: Src,
536}
537
538/// Emit the residual as a fixed dataflow circuit — one combinational `wire` per SSA value, no
539/// PC / fetch / dispatch. `result` names the SSA value driving the output. 64-bit, so the
540/// circuit's wrapping arithmetic is bit-identical to the VM's `Int`.
541pub fn emit_fabric_verilog(name: &str, n_in: usize, ops: &[FabricOp], result: usize) -> String {
542    let inputs = (0..n_in)
543        .map(|k| format!("input [63:0] in{k}"))
544        .collect::<Vec<_>>()
545        .join(", ");
546    let mut body = String::new();
547    for (i, op) in ops.iter().enumerate() {
548        body.push_str(&format!(
549            "  wire [63:0] v{i} = {};\n",
550            op.op.verilog(&op.a.verilog(), &op.b.verilog())
551        ));
552    }
553    format!(
554        "// fabric_{name} — a compiled dispatch-free residual as a fixed dataflow circuit.\n\
555         // No program counter, no fetch, no opcode dispatch: the residual's ops are WIRED\n\
556         // (SSA), not interpreted. Futamura P1: the specialized residual IS the circuit.\n\
557         // Lowered from the REAL compiler's residual; 64-bit so it wraps identically to the\n\
558         // VM's Int; proven == the running VM by iverilog.\n\
559         module fabric_{name}({inputs}, output [63:0] result);\n\
560         {body}  assign result = v{result};\n\
561         endmodule\n"
562    )
563}
564
565/// Evaluate the residual op-by-op (64-bit wrapping) — the reference the circuit is checked
566/// against, identical to the VM's arithmetic.
567pub fn eval_fabric(ops: &[FabricOp], inputs: &[i64], result: usize) -> i64 {
568    let mut vals: Vec<i64> = Vec::with_capacity(ops.len());
569    for op in ops {
570        let a = op.a.eval(inputs, &vals);
571        let b = op.b.eval(inputs, &vals);
572        vals.push(op.op.eval(a, b));
573    }
574    vals[result]
575}
576
577/// **The genuine Futamura P1 lowering.** Compile `src` with the real compiler, take the
578/// straight-line register bytecode of function `fn_name` (its dispatch-free residual), and
579/// lower it register→SSA to a dataflow op-list. Parameters become circuit inputs; `LoadConst`
580/// becomes a baked-in constant; `Add/Sub/Mul` become wired functional units. Also returns the
581/// input arity (parameter count). Panics on a non-straight-line op (control flow / unsupported)
582/// — the honest boundary of this block.
583pub fn residual_from_source(src: &str, fn_name: &str) -> (Vec<FabricOp>, usize, usize) {
584    use crate::vm::{Compiler, Constant, Op};
585    use std::collections::HashMap;
586
587    crate::ui_bridge::with_parsed_program(src, |parsed, interner| {
588        let (stmts, _types, _pol) = parsed.expect("parse failed");
589        let program = Compiler::compile(stmts, interner).expect("compile failed");
590        let f = program
591            .functions
592            .iter()
593            .find(|f| interner.resolve(f.name) == fn_name)
594            .unwrap_or_else(|| panic!("function `{fn_name}` not found in the compiled program"));
595
596        // The function's straight-line body: entry_pc .. first Return/ReturnNothing.
597        let mut body: Vec<Op> = Vec::new();
598        for op in &program.code[f.entry_pc..] {
599            body.push(*op);
600            if matches!(op, Op::Return { .. } | Op::ReturnNothing) {
601                break;
602            }
603        }
604
605        // register → Src (SSA). Parameters occupy R0..param_count.
606        let mut reg: HashMap<u16, Src> = HashMap::new();
607        for p in 0..f.param_count {
608            reg.insert(p, Src::In(p as usize));
609        }
610        let mut ops: Vec<FabricOp> = Vec::new();
611        let mut result: Option<Src> = None;
612        let get = |reg: &HashMap<u16, Src>, r: &u16| -> Src {
613            *reg.get(r)
614                .unwrap_or_else(|| panic!("residual reads unwritten register R{r}"))
615        };
616        for op in &body {
617            let binop = |aop: AluOp,
618                         dst: &u16,
619                         lhs: &u16,
620                         rhs: &u16,
621                         reg: &mut HashMap<u16, Src>,
622                         ops: &mut Vec<FabricOp>| {
623                let k = ops.len();
624                ops.push(FabricOp {
625                    op: aop,
626                    a: get(reg, lhs),
627                    b: get(reg, rhs),
628                });
629                reg.insert(*dst, Src::Val(k));
630            };
631            match op {
632                Op::LoadConst { dst, idx } => {
633                    let v = match program.constants[*idx as usize] {
634                        Constant::Int(n) => n,
635                        ref c => panic!("fabric residual: non-integer constant {c:?}"),
636                    };
637                    reg.insert(*dst, Src::Const(v));
638                }
639                Op::Move { dst, src } => {
640                    let s = get(&reg, src);
641                    reg.insert(*dst, s);
642                }
643                Op::Add { dst, lhs, rhs } => binop(AluOp::Add, dst, lhs, rhs, &mut reg, &mut ops),
644                Op::Sub { dst, lhs, rhs } => binop(AluOp::Sub, dst, lhs, rhs, &mut reg, &mut ops),
645                Op::Mul { dst, lhs, rhs } => binop(AluOp::Mul, dst, lhs, rhs, &mut reg, &mut ops),
646                // Fixed-point rescale: the Q-format `shifted right/left by` — the tiles already
647                // exist (AluOp::Shr/Shl), residual extraction just accepts them. Lets a quantized
648                // convolution stencil lower to a dataflow circuit.
649                Op::Shr { dst, lhs, rhs } => binop(AluOp::Shr, dst, lhs, rhs, &mut reg, &mut ops),
650                Op::Shl { dst, lhs, rhs } => binop(AluOp::Shl, dst, lhs, rhs, &mut reg, &mut ops),
651                // Bitwise ops complete the fixed-point/branch-free toolkit — e.g. a Sobel edge
652                // detector's `|x| = (x ^ (x>>>63)) - (x>>>63)` needs Xor. Tiles already exist.
653                Op::BitXor { dst, lhs, rhs } => {
654                    binop(AluOp::Xor, dst, lhs, rhs, &mut reg, &mut ops)
655                }
656                Op::BitAnd { dst, lhs, rhs } => {
657                    binop(AluOp::And, dst, lhs, rhs, &mut reg, &mut ops)
658                }
659                Op::BitOr { dst, lhs, rhs } => binop(AluOp::Or, dst, lhs, rhs, &mut reg, &mut ops),
660                Op::Return { src } => result = Some(get(&reg, src)),
661                Op::ReturnNothing => {}
662                other => panic!(
663                    "fabric residual: `{}` is not a straight-line arithmetic op (control flow / \
664                     unsupported — loops map to a CGRA, roadmap)",
665                    other.name()
666                ),
667            }
668        }
669        let result = match result {
670            Some(Src::Val(k)) => k,
671            other => panic!("fabric residual result is not a computed value: {other:?}"),
672        };
673        (ops, result, f.param_count as usize)
674    })
675}
676
677/// Lower a residual op-list to a `BoundedExpr` (bit-vector datapath) at the given width — the
678/// bridge that lets our certified SAT reason about the emitted circuit exhaustively.
679pub fn fabric_to_bounded(
680    ops: &[FabricOp],
681    result: usize,
682    width: u32,
683) -> crate::codegen_sva::sva_to_verify::BoundedExpr {
684    use crate::codegen_sva::sva_to_verify::{BitVecBoundedOp, BoundedExpr};
685    fn src_expr(s: Src, vals: &[BoundedExpr], width: u32) -> BoundedExpr {
686        match s {
687            Src::In(k) => BoundedExpr::BitVecVar(format!("in{k}"), width),
688            Src::Val(k) => vals[k].clone(),
689            Src::Const(c) => BoundedExpr::BitVecConst {
690                width,
691                value: c as u64,
692            },
693        }
694    }
695    let mut vals: Vec<BoundedExpr> = Vec::with_capacity(ops.len());
696    for op in ops {
697        let a = src_expr(op.a, &vals, width);
698        let b = src_expr(op.b, &vals, width);
699        let bop = match op.op {
700            AluOp::Add => BitVecBoundedOp::Add,
701            AluOp::Sub => BitVecBoundedOp::Sub,
702            AluOp::Mul => BitVecBoundedOp::Mul,
703            AluOp::And => BitVecBoundedOp::And,
704            AluOp::Or => BitVecBoundedOp::Or,
705            AluOp::Xor => BitVecBoundedOp::Xor,
706            AluOp::Shl => BitVecBoundedOp::Shl,
707            AluOp::Shr => BitVecBoundedOp::AShr,
708            AluOp::UShr => BitVecBoundedOp::Shr,
709        };
710        vals.push(BoundedExpr::BitVecBinary {
711            op: bop,
712            left: Box::new(a),
713            right: Box::new(b),
714        });
715    }
716    vals[result].clone()
717}
718
719/// The demonstration residual: a degree-3 polynomial in Horner form, `2x³ + 5x² − 3x + 100`.
720/// Straight-line (`+ - *`), so the compiler residualizes it dispatch-free.
721pub const POLY_BODY: &str = "(((2 * x + 5) * x - 3) * x + 100)";
722
723/// The full program, with `Main` chosen by the caller (`Show poly(N).`).
724pub fn poly_program(main: &str) -> String {
725    format!("## To poly (x: Int) -> Int:\n    Return {POLY_BODY}.\n## Main\n    {main}")
726}
727
728/// A self-checking testbench: drive N inputs, compare `result` against the VM oracle from
729/// `fabric_{in,out}.hex`. Prints PASS/FAIL. 64-bit values.
730pub fn emit_fabric_testbench(name: &str, n: usize) -> String {
731    format!(
732        r#"// tb_fabric_{name} — the dataflow circuit vs the RUNNING VM's output for the program.
733`timescale 1ns/1ps
734module tb_fabric_{name};
735  reg  [63:0] in0;
736  reg  [63:0] oin [0:{last}];
737  reg  [63:0] oout [0:{last}];
738  wire [63:0] result;
739  integer i, errors = 0;
740  fabric_{name} dut(.in0(in0), .result(result));
741  initial begin
742    $readmemh("fabric_in.hex", oin);
743    $readmemh("fabric_out.hex", oout);
744    for (i = 0; i < {n}; i = i + 1) begin
745      in0 = oin[i]; #1;
746      if (result !== oout[i]) begin errors = errors + 1; $display("FAIL eval %0d", i); end
747    end
748    if (errors == 0) $display("PASS: {n} circuit evaluations match the VM (residual = circuit, no interpreter)");
749    else $display("FAIL: %0d fabric mismatches", errors);
750    $finish;
751  end
752endmodule
753"#,
754        name = name,
755        last = n - 1,
756        n = n,
757    )
758}
759
760// ===========================================================================================
761// CGRA — the counted-loop projection: control flow in silicon (Futamura P1, sequential).
762//
763// A straight-line residual is a fixed SPATIAL circuit (above). A COUNTED LOOP is a fixed
764// spatial BODY datapath plus a sequential controller that streams the loop-carried state
765// through it once per induction value — exactly how a Coarse-Grained Reconfigurable Array runs
766// a loop: the array is configured ONCE with the body; iteration is TEMPORAL (clock the state
767// through the fixed fabric), not fetched/dispatched. `Repeat for i from lo to hi:` compiles to
768// a range iterator (`NewRange`/`IterNext`) over an accumulator; we recover the loop-carried
769// state, the induction variable, and the straight-line body, and lower them to that CGRA.
770//
771// Soundness is k-induction made physical: the BODY's one-step next-state is certified ≡ the
772// VM's per-iteration transition EXHAUSTIVELY by our own SAT (the induction STEP); the controller
773// counts the induction variable across exactly `lo..=hi` (the base + trip count); so the CGRA's
774// final state equals the VM's loop result for every input, by induction over the iterations.
775// ===========================================================================================
776
777/// A signed comparison (the VM's `Int` order) that gates a predicated select.
778#[derive(Clone, Copy, Debug, PartialEq, Hash, serde::Serialize, serde::Deserialize, Eq)]
779pub enum CmpKind {
780    Lt,
781    Gt,
782    Le,
783    Ge,
784    Eq,
785    Ne,
786}
787
788impl CmpKind {
789    pub fn eval(&self, a: i64, b: i64) -> bool {
790        match self {
791            CmpKind::Lt => a < b,
792            CmpKind::Gt => a > b,
793            CmpKind::Le => a <= b,
794            CmpKind::Ge => a >= b,
795            CmpKind::Eq => a == b,
796            CmpKind::Ne => a != b,
797        }
798    }
799    /// The signed Verilog relational operator.
800    pub fn verilog(&self) -> &'static str {
801        match self {
802            CmpKind::Lt => "<",
803            CmpKind::Gt => ">",
804            CmpKind::Le => "<=",
805            CmpKind::Ge => ">=",
806            CmpKind::Eq => "==",
807            CmpKind::Ne => "!=",
808        }
809    }
810}
811
812/// One body cell of the CGRA fabric: an arithmetic tile, or a predicated SELECT (mux) tile whose
813/// comparison is folded in. A branch in the loop body is if-CONVERTED to a `Select`: both arms
814/// compute, the signed predicate selects — control becomes data, no divergence.
815#[derive(Clone, Copy, Debug, PartialEq, Hash, serde::Serialize, serde::Deserialize, Eq)]
816pub enum LoopCell {
817    /// `v = a op b`.
818    Arith { op: AluOp, a: Src, b: Src },
819    /// `v = (ca cmp cb) ? t : e` — the CGRA mux tile, the predicate folded in.
820    Select {
821        cmp: CmpKind,
822        ca: Src,
823        cb: Src,
824        t: Src,
825        e: Src,
826    },
827    /// `v = a `unit` b` — a SEQUENTIAL functional-unit tile (signed division/remainder via the
828    /// `shared_divider`). Distinct from `Arith` so the emitter wires the multi-cycle divider, not a
829    /// combinational one. See [`FuncUnit`].
830    Func { unit: FuncUnit, a: Src, b: Src },
831    /// `v = seq[addr]` — a MEMORY-read tile reading the loop's iterated sequence (see
832    /// [`LoopFabric::seq_input`]) at `addr` (the induction variable). On silicon this is a scratchpad
833    /// read (the induction counter drives the address bus); in the model it indexes the sequence array.
834    MemRead { addr: Src },
835    /// `v = heap[addr]` — a HEAP-read tile (the shared state plane). The heap mirrors the VM's exact
836    /// layout: a collection at pointer `p` is `heap[p] = length` then its elements at `heap[p+1..=p+len]`
837    /// (1-based). `length of xs` is `HeapRead(p)`; `item i of xs` is `HeapRead(p + i)`. This is the
838    /// collection-read primitive the whole state plane composes from (with `HeapWrite`/`HeapAlloc`).
839    HeapRead { addr: Src },
840    /// `heap[addr] = val` (side effect); the tile's value is `val`. `SetIndex`/`ListPush` compose from this.
841    HeapWrite { addr: Src, val: Src },
842    /// Allocate `size` fresh words at the heap top (zeroed); the tile's value is the NEW pointer. `NewList`
843    /// composes an alloc + writes; value semantics (COW) are handled upstream by `DeepClone`/`EnsureOwned`.
844    HeapAlloc { size: Src },
845    /// A packed-SIMD tile: a `VecOp` (lane-wise add / bitwise) over two operand words for lane type `ty`.
846    /// First-class placeable — routes to a `CellKind::Simd` tile exactly like `Arith`; combinational; proven
847    /// ≡ N independent scalar ops by `simd_tile_lock` (SAT carry-isolation) + iverilog.
848    VectorArith {
849        op: super::simd::VecOp,
850        ty: super::simd::LaneTy,
851        a: Src,
852        b: Src,
853    },
854}
855
856/// A counted-loop residual lowered to a CGRA: a fixed BODY datapath (SSA over the current
857/// loop-carried state + the induction variable + loop invariants, INCLUDING predicated select
858/// tiles for data-dependent branches) driven by a sequential controller that clocks the state
859/// through the body once per induction value in the inclusive range `lo..=hi`.
860#[derive(Clone, Debug)]
861pub struct LoopFabric {
862    /// Circuit input arity (the function's parameters become circuit inputs `in0..`).
863    pub n_in: usize,
864    /// Number of loop-carried state registers (the accumulators).
865    pub n_state: usize,
866    /// Initial value of each state register, as a `Src` over the circuit inputs / constants.
867    pub init: Vec<Src>,
868    /// Inclusive induction range: the induction variable takes `lo, lo+1, …, hi`. Over inputs.
869    pub lo: Src,
870    pub hi: Src,
871    /// Loop-invariant values read by the body (fixed across iterations), as `Src` over inputs.
872    pub invariants: Vec<Src>,
873    /// The body as SSA `LoopCell`s. Body-input convention for `Src::In(k)`: `0..n_state` = the
874    /// current state registers, `n_state` = the induction variable, `n_state+1+j` = invariant `j`.
875    pub body: Vec<LoopCell>,
876    /// For each state register, the `Src` (over body inputs / body vals) giving its NEXT value
877    /// after one execution of the body.
878    pub next: Vec<Src>,
879    /// Which state register holds the loop's result (returned after the loop).
880    pub result_state: usize,
881    /// `Some(k)` marks a SEQUENCE loop (`Repeat for x in xs`) rather than a numeric range loop: input
882    /// `k` is the iterated `Seq` (a memory array), the induction range is `0..=len-1` derived from the
883    /// sequence's length at eval time, and the body reads `xs[iv]` through a [`LoopCell::MemRead`] tile.
884    /// `None` is a plain counted range loop (`lo`/`hi` are the numeric bounds). This is "collections as a
885    /// spatial resource": the induction variable IS the address generator streaming the array.
886    pub seq_input: Option<usize>,
887    /// The POST-LOOP transform (an epilogue island): straight-line `LoopCell`s over the loop's FINAL
888    /// state (`Src::In(i)` = final value of state register `i`), computing the returned value when the
889    /// function does more than return an accumulator (e.g. `Return (sum 1..n) * 2`). Empty when the loop
890    /// result IS a carried register — then [`Self::result_state`] names it. This is the first CFG
891    /// COMPOSITION: a loop island feeding a transform island, sequenced into one whole-function fabric.
892    pub epilogue: Vec<LoopCell>,
893    /// When `epilogue` is non-empty, the `Src` (over the final state / epilogue vals) giving the result.
894    pub epilogue_result: Src,
895    /// The PROLOGUE HEAP ISLAND: straight-line `LoopCell`s that build any LOCAL collection the loop iterates
896    /// (`Let ps be [(1,2),(3,4)]` → `HeapAlloc`/`HeapWrite`). Evaluated ONCE before the loop into a shared
897    /// heap; the loop body then reads/writes that same heap. Empty for a numeric / parameter-sequence loop.
898    pub prologue_heap: Vec<LoopCell>,
899    /// `Some(ptr)` marks a FOREACH OVER A LOCAL HEAP LIST (`Repeat for (a,b) in ps`, `ps` a local): `ptr`
900    /// (a `Src` over the prologue-heap island) is the iterated list's pointer, its length is `heap[ptr]`, and
901    /// element `iv` is `heap[ptr + 1 + iv]` — the body destructures it via `HeapRead` with the heap threaded.
902    /// Distinct from [`Self::seq_input`] (a scalar parameter array). `ptr` is passed to the body as the FINAL
903    /// invariant so the element-address generator can read it.
904    pub seq_heap_ptr: Option<Src>,
905}
906
907/// Render a `Src` with caller-supplied names for inputs and SSA vals (so the body can name its
908/// inputs `s0/iv/inv0` while the straight-line emitter names circuit inputs `in0`).
909fn src_named(
910    s: &Src,
911    name_in: &dyn Fn(usize) -> String,
912    name_val: &dyn Fn(usize) -> String,
913) -> String {
914    match s {
915        Src::In(k) => name_in(*k),
916        Src::Val(k) => name_val(*k),
917        Src::Const(c) => {
918            if *c < 0 {
919                format!("-64'sd{}", (*c as i128).unsigned_abs())
920            } else {
921                format!("64'sd{c}")
922            }
923        }
924    }
925}
926
927/// A promoted GLOBAL's fabric slot: in straight-line combinational code a module-level `Let` referenced
928/// inside a function is a pure DATAFLOW WIRE from its (single, set-before-use) definition to every use — no
929/// heap cell. The `Src` is that wire; the metadata (collection static length, text-ness, struct type)
930/// rides along exactly as it does through a `Move`, so a global holding a list/text/struct stays typed.
931#[derive(Clone, Copy)]
932struct GSlot {
933    src: Src,
934    len: Option<i64>,
935    text: bool,
936    ty: Option<u32>,
937}
938
939/// How a single-field policy `Check` decides its GRANT, extracted from the `PolicyRegistry` at build. Each
940/// variant mirrors EXACTLY what the VM's `evaluate_policy_condition` computes for that field/literal, so the
941/// fabric grant is bit-identical to the VM's pass/fail — the security property (rule 8).
942#[derive(Clone, Debug)]
943enum PolicyGrant {
944    /// A Bool/Int field compared to a parseable literal: grant ⇔ `subject.field == expected`
945    /// (Bool rides its slot as 0/1). The VM's `*n == e` / `*b == e`.
946    IntEq { slot: usize, expected: i64 },
947    /// A Text field compared to a literal: grant ⇔ `subject.field` (a char-list) equals `expected` byte for
948    /// byte. The VM's `s.as_str() == expected`.
949    TextEq { slot: usize, expected: Vec<u8> },
950    /// The VM's compare is UNSATISFIABLE, so the check always denies: a Bool/Int field against a literal the
951    /// VM can't parse to that type (e.g. `level equals 5` — the number is not captured, resolving to the
952    /// `"unknown"` sentinel — or a quoted string against a Bool/Int field, where the VM returns `false`).
953    /// Modeled as a constant-0 grant so the fabric matches the VM's always-deny instead of refusing.
954    AlwaysDeny,
955}
956
957/// The temporal kind a register holds, so a date-component builtin (`the hour of …`) picks the right decode:
958/// a `Moment` is nanoseconds since the epoch (`LoadNow`), a `Date` is whole days (`LoadToday`).
959#[derive(Clone, Copy, PartialEq, Eq, Debug)]
960enum TimeKind {
961    Moment,
962    Date,
963}
964
965thread_local! {
966    /// `NewStruct`'s field count keyed by its `type_name` constant index — the ONE piece of program
967    /// context `exec_ops` needs that the op itself doesn't carry (`GetField`/`StructInsert` carry the field
968    /// index directly). Populated by `decision_fabric_from_source` from `program.struct_types` before it
969    /// lowers, read by the `NewStruct` arm. A thread-local (like `NET_OFFLINE`) avoids threading a param
970    /// through every `exec_ops`/`dec_convert` call site for a single struct-shape lookup.
971    static STRUCT_FIELDS: std::cell::RefCell<std::collections::HashMap<u32, usize>> =
972        std::cell::RefCell::new(std::collections::HashMap::new());
973
974    /// `(struct type_name const, field_name const) -> 0-based slot position`. Resolves `GetField`/
975    /// `StructInsert`'s `field` (a constant index to the field NAME) to a heap offset — the field lives at
976    /// `heap[ptr + 1 + position]`. Populated per program from `struct_types`.
977    static STRUCT_FIELD_POS: std::cell::RefCell<std::collections::HashMap<(u32, u32), usize>> =
978        std::cell::RefCell::new(std::collections::HashMap::new());
979
980    /// `register -> struct type_name const` — register TYPE-TRACKING. A register's type is BRANCH-INVARIANT
981    /// (a variable has one type on every path), so a single shared map suffices (no per-branch cloning). Set
982    /// at `NewStruct`, propagated through `Move`, read at `GetField`/`StructInsert` to pick the field slot.
983    static REG_TYPE: std::cell::RefCell<std::collections::HashMap<u16, u32>> =
984        std::cell::RefCell::new(std::collections::HashMap::new());
985
986    /// The whole program's code + per-function `(entry_pc, param_count)`, so a `Call` can INLINE its callee's
987    /// decision fabric (the Futamura projection: specializing the interpreter for the call site). Set by
988    /// `decision_fabric_from_source`; `INLINE_DEPTH` guards against a recursive/too-deep call graph (which
989    /// would inline forever — a spatial call STACK, not inlining, is the honest answer there).
990    static INLINE_CODE: std::cell::RefCell<Vec<crate::vm::Op>> = std::cell::RefCell::new(Vec::new());
991    static INLINE_FNS: std::cell::RefCell<Vec<(usize, u16)>> = std::cell::RefCell::new(Vec::new());
992    static INLINE_DEPTH: std::cell::RefCell<usize> = std::cell::RefCell::new(0);
993
994    /// `register -> STATIC collection length`, tracked through straight-line construction (`NewList`/
995    /// `NewTuple` count, `NewEmptyList` 0, ±1 per push/pop, propagated by `Move`). The combinational heap
996    /// model already only supports straight-line heap mutation (a branchy `HeapWrite` fires unconditionally),
997    /// so this length is EXACT within that same scope — it lets `Contains` unroll into a bounded OR-scan.
998    static COLL_LEN: std::cell::RefCell<std::collections::HashMap<u16, i64>> =
999        std::cell::RefCell::new(std::collections::HashMap::new());
1000
1001    /// The whole-program output TAPE: for each `Show`, the value `Src` and whether it is TEXT (a char-list
1002    /// pointer, rendered as its string) vs an Int (rendered as decimal). A `Show` is a side effect, so it is
1003    /// only sound in straight-line code (the whole-program builder refuses a branch).
1004    static SHOW_OUTPUTS: std::cell::RefCell<Vec<(Src, bool, Option<Src>)>> =
1005        std::cell::RefCell::new(Vec::new());
1006
1007    /// Registers currently holding TEXT (a char-list `[len, byte0..]` on the heap). Set at a Text `LoadConst`
1008    /// / `Concat` / `FormatValue`, propagated by `Move` — so `Show` renders the value as a string, not a
1009    /// number. (Text type is branch-invariant, like a struct type, so a shared set suffices.)
1010    static TEXT_REGS: std::cell::RefCell<std::collections::HashSet<u16>> =
1011        std::cell::RefCell::new(std::collections::HashSet::new());
1012
1013    /// Registers holding a Text constant, by its constant index: the NAME a struct field is keyed by when a
1014    /// place mutation writes it back (`SetIndex obj[name] = v`). Set at a Text `LoadConst`, carried by `Move`.
1015    static NAME_REGS: std::cell::RefCell<std::collections::HashMap<u16, u32>> =
1016        std::cell::RefCell::new(std::collections::HashMap::new());
1017
1018    /// Registers currently holding a FLOAT (an `f64` bit pattern in an i64 slot). Set at a `Constant::Float`
1019    /// load / float arithmetic / `Sqrt`, propagated by `Move`. Routes `Add`/`Sub`/`Mul`/`Div` through the
1020    /// float functional units instead of the integer ALU, and makes `Show` refuse (float→string is a
1021    /// follow-on). Type is branch-invariant, so a shared set suffices (like `TEXT_REGS`).
1022    static FLOAT_REGS: std::cell::RefCell<std::collections::HashSet<u16>> =
1023        std::cell::RefCell::new(std::collections::HashSet::new());
1024
1025    /// Registers holding a WORD32 (ℤ/2³²) value — set by a `word32()` construction or any word32 builtin
1026    /// (`word_and`/`word_or`/`word_not`/`rotl`/`rotr`/`word32Shr`), propagated by `Move` and the bitwise ops,
1027    /// cleared by `intOfWord32`. Makes integer `Add`/`Sub`/`Mul` on a word32 operand WRAP mod 2³² (mask to 32
1028    /// bits) — the SHA-256 round add. Branch-invariant, so a shared set suffices (like `FLOAT_REGS`).
1029    static W32_REGS: std::cell::RefCell<std::collections::HashSet<u16>> =
1030        std::cell::RefCell::new(std::collections::HashSet::new());
1031
1032    /// Registers holding a collection whose ELEMENTS are floats (a `Seq of Float`). Set when a float value is
1033    /// pushed / stored into a collection (or a `NewList` of float elements), propagated by `Move`. Read at
1034    /// `Index`/`ListPop` to mark the read register float — the float-in-Seq bridge, so `item i of v` off a float
1035    /// Seq flows into the float functional units (nbody/spectral). Branch-invariant, so a shared set suffices.
1036    static FLOAT_COLLS: std::cell::RefCell<std::collections::HashSet<u16>> =
1037        std::cell::RefCell::new(std::collections::HashSet::new());
1038
1039    /// `register -> "is this value a BigInt?" flag Src` for the `BigIntWide` lane. A value PROMOTES to a
1040    /// BigInt when an `Add`/`Mul` overflows `i64` (the VM's `Value` returns `BigInt` then, and `Low` of a
1041    /// BigInt is 0). Bigness is MONOTONE under `Add`/`Mul` (only `Sub` could un-promote, which is why `Sub`
1042    /// is refused), so the fabric tracks a running 0/1 flag; `Low` gates on it, and `Show` refuses a BigInt.
1043    static BIGINT_BIG: std::cell::RefCell<std::collections::HashMap<u16, Src>> =
1044        std::cell::RefCell::new(std::collections::HashMap::new());
1045
1046    /// Promoted globals: `global index -> GSlot`. A module-level `Let` referenced from a function compiles
1047    /// to `GlobalSet` (at the definition) + `GlobalGet` (at each use). In the straight-line fabric the value
1048    /// is set before it is read, so the map carries the definition's wire to every reader — a global is a
1049    /// wire, not a cell (like `Move`, which is why `GlobalGet`/`GlobalSet` classify as `Wire`).
1050    static GLOBALS: std::cell::RefCell<std::collections::HashMap<u16, GSlot>> =
1051        std::cell::RefCell::new(std::collections::HashMap::new());
1052
1053    /// The OFFLINE-networking loopback inbox: a FIFO (same `[read_cursor, count, ..]` shape as a channel)
1054    /// established by `NetListen`. `NetSend` PUBLISHES into it (output-transparent until drained) and
1055    /// `NetAwait` DRAINS it — a `Send … to <self> ; Await … from <self>` round-trips locally, exactly the
1056    /// deterministic single-node loopback the net oracle uses. `None` until the first `Listen`.
1057    static NET_INBOX: std::cell::RefCell<Option<Src>> = std::cell::RefCell::new(None);
1058
1059    /// A `select`'s pending arms, in declaration order: `Some((channel_ptr, var))` for a `Receive` arm,
1060    /// `None` for an `After …` timeout arm. `SelectArmRecv`/`SelectArmTimeout` push; `SelectWait` consumes
1061    /// them to resolve the winner (first non-empty channel, else the timeout) and drains only the winner.
1062    static SELECT_ARMS: std::cell::RefCell<Vec<Option<(Src, u16)>>> =
1063        std::cell::RefCell::new(Vec::new());
1064
1065    /// Set once a `FailWith` fires: the program has raised an error, so NOTHING it would `Show` afterward
1066    /// reaches stdout (the VM returns `Err` at that point). The whole-program output tape suppresses every
1067    /// subsequent `Show`, so the fabric's tape equals the VM's output up to the failure. Reset per build.
1068    static HALTED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
1069
1070    /// `register -> TimeKind` — a register holding a clock value (`LoadNow` → Moment nanos, `LoadToday` →
1071    /// Date days), propagated by `Move`. Read by a `CallBuiltin` date-component extractor so `the hour of`
1072    /// decodes nanos-of-day and `the weekday of` decodes days — the fabric reads the SAME injected clock the
1073    /// VM does (`temporal::fixed_clock`), so the extracted Int is bit-identical. Type is branch-invariant.
1074    static REG_TIMEKIND: std::cell::RefCell<std::collections::HashMap<u16, TimeKind>> =
1075        std::cell::RefCell::new(std::collections::HashMap::new());
1076
1077    /// A `MakeClosure` result: `register -> (body function index, captured (value, present) pairs)`. The
1078    /// closure's body is a STATIC function index, so a made-then-called closure inlines like a `Call` whose
1079    /// callee is resolved from this map (the Futamura projection of dynamic dispatch). Captures are gathered
1080    /// at `MakeClosure` time (locals from `locals_start`, globals from `GLOBALS`) in the body's capture order.
1081    #[allow(clippy::type_complexity)]
1082    static CLOSURES: std::cell::RefCell<std::collections::HashMap<u16, (usize, Vec<(Src, bool)>)>> =
1083        std::cell::RefCell::new(std::collections::HashMap::new());
1084
1085    /// Per-function capture layout: `function index -> [None for a local capture | Some(global index)]`, in
1086    /// the body's `captures` order. Populated from `program.functions[*].captures` so `MakeClosure` knows how
1087    /// many locals to read from `locals_start` and which captures snapshot a global.
1088    static CLOSURE_CAPTURES: std::cell::RefCell<Vec<Vec<Option<u16>>>> =
1089        std::cell::RefCell::new(Vec::new());
1090
1091    /// The program's MODELED policy conditions: `(subject type-name const idx, predicate/action symbol
1092    /// index, is_capability) -> PolicyGrant`. A `Check` decides a GRANT predicate from the field's value; the
1093    /// `PolicyRegistry` that carries the conditions lives in the PARSE result (not the `CompiledProgram`), so
1094    /// `whole_program_fabric_from_source` extracts them here at build. Only the SINGLE-FIELD conditions the
1095    /// fabric can faithfully model are inserted (`FieldBool` / `FieldEquals` on a Bool/Int/Text field, incl.
1096    /// the always-deny case for a literal the VM can't parse); a compound (And/Or) / object-cross-field /
1097    /// predicate-indirection condition is left OUT so the `CheckPolicy` arm PANICS on it rather than silently
1098    /// granting — a wrong grant is a security hole (rule 8), so an unmodeled check fails loud, never quietly.
1099    static POLICY_CONDITIONS: std::cell::RefCell<std::collections::HashMap<(u32, u32, bool), PolicyGrant>> =
1100        std::cell::RefCell::new(std::collections::HashMap::new());
1101
1102    /// The running policy GRANT guard: an AND-chain (a 0/1 `Src`) of every `CheckPolicy` grant evaluated so
1103    /// far in program order. `None` = no check yet = unconditionally granted. Each `Show` records the guard
1104    /// in force at its point; `eval_whole_program` HALTS the output tape at the first `Show` whose guard is
1105    /// false — the observable equivalent of the VM returning `Err` at a failed `Check` (all later output
1106    /// suppressed). Grants are monotone-ANDed forward, so once false it stays false (halt = stop at first).
1107    static POLICY_GUARD: std::cell::RefCell<Option<Src>> = std::cell::RefCell::new(None);
1108}
1109
1110/// Resolve a `GetField`/`StructInsert` field (a constant index to the field NAME) to its 0-based heap slot,
1111/// using the object register's tracked struct type (`REG_TYPE`, set at `NewStruct`) and the per-type
1112/// name→slot map (`STRUCT_FIELD_POS`). The field then lives at `heap[ptr + 1 + slot]`.
1113fn struct_field_slot(obj_reg: u16, field_const: u32) -> usize {
1114    let tn = REG_TYPE.with(|m| m.borrow().get(&obj_reg).copied()).expect(
1115        "struct field access on a register with no tracked struct type \
1116                 (param-passed structs need param-type seeding — not yet wired)",
1117    );
1118    STRUCT_FIELD_POS
1119        .with(|m| m.borrow().get(&(tn, field_const)).copied())
1120        .expect("struct field slot not found for (type, field) — STRUCT_FIELD_POS not populated for this type")
1121}
1122
1123/// [`struct_field_slot`] for a field named by the Text constant `name` rather than by the constant a `GetField`
1124/// carries: the slot of whichever of the object's fields has that name.
1125fn struct_field_slot_named(obj_reg: u16, name: u32, consts: &[crate::vm::Constant]) -> usize {
1126    let tn = REG_TYPE
1127        .with(|m| m.borrow().get(&obj_reg).copied())
1128        .expect("struct field write on a register with no tracked struct type");
1129    let text = |i: u32| match consts.get(i as usize) {
1130        Some(crate::vm::Constant::Text(s)) => Some(s.as_str()),
1131        _ => None,
1132    };
1133    let wanted = text(name).expect("a struct field is keyed by a Text constant");
1134    STRUCT_FIELD_POS
1135        .with(|m| {
1136            m.borrow()
1137                .iter()
1138                .find(|((t, f), _)| *t == tn && text(*f) == Some(wanted))
1139                .map(|(_, &slot)| slot)
1140        })
1141        .unwrap_or_else(|| {
1142            panic!("struct field write: no field named {wanted:?} on the object's type")
1143        })
1144}
1145
1146/// The fabric's numeric tag for an enum constructor / variant NAME (a constant index into `consts`).
1147/// The VM matches by NAME (`ind.constructor == variant_name`), so the fabric assigns each distinct name a
1148/// canonical tag = the FIRST const index carrying that Text. Two const indices with the same Text collapse
1149/// to the same tag, so `NewInductive`'s written tag and `TestArm`'s compared tag agree ⟺ the names agree —
1150/// exactly the VM's semantics, with no dependence on `enum_types`.
1151fn canonical_text_tag(consts: &[crate::vm::Constant], idx: u32) -> i64 {
1152    let name = match &consts[idx as usize] {
1153        crate::vm::Constant::Text(s) => s.as_str(),
1154        other => panic!("enum tag: constant {idx} is not Text: {other:?}"),
1155    };
1156    consts
1157        .iter()
1158        .position(|c| matches!(c, crate::vm::Constant::Text(s) if s == name))
1159        .expect("enum tag: name not found in constants") as i64
1160}
1161
1162/// Build a DECIMAL char-list `[len, ('-')?, d_{k-1}, ..., d_0]` for integer `v` on the heap; returns the
1163/// pointer `Src` (`heap[ptr] = len`, the RUNTIME digit count). Most-significant-first, '-' for negatives, no
1164/// leading zeros (0 → "0"). 19 positions cover the full i64 range. This is the int→text rendering `Concat`
1165/// (string interpolation) performs on a non-text operand — byte-exact with the VM's `n.to_string()`.
1166fn push_int_to_decimal(cells: &mut Vec<LoopCell>, v: Src) -> Src {
1167    let ksign = cells.len();
1168    cells.push(LoopCell::Select {
1169        cmp: CmpKind::Lt,
1170        ca: v,
1171        cb: Src::Const(0),
1172        t: Src::Const(1),
1173        e: Src::Const(0),
1174    });
1175    let sign = Src::Val(ksign);
1176    let kneg = cells.len();
1177    cells.push(LoopCell::Arith {
1178        op: AluOp::Sub,
1179        a: Src::Const(0),
1180        b: v,
1181    });
1182    let km = cells.len();
1183    cells.push(LoopCell::Select {
1184        cmp: CmpKind::Lt,
1185        ca: v,
1186        cb: Src::Const(0),
1187        t: Src::Val(kneg),
1188        e: v,
1189    });
1190    let m = Src::Val(km);
1191    let ka = cells.len();
1192    cells.push(LoopCell::HeapAlloc {
1193        size: Src::Const(21),
1194    }); // header + '-' + up to 19 digits
1195    let ptr = Src::Val(ka);
1196    let kdaddr = cells.len(); // '-' at heap[ptr+1] (overwritten by the first digit when there is no sign)
1197    cells.push(LoopCell::Arith {
1198        op: AluOp::Add,
1199        a: ptr,
1200        b: Src::Const(1),
1201    });
1202    cells.push(LoopCell::HeapWrite {
1203        addr: Src::Val(kdaddr),
1204        val: Src::Const(45),
1205    });
1206    let kwp = cells.len(); // write_pos = sign ? 2 : 1
1207    cells.push(LoopCell::Select {
1208        cmp: CmpKind::Ne,
1209        ca: sign,
1210        cb: Src::Const(0),
1211        t: Src::Const(2),
1212        e: Src::Const(1),
1213    });
1214    let mut write_pos = Src::Val(kwp);
1215    let mut p10: i64 = 1;
1216    for _ in 0..18 {
1217        p10 *= 10; // 10^18
1218    }
1219    for i in (0..19).rev() {
1220        let kq = cells.len();
1221        cells.push(LoopCell::Func {
1222            unit: FuncUnit::DivS,
1223            a: m,
1224            b: Src::Const(p10),
1225        });
1226        let kr = cells.len();
1227        cells.push(LoopCell::Func {
1228            unit: FuncUnit::ModS,
1229            a: Src::Val(kq),
1230            b: Src::Const(10),
1231        });
1232        let kchar = cells.len();
1233        cells.push(LoopCell::Arith {
1234            op: AluOp::Add,
1235            a: Src::Val(kr),
1236            b: Src::Const(48),
1237        }); // '0' + digit
1238            // output this position iff it is the ones place OR a digit ≥ 10^i is present (i.e. not a leading zero)
1239        let output = if i == 0 {
1240            Src::Const(1)
1241        } else {
1242            let klt = cells.len();
1243            cells.push(LoopCell::Select {
1244                cmp: CmpKind::Lt,
1245                ca: m,
1246                cb: Src::Const(p10),
1247                t: Src::Const(1),
1248                e: Src::Const(0),
1249            });
1250            let kge = cells.len();
1251            cells.push(LoopCell::Select {
1252                cmp: CmpKind::Eq,
1253                ca: Src::Val(klt),
1254                cb: Src::Const(0),
1255                t: Src::Const(1),
1256                e: Src::Const(0),
1257            });
1258            Src::Val(kge)
1259        };
1260        let kwaddr = cells.len(); // write char at heap[ptr + write_pos] (uncounted if not output)
1261        cells.push(LoopCell::Arith {
1262            op: AluOp::Add,
1263            a: ptr,
1264            b: write_pos,
1265        });
1266        cells.push(LoopCell::HeapWrite {
1267            addr: Src::Val(kwaddr),
1268            val: Src::Val(kchar),
1269        });
1270        let kwp1 = cells.len();
1271        cells.push(LoopCell::Arith {
1272            op: AluOp::Add,
1273            a: write_pos,
1274            b: Src::Const(1),
1275        });
1276        let kwpn = cells.len(); // write_pos = output ? write_pos+1 : write_pos
1277        cells.push(LoopCell::Select {
1278            cmp: CmpKind::Ne,
1279            ca: output,
1280            cb: Src::Const(0),
1281            t: Src::Val(kwp1),
1282            e: write_pos,
1283        });
1284        write_pos = Src::Val(kwpn);
1285        p10 /= 10;
1286    }
1287    let klen = cells.len(); // len = write_pos - 1
1288    cells.push(LoopCell::Arith {
1289        op: AluOp::Sub,
1290        a: write_pos,
1291        b: Src::Const(1),
1292    });
1293    cells.push(LoopCell::HeapWrite {
1294        addr: ptr,
1295        val: Src::Val(klen),
1296    });
1297    ptr
1298}
1299
1300/// Every register a decision-fabric body WRITES (a superset used as the branch-merge carry set — a register
1301/// equal in both arms never spawns a `Select`). Comparison dsts are predicates, not state, so excluded.
1302fn carried_regs(body: &[(usize, crate::vm::Op)]) -> Vec<u16> {
1303    use crate::vm::Op;
1304    let mut carried: Vec<u16> = Vec::new();
1305    for (_, op) in body {
1306        let dst = match op {
1307            Op::LoadConst { dst, .. }
1308            | Op::Move { dst, .. }
1309            | Op::Add { dst, .. }
1310            | Op::Sub { dst, .. }
1311            | Op::Mul { dst, .. }
1312            | Op::Pow { dst, .. }
1313            | Op::BitAnd { dst, .. }
1314            | Op::BitOr { dst, .. }
1315            | Op::BitXor { dst, .. }
1316            | Op::Shl { dst, .. }
1317            | Op::Shr { dst, .. }
1318            | Op::Length { dst, .. }
1319            | Op::Index { dst, .. }
1320            | Op::IndexUnchecked { dst, .. }
1321            | Op::NewList { dst, .. }
1322            | Op::NewTuple { dst, .. }
1323            | Op::NewEmptyList { dst, .. }
1324            | Op::NewEmptyListWithCapacity { dst, .. }
1325            | Op::NewEmptyListFixed { dst, .. }
1326            | Op::NewEmptyListI32 { dst, .. }
1327            | Op::NewEmptySet { dst, .. }
1328            | Op::NewEmptyMap { dst, .. }
1329            | Op::NewEmptyMapWithCapacity { dst, .. }
1330            | Op::ListPop { dst, .. }
1331            | Op::NewStruct { dst, .. }
1332            | Op::GetField { dst, .. }
1333            | Op::NewInductive { dst, .. }
1334            | Op::BindArm { dst, .. }
1335            | Op::Not { dst, .. }
1336            | Op::Call { dst, .. }
1337            | Op::ChanNew { dst, .. }
1338            | Op::ChanRecv { dst, .. }
1339            | Op::ChanTrySend { dst, .. }
1340            | Op::Contains { dst, .. }
1341            | Op::DeepClone { dst, .. }
1342            | Op::SeqConcat { dst, .. }
1343            | Op::SliceOp { dst, .. }
1344            | Op::UnionOp { dst, .. }
1345            | Op::IntersectOp { dst, .. }
1346            | Op::Concat { dst, .. }
1347            | Op::AddAssign { dst, .. } => Some(*dst),
1348            _ => None,
1349        };
1350        if let Some(d) = dst {
1351            if !carried.contains(&d) {
1352                carried.push(d);
1353            }
1354        }
1355    }
1356    carried
1357}
1358
1359/// Inline a callee's decision fabric at a `Call` site — the Futamura projection made concrete: the callee's
1360/// combinational datapath is emitted directly into the caller's `cells`, with its parameters SEEDED by the
1361/// caller's argument `Src`s (so the callee's `Src::In` never appears) and its intermediate `Src::Val` indices
1362/// naturally absolute in the shared `cells` vector (no remapping). Returns the `Src` driving the callee's
1363/// return value. A recursive/too-deep call graph is refused loudly (that needs a spatial call stack).
1364fn inline_callee(
1365    func_idx: usize,
1366    arg_srcs: &[Src],
1367    cells: &mut Vec<LoopCell>,
1368    consts: &[crate::vm::Constant],
1369) -> Src {
1370    use crate::vm::Op;
1371    let (entry_pc, param_count) = INLINE_FNS.with(|f| {
1372        *f.borrow()
1373            .get(func_idx)
1374            .unwrap_or_else(|| panic!("fabric inline: no function #{func_idx}"))
1375    });
1376    let depth = INLINE_DEPTH.with(|d| {
1377        let mut d = d.borrow_mut();
1378        *d += 1;
1379        *d
1380    });
1381    assert!(depth < 64, "fabric inline: call graph too deep or recursive — a spatial call STACK is needed, not inlining");
1382
1383    let code = INLINE_CODE.with(|c| c.borrow().clone());
1384    let mut body: Vec<(usize, Op)> = Vec::new();
1385    let mut ret_src: Option<u16> = None;
1386    for (off, op) in code[entry_pc..].iter().enumerate() {
1387        if let Op::Return { src } = op {
1388            ret_src = Some(*src);
1389            break;
1390        }
1391        if matches!(op, Op::ReturnNothing) {
1392            break;
1393        }
1394        body.push((entry_pc + off, *op));
1395    }
1396    let ret_src = ret_src.expect("fabric inline: callee does not Return a value");
1397    let carried = carried_regs(&body);
1398
1399    let mut reg: std::collections::HashMap<u16, Src> = std::collections::HashMap::new();
1400    for p in 0..param_count {
1401        reg.insert(p, arg_srcs[p as usize]); // seed each param with the caller's argument Src
1402    }
1403    let mut preds: std::collections::HashMap<u16, (CmpKind, Src, Src)> =
1404        std::collections::HashMap::new();
1405    dec_convert(&body, &mut reg, cells, &mut preds, &carried, consts);
1406    let result = *reg
1407        .get(&ret_src)
1408        .unwrap_or_else(|| panic!("fabric inline: callee return register R{ret_src} is unwritten"));
1409
1410    INLINE_DEPTH.with(|d| *d.borrow_mut() -= 1);
1411    result
1412}
1413
1414/// INLINE a SPAWNED child's straight-line side effects at the spawn point (the Futamura projection of the
1415/// scheduler). A producer child only ENQUEUES to channels the parent later dequeues, so running its sends at
1416/// the spawn — filling the buffer before the parent reads — yields the SAME observable as the cooperative
1417/// schedule (parent blocks on the empty channel, child runs, parent resumes). The child returns nothing, so
1418/// there is no result; its heap writes (channel enqueues) persist in the shared `cells`. A child that loops
1419/// is refused by `dec_convert` (loops → CGRA), the honest boundary.
1420fn inline_spawn(
1421    func_idx: usize,
1422    arg_srcs: &[Src],
1423    cells: &mut Vec<LoopCell>,
1424    consts: &[crate::vm::Constant],
1425) {
1426    use crate::vm::Op;
1427    let (entry_pc, param_count) = INLINE_FNS.with(|f| {
1428        *f.borrow()
1429            .get(func_idx)
1430            .unwrap_or_else(|| panic!("fabric spawn: no function #{func_idx}"))
1431    });
1432    let depth = INLINE_DEPTH.with(|d| {
1433        let mut d = d.borrow_mut();
1434        *d += 1;
1435        *d
1436    });
1437    assert!(depth < 64, "fabric spawn: call graph too deep or recursive");
1438
1439    let code = INLINE_CODE.with(|c| c.borrow().clone());
1440    let mut body: Vec<(usize, Op)> = Vec::new();
1441    for (off, op) in code[entry_pc..].iter().enumerate() {
1442        if matches!(op, Op::Return { .. } | Op::ReturnNothing) {
1443            break;
1444        }
1445        body.push((entry_pc + off, *op));
1446    }
1447    let carried = carried_regs(&body);
1448    let mut reg: std::collections::HashMap<u16, Src> = std::collections::HashMap::new();
1449    for p in 0..param_count {
1450        reg.insert(p, arg_srcs[p as usize]); // seed each param with the spawner's argument Src
1451    }
1452    let mut preds: std::collections::HashMap<u16, (CmpKind, Src, Src)> =
1453        std::collections::HashMap::new();
1454    dec_convert(&body, &mut reg, cells, &mut preds, &carried, consts);
1455    INLINE_DEPTH.with(|d| *d.borrow_mut() -= 1);
1456}
1457
1458/// INLINE a CLOSURE call (`CallValue`): the same body-inlining as `inline_callee`, but the callee's frame is
1459/// `[params, capture VALUES, capture PRESENT FLAGS]` — args seed the params, and the closure's captured
1460/// `(value, present)` pairs seed the two capture bands (`param_count + k` and `param_count + cap_count + k`),
1461/// exactly as the VM binds them. The body index is STATIC (from the `MakeClosure`), so dispatch is resolved.
1462fn inline_closure(
1463    func_idx: usize,
1464    arg_srcs: &[Src],
1465    captures: &[(Src, bool)],
1466    cells: &mut Vec<LoopCell>,
1467    consts: &[crate::vm::Constant],
1468) -> Src {
1469    use crate::vm::Op;
1470    let (entry_pc, param_count) = INLINE_FNS.with(|f| {
1471        *f.borrow()
1472            .get(func_idx)
1473            .unwrap_or_else(|| panic!("fabric closure: no function #{func_idx}"))
1474    });
1475    let depth = INLINE_DEPTH.with(|d| {
1476        let mut d = d.borrow_mut();
1477        *d += 1;
1478        *d
1479    });
1480    assert!(
1481        depth < 64,
1482        "fabric closure: call graph too deep or recursive"
1483    );
1484
1485    let code = INLINE_CODE.with(|c| c.borrow().clone());
1486    let mut body: Vec<(usize, Op)> = Vec::new();
1487    let mut ret_src: Option<u16> = None;
1488    for (off, op) in code[entry_pc..].iter().enumerate() {
1489        if let Op::Return { src } = op {
1490            ret_src = Some(*src);
1491            break;
1492        }
1493        if matches!(op, Op::ReturnNothing) {
1494            break;
1495        }
1496        body.push((entry_pc + off, *op));
1497    }
1498    let ret_src = ret_src.expect("fabric closure: body does not Return a value");
1499    let carried = carried_regs(&body);
1500
1501    let mut reg: std::collections::HashMap<u16, Src> = std::collections::HashMap::new();
1502    for p in 0..param_count {
1503        reg.insert(p, arg_srcs[p as usize]); // params 0..param_count = the call arguments
1504    }
1505    let cap_count = captures.len() as u16;
1506    for (k, (val, present)) in captures.iter().enumerate() {
1507        let k = k as u16;
1508        reg.insert(param_count + k, *val); // capture value band
1509        reg.insert(param_count + cap_count + k, Src::Const(i64::from(*present)));
1510        // present-flag band
1511    }
1512    let mut preds: std::collections::HashMap<u16, (CmpKind, Src, Src)> =
1513        std::collections::HashMap::new();
1514    dec_convert(&body, &mut reg, cells, &mut preds, &carried, consts);
1515    let result = *reg
1516        .get(&ret_src)
1517        .unwrap_or_else(|| panic!("fabric closure: return register R{ret_src} is unwritten"));
1518    INLINE_DEPTH.with(|d| *d.borrow_mut() -= 1);
1519    result
1520}
1521
1522/// Populate the program-context thread-locals every fabric build reads: the struct field-count and
1523/// (type,field)→slot maps (robust to duplicate string interning), the reset of the branch-invariant
1524/// register-type map, the whole program's code + per-function entry for `Call` inlining, and the
1525/// collection-length + output-tape resets. Shared by `decision_fabric_from_source` and the whole-program
1526/// builder so both see identical struct/enum/call context.
1527fn setup_program_thread_locals(program: &crate::vm::CompiledProgram) {
1528    use std::collections::HashMap;
1529    // Every const index that names a struct type → its field count (a name can be interned at >1 index).
1530    STRUCT_FIELDS.with(|m| {
1531        let mut m = m.borrow_mut();
1532        m.clear();
1533        for (i, c) in program.constants.iter().enumerate() {
1534            if let crate::vm::Constant::Text(name) = c {
1535                if let Some(sd) = program.struct_types.iter().find(|s| &s.name == name) {
1536                    m.insert(i as u32, sd.fields.len());
1537                }
1538            }
1539        }
1540    });
1541    // (type-name const, field-name const) → declaration-order slot; cross every matching const-index pair.
1542    STRUCT_FIELD_POS.with(|m| {
1543        let mut m = m.borrow_mut();
1544        m.clear();
1545        for sd in &program.struct_types {
1546            let name_pos: HashMap<&str, usize> = sd
1547                .fields
1548                .iter()
1549                .enumerate()
1550                .map(|(pos, (fname, _))| (fname.as_str(), pos))
1551                .collect();
1552            for (t, ct) in program.constants.iter().enumerate() {
1553                if !matches!(ct, crate::vm::Constant::Text(n) if *n == sd.name) {
1554                    continue;
1555                }
1556                for (f, cf) in program.constants.iter().enumerate() {
1557                    if let crate::vm::Constant::Text(fname) = cf {
1558                        if let Some(&pos) = name_pos.get(fname.as_str()) {
1559                            m.insert((t as u32, f as u32), pos);
1560                        }
1561                    }
1562                }
1563            }
1564        }
1565        if std::env::var("FABRIC_DUMP").is_ok() {
1566            let st: Vec<(String, Vec<String>)> = program
1567                .struct_types
1568                .iter()
1569                .map(|s| {
1570                    (
1571                        s.name.clone(),
1572                        s.fields.iter().map(|(f, _)| f.clone()).collect(),
1573                    )
1574                })
1575                .collect();
1576            let tc: Vec<(usize, String)> = program
1577                .constants
1578                .iter()
1579                .enumerate()
1580                .filter_map(|(i, c)| match c {
1581                    crate::vm::Constant::Text(t) => Some((i, t.clone())),
1582                    _ => None,
1583                })
1584                .collect();
1585            eprintln!("STRUCTDUMP struct_types={st:?}");
1586            eprintln!("STRUCTDUMP text_consts={tc:?}");
1587            eprintln!("STRUCTDUMP field_pos={:?}", m.iter().collect::<Vec<_>>());
1588        }
1589    });
1590    REG_TYPE.with(|m| m.borrow_mut().clear());
1591    INLINE_CODE.with(|c| *c.borrow_mut() = program.code.clone());
1592    INLINE_FNS.with(|m| {
1593        *m.borrow_mut() = program
1594            .functions
1595            .iter()
1596            .map(|fun| (fun.entry_pc, fun.param_count))
1597            .collect();
1598    });
1599    CLOSURE_CAPTURES.with(|m| {
1600        *m.borrow_mut() = program
1601            .functions
1602            .iter()
1603            .map(|fun| fun.captures.iter().map(|(_, gidx)| *gidx).collect())
1604            .collect();
1605    });
1606    CLOSURES.with(|c| c.borrow_mut().clear());
1607    INLINE_DEPTH.with(|d| *d.borrow_mut() = 0);
1608    COLL_LEN.with(|m| m.borrow_mut().clear());
1609    SHOW_OUTPUTS.with(|o| o.borrow_mut().clear());
1610    TEXT_REGS.with(|t| t.borrow_mut().clear());
1611    NAME_REGS.with(|t| t.borrow_mut().clear());
1612    FLOAT_REGS.with(|t| t.borrow_mut().clear());
1613    W32_REGS.with(|t| t.borrow_mut().clear());
1614    BIGINT_BIG.with(|m| m.borrow_mut().clear());
1615    GLOBALS.with(|g| g.borrow_mut().clear());
1616    NET_INBOX.with(|n| *n.borrow_mut() = None);
1617    SELECT_ARMS.with(|a| a.borrow_mut().clear());
1618    HALTED.with(|h| h.set(false));
1619    // Policy conditions are re-populated per program by `whole_program_fabric_from_program_with_policies`
1620    // (empty for hand-authored bytecode, which carries no `PolicyRegistry`); the grant guard starts open.
1621    POLICY_CONDITIONS.with(|m| m.borrow_mut().clear());
1622    POLICY_GUARD.with(|g| *g.borrow_mut() = None);
1623    REG_TIMEKIND.with(|m| m.borrow_mut().clear());
1624}
1625
1626/// Logical (zero-filling) right shift by a COMPILE-TIME-CONSTANT amount `s` (0..=63), realized with the
1627/// arithmetic `Shr` tile plus an `And` that clears the sign-extended top `s` bits (the mask is baked at
1628/// build time). `s == 0` is the identity. This is the `u64 >>` the magic-reciprocal division (`MagicDivU`)
1629/// needs — the fabric's `AluOp::Shr` is arithmetic, so no separate logical-shift ALU op is required.
1630fn logical_shr_const(cells: &mut Vec<LoopCell>, src: Src, s: i64) -> Src {
1631    if s == 0 {
1632        return src;
1633    }
1634    let kshr = cells.len();
1635    cells.push(LoopCell::Arith {
1636        op: AluOp::Shr,
1637        a: src,
1638        b: Src::Const(s),
1639    });
1640    let mask = (((1u128 << (64 - s)) - 1) as u64) as i64; // keep the low (64 - s) bits, zero the rest
1641    let kand = cells.len();
1642    cells.push(LoopCell::Arith {
1643        op: AluOp::And,
1644        a: Src::Val(kshr),
1645        b: Src::Const(mask),
1646    });
1647    Src::Val(kand)
1648}
1649
1650/// Push one `Arith` tile and return its SSA `Src` — a terse builder for the calendar tile chains below.
1651fn t_arith(cells: &mut Vec<LoopCell>, op: AluOp, a: Src, b: Src) -> Src {
1652    let k = cells.len();
1653    cells.push(LoopCell::Arith { op, a, b });
1654    Src::Val(k)
1655}
1656/// Push one `Func` tile and return its SSA `Src`.
1657fn t_func(cells: &mut Vec<LoopCell>, unit: FuncUnit, a: Src, b: Src) -> Src {
1658    let k = cells.len();
1659    cells.push(LoopCell::Func { unit, a, b });
1660    Src::Val(k)
1661}
1662/// Push one `Select` tile (`cmp(ca,cb) ? t : e`) and return its SSA `Src`.
1663fn t_select(cells: &mut Vec<LoopCell>, cmp: CmpKind, ca: Src, cb: Src, t: Src, e: Src) -> Src {
1664    let k = cells.len();
1665    cells.push(LoopCell::Select { cmp, ca, cb, t, e });
1666    Src::Val(k)
1667}
1668
1669/// The GREGORIAN `civil_from_days(z)` (Howard Hinnant) as a combinational tile chain — `days` since the
1670/// epoch → `(year, month, day)`. Every `/` is TRUNCATING signed division (`DivS`), which the algorithm's
1671/// `z >= 0 ? z : z - 146096` era-adjust (a `Select`) makes exact for negative day numbers too. Bit-identical
1672/// to [`logicaffeine_base::temporal::civil_from_days`]. See that function for the derivation.
1673fn civil_from_days_tiles(cells: &mut Vec<LoopCell>, days: Src) -> (Src, Src, Src) {
1674    let c = Src::Const;
1675    let z = t_arith(cells, AluOp::Add, days, c(719468));
1676    let z_m = t_arith(cells, AluOp::Sub, z, c(146096));
1677    let era_num = t_select(cells, CmpKind::Ge, z, c(0), z, z_m); // z >= 0 ? z : z - 146096
1678    let era = t_func(cells, FuncUnit::DivS, era_num, c(146097));
1679    let era146097 = t_arith(cells, AluOp::Mul, era, c(146097));
1680    let doe = t_arith(cells, AluOp::Sub, z, era146097); // [0, 146096]
1681    let doe1460 = t_func(cells, FuncUnit::DivS, doe, c(1460));
1682    let doe36524 = t_func(cells, FuncUnit::DivS, doe, c(36524));
1683    let doe146096 = t_func(cells, FuncUnit::DivS, doe, c(146096));
1684    let yn1 = t_arith(cells, AluOp::Sub, doe, doe1460);
1685    let yn2 = t_arith(cells, AluOp::Add, yn1, doe36524);
1686    let yn3 = t_arith(cells, AluOp::Sub, yn2, doe146096);
1687    let yoe = t_func(cells, FuncUnit::DivS, yn3, c(365)); // [0, 399]
1688    let era400 = t_arith(cells, AluOp::Mul, era, c(400));
1689    let y = t_arith(cells, AluOp::Add, yoe, era400);
1690    let yoe4 = t_func(cells, FuncUnit::DivS, yoe, c(4));
1691    let yoe100 = t_func(cells, FuncUnit::DivS, yoe, c(100));
1692    let t365 = t_arith(cells, AluOp::Mul, yoe, c(365));
1693    let t2 = t_arith(cells, AluOp::Add, t365, yoe4);
1694    let t3 = t_arith(cells, AluOp::Sub, t2, yoe100);
1695    let doy = t_arith(cells, AluOp::Sub, doe, t3); // [0, 365]
1696    let mpn1 = t_arith(cells, AluOp::Mul, doy, c(5));
1697    let mpn2 = t_arith(cells, AluOp::Add, mpn1, c(2));
1698    let mp = t_func(cells, FuncUnit::DivS, mpn2, c(153)); // [0, 11]
1699    let d153 = t_arith(cells, AluOp::Mul, mp, c(153));
1700    let d1532 = t_arith(cells, AluOp::Add, d153, c(2));
1701    let d5 = t_func(cells, FuncUnit::DivS, d1532, c(5));
1702    let dd = t_arith(cells, AluOp::Sub, doy, d5);
1703    let day = t_arith(cells, AluOp::Add, dd, c(1)); // [1, 31]
1704    let mp3 = t_arith(cells, AluOp::Add, mp, c(3));
1705    let mp9 = t_arith(cells, AluOp::Sub, mp, c(9));
1706    let month = t_select(cells, CmpKind::Lt, mp, c(10), mp3, mp9); // mp < 10 ? mp + 3 : mp - 9
1707    let y1 = t_arith(cells, AluOp::Add, y, c(1));
1708    let year = t_select(cells, CmpKind::Le, month, c(2), y1, y); // month <= 2 ? y + 1 : y
1709    (year, month, day)
1710}
1711
1712/// The inverse `days_from_civil(year, month, day)` (Howard Hinnant) as a combinational tile chain —
1713/// bit-identical to [`logicaffeine_base::temporal::days_from_civil`]. Used by the ISO-week anchor.
1714fn days_from_civil_tiles(cells: &mut Vec<LoopCell>, year: Src, month: Src, day: Src) -> Src {
1715    let c = Src::Const;
1716    let ym1 = t_arith(cells, AluOp::Sub, year, c(1));
1717    let y = t_select(cells, CmpKind::Le, month, c(2), ym1, year); // month <= 2 ? year - 1 : year
1718    let ym399 = t_arith(cells, AluOp::Sub, y, c(399));
1719    let era_num = t_select(cells, CmpKind::Ge, y, c(0), y, ym399); // y >= 0 ? y : y - 399
1720    let era = t_func(cells, FuncUnit::DivS, era_num, c(400));
1721    let era400 = t_arith(cells, AluOp::Mul, era, c(400));
1722    let yoe = t_arith(cells, AluOp::Sub, y, era400); // [0, 399]
1723    let mm3 = t_arith(cells, AluOp::Sub, month, c(3));
1724    let mp9 = t_arith(cells, AluOp::Add, month, c(9));
1725    let mp = t_select(cells, CmpKind::Gt, month, c(2), mm3, mp9); // m > 2 ? m - 3 : m + 9
1726    let d153 = t_arith(cells, AluOp::Mul, mp, c(153));
1727    let d1532 = t_arith(cells, AluOp::Add, d153, c(2));
1728    let d5 = t_func(cells, FuncUnit::DivS, d1532, c(5));
1729    let dd = t_arith(cells, AluOp::Add, d5, day);
1730    let doy = t_arith(cells, AluOp::Sub, dd, c(1)); // [0, 365]
1731    let yoe365 = t_arith(cells, AluOp::Mul, yoe, c(365));
1732    let yoe4 = t_func(cells, FuncUnit::DivS, yoe, c(4));
1733    let yoe100 = t_func(cells, FuncUnit::DivS, yoe, c(100));
1734    let doe1 = t_arith(cells, AluOp::Add, yoe365, yoe4);
1735    let doe2 = t_arith(cells, AluOp::Sub, doe1, yoe100);
1736    let doe = t_arith(cells, AluOp::Add, doe2, doy); // [0, 146096]
1737    let e146097 = t_arith(cells, AluOp::Mul, era, c(146097));
1738    let d1 = t_arith(cells, AluOp::Add, e146097, doe);
1739    t_arith(cells, AluOp::Sub, d1, c(719468))
1740}
1741
1742/// The ISO-8601 `iso_weekday_from_days(z)` as tiles: `(z + 3) mod 7 + 1` in `1..=7` (Mon..Sun). `ModS` (the
1743/// dividend-signed remainder) equals `rem_euclid` for `z >= -3` — every real day number since 1970 — matching
1744/// the existing `weekday`/`hour` lowering's `ModS`-for-`rem_euclid` convention.
1745fn iso_weekday_from_days_tiles(cells: &mut Vec<LoopCell>, z: Src) -> Src {
1746    let z3 = t_arith(cells, AluOp::Add, z, Src::Const(3));
1747    let m = t_func(cells, FuncUnit::ModS, z3, Src::Const(7));
1748    t_arith(cells, AluOp::Add, m, Src::Const(1))
1749}
1750
1751/// Every per-register fact the lowering tracks — Text, float, float-collection, static length, wide
1752/// bignum, struct type, time kind, closure — dropped for `r`.
1753fn forget_reg(r: u16) {
1754    TEXT_REGS.with(|t| t.borrow_mut().remove(&r));
1755    NAME_REGS.with(|t| t.borrow_mut().remove(&r));
1756    FLOAT_REGS.with(|t| t.borrow_mut().remove(&r));
1757    FLOAT_COLLS.with(|t| t.borrow_mut().remove(&r));
1758    COLL_LEN.with(|t| t.borrow_mut().remove(&r));
1759    BIGINT_BIG.with(|t| t.borrow_mut().remove(&r));
1760    REG_TYPE.with(|t| t.borrow_mut().remove(&r));
1761    REG_TIMEKIND.with(|t| t.borrow_mut().remove(&r));
1762    CLOSURES.with(|t| t.borrow_mut().remove(&r));
1763}
1764
1765/// A register an op WRITES without READING holds nothing it held before: its facts are dropped before
1766/// the op's arm runs, so the arm derives it from the op alone — a temporary the VM reuses for an Int after
1767/// a Text no longer reads as Text. A register both read and written (`x = x + 1`) keeps its facts for the
1768/// arm to read. `captures` is a closure's capture count (`Op::for_each_read_reg`).
1769fn forget_overwritten(op: &crate::vm::Op, captures: impl Fn(u16) -> usize) {
1770    let mut read = Vec::new();
1771    op.for_each_read_reg(captures, |r| read.push(r));
1772    op.for_each_written_reg(|r| {
1773        if !read.contains(&r) {
1774            forget_reg(r);
1775        }
1776    });
1777}
1778
1779/// A Text literal on the heap: the char-list `[len, byte0, byte1, …]` (UTF-8 bytes, so `length of` is the
1780/// byte count, matching the VM), with `extra` words of growth capacity past it. Returns its base pointer.
1781fn heap_text_literal(cells: &mut Vec<LoopCell>, bytes: &[u8], extra: i64) -> Src {
1782    let n = bytes.len();
1783    let ka = cells.len();
1784    cells.push(LoopCell::HeapAlloc {
1785        size: Src::Const(n as i64 + 1 + extra),
1786    });
1787    let ptr = Src::Val(ka);
1788    cells.push(LoopCell::HeapWrite {
1789        addr: ptr,
1790        val: Src::Const(n as i64),
1791    });
1792    for (i, b) in bytes.iter().enumerate() {
1793        let kadd = cells.len();
1794        cells.push(LoopCell::Arith {
1795            op: AluOp::Add,
1796            a: ptr,
1797            b: Src::Const(i as i64 + 1),
1798        });
1799        cells.push(LoopCell::HeapWrite {
1800            addr: Src::Val(kadd),
1801            val: Src::Const(*b as i64),
1802        });
1803    }
1804    ptr
1805}
1806
1807/// `args()` on a circuit: argv, program name first, as on every tier (`semantics::argv_with_program_name`). A
1808/// circuit has no host argv, so its argument list is the program name alone — `[1, name]`, the name a heap
1809/// Text — with `extra` words of growth capacity. Returns the list's base pointer.
1810fn heap_argv(cells: &mut Vec<LoopCell>, extra: i64) -> Src {
1811    let name = heap_text_literal(cells, crate::semantics::PROGRAM_NAME.as_bytes(), 0);
1812    let ka = cells.len();
1813    cells.push(LoopCell::HeapAlloc {
1814        size: Src::Const(2 + extra),
1815    });
1816    let ptr = Src::Val(ka);
1817    cells.push(LoopCell::HeapWrite {
1818        addr: ptr,
1819        val: Src::Const(1),
1820    });
1821    let kadd = cells.len();
1822    cells.push(LoopCell::Arith {
1823        op: AluOp::Add,
1824        a: ptr,
1825        b: Src::Const(1),
1826    });
1827    cells.push(LoopCell::HeapWrite {
1828        addr: Src::Val(kadd),
1829        val: name,
1830    });
1831    ptr
1832}
1833
1834/// Straight-line (jump-free) execution of an op run into the SSA register map + cell list, shared
1835/// by the counted-loop and `while`-loop body extractors. Comparisons are recorded as pending
1836/// predicates (folded into a `Select` at a branch merge).
1837fn exec_ops(
1838    ops: &[crate::vm::Op],
1839    reg: &mut std::collections::HashMap<u16, Src>,
1840    cells: &mut Vec<LoopCell>,
1841    preds: &mut std::collections::HashMap<u16, (CmpKind, Src, Src)>,
1842    consts: &[crate::vm::Constant],
1843) {
1844    use crate::vm::{Constant, Op};
1845    let getb = |reg: &std::collections::HashMap<u16, Src>, r: u16| -> Src {
1846        *reg.get(&r)
1847            .unwrap_or_else(|| panic!("loop body reads unwritten register R{r}"))
1848    };
1849    for op in ops {
1850        forget_overwritten(op, |_| usize::MAX);
1851        let cmp = match op {
1852            Op::Lt { .. } => Some(CmpKind::Lt),
1853            Op::Gt { .. } => Some(CmpKind::Gt),
1854            Op::LtEq { .. } => Some(CmpKind::Le),
1855            Op::GtEq { .. } => Some(CmpKind::Ge),
1856            Op::Eq { .. } => Some(CmpKind::Eq),
1857            Op::NotEq { .. } => Some(CmpKind::Ne),
1858            _ => None,
1859        };
1860        if let Some(kind) = cmp {
1861            if let Op::Lt { dst, lhs, rhs }
1862            | Op::Gt { dst, lhs, rhs }
1863            | Op::LtEq { dst, lhs, rhs }
1864            | Op::GtEq { dst, lhs, rhs }
1865            | Op::Eq { dst, lhs, rhs }
1866            | Op::NotEq { dst, lhs, rhs } = op
1867            {
1868                preds.insert(*dst, (kind, getb(reg, *lhs), getb(reg, *rhs)));
1869            }
1870            continue;
1871        }
1872        match op {
1873            Op::LoadConst { dst, idx: ci } => match &consts[*ci as usize] {
1874                Constant::Int(n) => {
1875                    reg.insert(*dst, Src::Const(*n));
1876                }
1877                Constant::Bool(b) => {
1878                    reg.insert(*dst, Src::Const(i64::from(*b))); // Bool rides a scalar slot as 0/1
1879                }
1880                Constant::Text(s) => {
1881                    // A Text literal → its heap char-list, marked TEXT so `Show` renders it as a string, and
1882                    // remembered as a NAME for a place mutation that keys a struct field by it.
1883                    let ptr = heap_text_literal(cells, s.as_bytes(), 0);
1884                    reg.insert(*dst, ptr);
1885                    TEXT_REGS.with(|t| {
1886                        t.borrow_mut().insert(*dst);
1887                    });
1888                    NAME_REGS.with(|t| {
1889                        t.borrow_mut().insert(*dst, *ci);
1890                    });
1891                    COLL_LEN.with(|m| {
1892                        m.borrow_mut().insert(*dst, s.len() as i64);
1893                    });
1894                }
1895                // A `Duration` rides a scalar slot as its nanosecond count — used only as a `select`
1896                // timeout's `ticks` (which the fabric's priority resolver ignores: a ready recv always wins).
1897                Constant::Duration(n) => {
1898                    reg.insert(*dst, Src::Const(*n));
1899                }
1900                // A FLOAT rides a scalar slot as its `f64` BIT PATTERN; the reg is marked so arithmetic routes
1901                // through the float functional units and `Show` refuses (float→string is a follow-on).
1902                Constant::Float(f) => {
1903                    reg.insert(*dst, Src::Const(f.to_bits() as i64));
1904                    FLOAT_REGS.with(|t| {
1905                        t.borrow_mut().insert(*dst);
1906                    });
1907                }
1908                // `Nothing` rides a scalar slot as 0, the word a cleared pointer field holds — a place mutation
1909                // clears the parent's field to it before mutating the leaf and writing the leaf back.
1910                Constant::Nothing => {
1911                    reg.insert(*dst, Src::Const(0));
1912                }
1913                c => panic!("loop body: unsupported constant {c:?}"),
1914            },
1915            // `now` / `today` — read the clock. A combinational fabric has NO wall clock, so a deterministic
1916            // differential requires an INJECTED fixed clock (`temporal::set_fixed_clock`, which the VM reads
1917            // too); the fabric reads the same instant and bakes it as a Const (a value-load, like LoadConst —
1918            // hence a Wire tile). `LoadNow` yields a Moment (nanos), `LoadToday` a Date (whole days); the kind
1919            // is tracked so a date-component extractor decodes it correctly. Absent an injected clock the
1920            // fabric REFUSES rather than fabricating a time — an honest boundary (tests inject the clock).
1921            Op::LoadNow { dst } => {
1922                let (_, nanos) = crate::semantics::temporal::fixed_clock().expect(
1923                    "fabric LoadNow: the combinational fabric cannot read the wall clock — inject a fixed \
1924                     clock (temporal::set_fixed_clock) so the VM and the fabric read the same instant",
1925                );
1926                reg.insert(*dst, Src::Const(nanos));
1927                REG_TIMEKIND.with(|m| m.borrow_mut().insert(*dst, TimeKind::Moment));
1928            }
1929            Op::LoadToday { dst } => {
1930                let (days, _) = crate::semantics::temporal::fixed_clock().expect(
1931                    "fabric LoadToday: the combinational fabric cannot read the wall clock — inject a fixed \
1932                     clock (temporal::set_fixed_clock) so the VM and the fabric read the same instant",
1933                );
1934                reg.insert(*dst, Src::Const(i64::from(days)));
1935                REG_TIMEKIND.with(|m| m.borrow_mut().insert(*dst, TimeKind::Date));
1936            }
1937            // `new counter` / `new growing set` / … — create a fresh CRDT value. The replica id the VM draws
1938            // (`next_replica_id`) is captured nondeterminism that does NOT affect the SINGLE-replica
1939            // observable (it only breaks merge ties), so the fabric models the deterministic INITIAL state.
1940            // A fresh counter / PN-counter is 0 (renders as a plain Int, matching the VM's `Show`); a set /
1941            // seq / register starts empty but renders as a collection (`{}` / `[]`), which needs collection
1942            // rendering on the output tape — a follow-on, so those kinds are refused loudly.
1943            Op::NewCrdt { dst, kind } => match kind {
1944                // a counter / PN-counter (kind 4/5) is a single Int, initially 0.
1945                4 | 5 => {
1946                    reg.insert(*dst, Src::Const(0));
1947                }
1948                // a set / sequence / register CRDT (kind 0/1/2/3) is a live COLLECTION: its single-replica
1949                // state is an empty list `[0]` on the heap (grown in place by `CrdtAppend`/`SetAdd`), exactly
1950                // like `NewEmptyList` — the fabric models the deterministic converged value.
1951                _ => {
1952                    let ka = cells.len();
1953                    cells.push(LoopCell::HeapAlloc {
1954                        size: Src::Const(1),
1955                    });
1956                    let ptr = Src::Val(ka);
1957                    cells.push(LoopCell::HeapWrite {
1958                        addr: ptr,
1959                        val: Src::Const(0),
1960                    });
1961                    reg.insert(*dst, ptr);
1962                }
1963            },
1964            Op::Move { dst, src } => {
1965                let s = getb(reg, *src);
1966                reg.insert(*dst, s);
1967                NAME_REGS.with(|m| {
1968                    let mut m = m.borrow_mut();
1969                    if let Some(n) = m.get(src).copied() {
1970                        m.insert(*dst, n);
1971                    }
1972                });
1973                // a struct type / a collection's static length ride along the move
1974                REG_TYPE.with(|m| {
1975                    let mut m = m.borrow_mut();
1976                    if let Some(t) = m.get(src).copied() {
1977                        m.insert(*dst, t);
1978                    }
1979                });
1980                COLL_LEN.with(|m| {
1981                    let mut m = m.borrow_mut();
1982                    if let Some(l) = m.get(src).copied() {
1983                        m.insert(*dst, l);
1984                    }
1985                });
1986                TEXT_REGS.with(|t| {
1987                    let mut t = t.borrow_mut();
1988                    if t.contains(src) {
1989                        t.insert(*dst);
1990                    }
1991                });
1992                FLOAT_REGS.with(|t| {
1993                    let mut t = t.borrow_mut();
1994                    if t.contains(src) {
1995                        t.insert(*dst);
1996                    }
1997                });
1998                // WORD32-ness rides along a move, so a `+` on the copy still wraps mod 2³² (SHA-256 keeps
1999                // word32 values in temporaries between the mix and the round add).
2000                W32_REGS.with(|t| {
2001                    let mut t = t.borrow_mut();
2002                    if t.contains(src) {
2003                        t.insert(*dst);
2004                    } else {
2005                        t.remove(dst);
2006                    }
2007                });
2008                REG_TIMEKIND.with(|m| {
2009                    let mut m = m.borrow_mut();
2010                    if let Some(k) = m.get(src).copied() {
2011                        m.insert(*dst, k);
2012                    }
2013                });
2014            }
2015            // `Swap a and b` — the zero-garbage reversible register exchange: swap the two Srcs (pure
2016            // rewiring, no cell) and any collection/text/struct tracking, so the names fully exchange identity.
2017            Op::RevSwap { a, b } => {
2018                let sa = getb(reg, *a);
2019                let sb = getb(reg, *b);
2020                reg.insert(*a, sb);
2021                reg.insert(*b, sa);
2022                COLL_LEN.with(|m| {
2023                    let mut m = m.borrow_mut();
2024                    let (va, vb) = (m.remove(a), m.remove(b));
2025                    if let Some(v) = vb {
2026                        m.insert(*a, v);
2027                    }
2028                    if let Some(v) = va {
2029                        m.insert(*b, v);
2030                    }
2031                });
2032                REG_TYPE.with(|m| {
2033                    let mut m = m.borrow_mut();
2034                    let (va, vb) = (m.remove(a), m.remove(b));
2035                    if let Some(v) = vb {
2036                        m.insert(*a, v);
2037                    }
2038                    if let Some(v) = va {
2039                        m.insert(*b, v);
2040                    }
2041                });
2042                TEXT_REGS.with(|t| {
2043                    let mut t = t.borrow_mut();
2044                    let (ha, hb) = (t.remove(a), t.remove(b));
2045                    if hb {
2046                        t.insert(*a);
2047                    }
2048                    if ha {
2049                        t.insert(*b);
2050                    }
2051                });
2052            }
2053            // Reversible ALU (the `rev_alu_core` ISA — no source emits them, hand-bytecode witnessed): each
2054            // updates `place` IN PLACE with a combinational cell, exactly like its irreversible twin. RevXor
2055            // `p ^= d`, RevAdd `p += d`, RevSub `p -= d`, RevNeg `p = -p` (0 - p). Zero-garbage on silicon;
2056            // on the combinational fabric they are the same XOR/ADD/SUB tiles writing a fresh SSA value.
2057            Op::RevXor { place, delta } => {
2058                let k = cells.len();
2059                cells.push(LoopCell::Arith {
2060                    op: AluOp::Xor,
2061                    a: getb(reg, *place),
2062                    b: getb(reg, *delta),
2063                });
2064                reg.insert(*place, Src::Val(k));
2065            }
2066            Op::RevAdd { place, delta } => {
2067                let k = cells.len();
2068                cells.push(LoopCell::Arith {
2069                    op: AluOp::Add,
2070                    a: getb(reg, *place),
2071                    b: getb(reg, *delta),
2072                });
2073                reg.insert(*place, Src::Val(k));
2074            }
2075            Op::RevSub { place, delta } => {
2076                let k = cells.len();
2077                cells.push(LoopCell::Arith {
2078                    op: AluOp::Sub,
2079                    a: getb(reg, *place),
2080                    b: getb(reg, *delta),
2081                });
2082                reg.insert(*place, Src::Val(k));
2083            }
2084            Op::RevNeg { place } => {
2085                let k = cells.len();
2086                cells.push(LoopCell::Arith {
2087                    op: AluOp::Sub,
2088                    a: Src::Const(0),
2089                    b: getb(reg, *place),
2090                });
2091                reg.insert(*place, Src::Val(k));
2092            }
2093            Op::Add { dst, lhs, rhs } => {
2094                let float =
2095                    FLOAT_REGS.with(|t| t.borrow().contains(lhs) || t.borrow().contains(rhs));
2096                let w32 = W32_REGS.with(|t| t.borrow().contains(lhs) || t.borrow().contains(rhs));
2097                let (a, b) = (getb(reg, *lhs), getb(reg, *rhs));
2098                let k = cells.len();
2099                if float {
2100                    cells.push(LoopCell::Func {
2101                        unit: FuncUnit::FAddF,
2102                        a,
2103                        b,
2104                    });
2105                    FLOAT_REGS.with(|t| {
2106                        t.borrow_mut().insert(*dst);
2107                    });
2108                    W32_REGS.with(|t| {
2109                        t.borrow_mut().remove(dst);
2110                    });
2111                    reg.insert(*dst, Src::Val(k));
2112                } else if w32 {
2113                    // WORD32 wrapping add: the integer sum masked back to 32 bits (mod 2³²) — SHA-256 rounds.
2114                    cells.push(LoopCell::Arith {
2115                        op: AluOp::Add,
2116                        a,
2117                        b,
2118                    });
2119                    let km = cells.len();
2120                    cells.push(LoopCell::Arith {
2121                        op: AluOp::And,
2122                        a: Src::Val(k),
2123                        b: Src::Const(0xFFFF_FFFF),
2124                    });
2125                    reg.insert(*dst, Src::Val(km));
2126                    W32_REGS.with(|t| {
2127                        t.borrow_mut().insert(*dst);
2128                    });
2129                } else {
2130                    cells.push(LoopCell::Arith {
2131                        op: AluOp::Add,
2132                        a,
2133                        b,
2134                    });
2135                    reg.insert(*dst, Src::Val(k));
2136                    W32_REGS.with(|t| {
2137                        t.borrow_mut().remove(dst);
2138                    });
2139                    FLOAT_REGS.with(|t| {
2140                        t.borrow_mut().remove(dst);
2141                    });
2142                }
2143            }
2144            // Compound accumulate `Set x to x + y` → `x += y`. A FLOAT accumulator routes to the `FAddF`
2145            // functional unit (mirroring `Op::Add`) so a float loop's `Set s to s + …` is a real float add,
2146            // not an integer add of two `f64` bit patterns. (The VM's Text fast path is a string append the
2147            // fabric heap plane doesn't model — scalar accumulate only.)
2148            Op::AddAssign { dst, src } => {
2149                let float =
2150                    FLOAT_REGS.with(|t| t.borrow().contains(dst) || t.borrow().contains(src));
2151                let (a, b) = (getb(reg, *dst), getb(reg, *src));
2152                let k = cells.len();
2153                if float {
2154                    cells.push(LoopCell::Func {
2155                        unit: FuncUnit::FAddF,
2156                        a,
2157                        b,
2158                    });
2159                    FLOAT_REGS.with(|t| {
2160                        t.borrow_mut().insert(*dst);
2161                    });
2162                } else {
2163                    cells.push(LoopCell::Arith {
2164                        op: AluOp::Add,
2165                        a,
2166                        b,
2167                    });
2168                }
2169                reg.insert(*dst, Src::Val(k));
2170            }
2171            // Logical not: `!is_truthy(src)` = `(src == 0) ? 1 : 0` (Bool as 0/1), byte-exact with the VM's
2172            // `not_value`. A Select mux realises it as a combinational value (usable in later arithmetic or,
2173            // via the value-condition path, as a branch predicate).
2174            Op::Not { dst, src } => {
2175                let k = cells.len();
2176                cells.push(LoopCell::Select {
2177                    cmp: CmpKind::Eq,
2178                    ca: getb(reg, *src),
2179                    cb: Src::Const(0),
2180                    t: Src::Const(1),
2181                    e: Src::Const(0),
2182                });
2183                reg.insert(*dst, Src::Val(k));
2184            }
2185            Op::Sub { dst, lhs, rhs } => {
2186                let float =
2187                    FLOAT_REGS.with(|t| t.borrow().contains(lhs) || t.borrow().contains(rhs));
2188                let w32 = W32_REGS.with(|t| t.borrow().contains(lhs) || t.borrow().contains(rhs));
2189                let (a, b) = (getb(reg, *lhs), getb(reg, *rhs));
2190                let k = cells.len();
2191                if float {
2192                    cells.push(LoopCell::Func {
2193                        unit: FuncUnit::FSubF,
2194                        a,
2195                        b,
2196                    });
2197                    FLOAT_REGS.with(|t| {
2198                        t.borrow_mut().insert(*dst);
2199                    });
2200                    W32_REGS.with(|t| {
2201                        t.borrow_mut().remove(dst);
2202                    });
2203                    reg.insert(*dst, Src::Val(k));
2204                } else if w32 {
2205                    cells.push(LoopCell::Arith {
2206                        op: AluOp::Sub,
2207                        a,
2208                        b,
2209                    });
2210                    let km = cells.len();
2211                    cells.push(LoopCell::Arith {
2212                        op: AluOp::And,
2213                        a: Src::Val(k),
2214                        b: Src::Const(0xFFFF_FFFF),
2215                    });
2216                    reg.insert(*dst, Src::Val(km));
2217                    W32_REGS.with(|t| {
2218                        t.borrow_mut().insert(*dst);
2219                    });
2220                } else {
2221                    cells.push(LoopCell::Arith {
2222                        op: AluOp::Sub,
2223                        a,
2224                        b,
2225                    });
2226                    reg.insert(*dst, Src::Val(k));
2227                    W32_REGS.with(|t| {
2228                        t.borrow_mut().remove(dst);
2229                    });
2230                    FLOAT_REGS.with(|t| {
2231                        t.borrow_mut().remove(dst);
2232                    });
2233                }
2234            }
2235            Op::Mul { dst, lhs, rhs } => {
2236                let float =
2237                    FLOAT_REGS.with(|t| t.borrow().contains(lhs) || t.borrow().contains(rhs));
2238                let w32 = W32_REGS.with(|t| t.borrow().contains(lhs) || t.borrow().contains(rhs));
2239                let (a, b) = (getb(reg, *lhs), getb(reg, *rhs));
2240                let k = cells.len();
2241                if float {
2242                    cells.push(LoopCell::Func {
2243                        unit: FuncUnit::FMulF,
2244                        a,
2245                        b,
2246                    });
2247                    FLOAT_REGS.with(|t| {
2248                        t.borrow_mut().insert(*dst);
2249                    });
2250                    W32_REGS.with(|t| {
2251                        t.borrow_mut().remove(dst);
2252                    });
2253                    reg.insert(*dst, Src::Val(k));
2254                } else if w32 {
2255                    cells.push(LoopCell::Arith {
2256                        op: AluOp::Mul,
2257                        a,
2258                        b,
2259                    });
2260                    let km = cells.len();
2261                    cells.push(LoopCell::Arith {
2262                        op: AluOp::And,
2263                        a: Src::Val(k),
2264                        b: Src::Const(0xFFFF_FFFF),
2265                    });
2266                    reg.insert(*dst, Src::Val(km));
2267                    W32_REGS.with(|t| {
2268                        t.borrow_mut().insert(*dst);
2269                    });
2270                } else {
2271                    cells.push(LoopCell::Arith {
2272                        op: AluOp::Mul,
2273                        a,
2274                        b,
2275                    });
2276                    reg.insert(*dst, Src::Val(k));
2277                    W32_REGS.with(|t| {
2278                        t.borrow_mut().remove(dst);
2279                    });
2280                    FLOAT_REGS.with(|t| {
2281                        t.borrow_mut().remove(dst);
2282                    });
2283                }
2284            }
2285            // Bitwise / shift — combinational ALU tiles, bit-identical to the VM's `Int` ops.
2286            Op::BitAnd { dst, lhs, rhs } => {
2287                let k = cells.len();
2288                cells.push(LoopCell::Arith {
2289                    op: AluOp::And,
2290                    a: getb(reg, *lhs),
2291                    b: getb(reg, *rhs),
2292                });
2293                reg.insert(*dst, Src::Val(k));
2294            }
2295            Op::BitOr { dst, lhs, rhs } => {
2296                let k = cells.len();
2297                cells.push(LoopCell::Arith {
2298                    op: AluOp::Or,
2299                    a: getb(reg, *lhs),
2300                    b: getb(reg, *rhs),
2301                });
2302                reg.insert(*dst, Src::Val(k));
2303            }
2304            Op::BitXor { dst, lhs, rhs } => {
2305                let k = cells.len();
2306                cells.push(LoopCell::Arith {
2307                    op: AluOp::Xor,
2308                    a: getb(reg, *lhs),
2309                    b: getb(reg, *rhs),
2310                });
2311                reg.insert(*dst, Src::Val(k));
2312            }
2313            Op::Shl { dst, lhs, rhs } => {
2314                let k = cells.len();
2315                cells.push(LoopCell::Arith {
2316                    op: AluOp::Shl,
2317                    a: getb(reg, *lhs),
2318                    b: getb(reg, *rhs),
2319                });
2320                reg.insert(*dst, Src::Val(k));
2321            }
2322            Op::Shr { dst, lhs, rhs } => {
2323                let k = cells.len();
2324                cells.push(LoopCell::Arith {
2325                    op: AluOp::Shr,
2326                    a: getb(reg, *lhs),
2327                    b: getb(reg, *rhs),
2328                });
2329                reg.insert(*dst, Src::Val(k));
2330            }
2331            // Division / remainder — a SEQUENTIAL functional-unit tile (the `shared_divider`), not a
2332            // combinational ALU op. `Op::Div` is the truncated signed quotient (`/`); `Op::Mod` the signed
2333            // remainder; `Op::FloorDiv` the floored quotient. All trap `b == 0 → 0` (see `FuncUnit`).
2334            // `Op::ExactDiv` is DELIBERATELY excluded: the VM yields a Rational (`7/2 → 7/2`) that an
2335            // integer fabric cannot represent, so it falls to the honest `unsupported` arm below.
2336            Op::Div { dst, lhs, rhs } => {
2337                let float =
2338                    FLOAT_REGS.with(|t| t.borrow().contains(lhs) || t.borrow().contains(rhs));
2339                let (a, b) = (getb(reg, *lhs), getb(reg, *rhs));
2340                let k = cells.len();
2341                if float {
2342                    cells.push(LoopCell::Func {
2343                        unit: FuncUnit::FDivF,
2344                        a,
2345                        b,
2346                    });
2347                    FLOAT_REGS.with(|t| {
2348                        t.borrow_mut().insert(*dst);
2349                    });
2350                } else {
2351                    cells.push(LoopCell::Func {
2352                        unit: FuncUnit::DivS,
2353                        a,
2354                        b,
2355                    });
2356                    // An integer quotient CLEARS any stale float tag on `dst` (registers are reused across
2357                    // blocks, and `FLOAT_REGS` is global — a reg that held an f64 in a prior block is now Int).
2358                    FLOAT_REGS.with(|t| {
2359                        t.borrow_mut().remove(dst);
2360                    });
2361                }
2362                reg.insert(*dst, Src::Val(k));
2363            }
2364            Op::Mod { dst, lhs, rhs } => {
2365                let k = cells.len();
2366                cells.push(LoopCell::Func {
2367                    unit: FuncUnit::ModS,
2368                    a: getb(reg, *lhs),
2369                    b: getb(reg, *rhs),
2370                });
2371                reg.insert(*dst, Src::Val(k));
2372                FLOAT_REGS.with(|t| {
2373                    t.borrow_mut().remove(dst);
2374                }); // integer remainder — clear stale float tag
2375            }
2376            Op::FloorDiv { dst, lhs, rhs } => {
2377                let k = cells.len();
2378                cells.push(LoopCell::Func {
2379                    unit: FuncUnit::FloorDivS,
2380                    a: getb(reg, *lhs),
2381                    b: getb(reg, *rhs),
2382                });
2383                reg.insert(*dst, Src::Val(k));
2384                FLOAT_REGS.with(|t| {
2385                    t.borrow_mut().remove(dst);
2386                }); // integer floor-div — clear stale float tag
2387            }
2388            // `a to the power of b` — the sequential power functional unit (RTL: a `b`-step multiply FSM).
2389            Op::Pow { dst, lhs, rhs } => {
2390                let k = cells.len();
2391                cells.push(LoopCell::Func {
2392                    unit: FuncUnit::Pow,
2393                    a: getb(reg, *lhs),
2394                    b: getb(reg, *rhs),
2395                });
2396                reg.insert(*dst, Src::Val(k));
2397            }
2398            // NUMERIC COPROCESSOR builtins in a combinational loop body — `sqrt`/`floor` on the float
2399            // functional units (`FSqrtF`/`FFloorF`), `abs` (float = sign-bit clear, int = negate+select),
2400            // `min`/`max` (integer select). Mirrors the whole-program engine's lowering with `LoopCell` tiles,
2401            // so `Set s to sqrt(s + …)` / `abs(…)` / `min(…, …)` in a loop become spatial datapath tiles.
2402            Op::CallBuiltin {
2403                dst,
2404                builtin,
2405                args_start,
2406                arg_count: _,
2407            } => {
2408                use crate::semantics::builtins::BuiltinId;
2409                let a0 = getb(reg, *args_start);
2410                let is_f = FLOAT_REGS.with(|t| t.borrow().contains(args_start));
2411                match builtin {
2412                    BuiltinId::Sqrt if is_f => {
2413                        let k = cells.len();
2414                        cells.push(LoopCell::Func { unit: FuncUnit::FSqrtF, a: a0, b: Src::Const(0) });
2415                        reg.insert(*dst, Src::Val(k));
2416                        FLOAT_REGS.with(|t| { t.borrow_mut().insert(*dst); });
2417                    }
2418                    BuiltinId::Floor if is_f => {
2419                        let k = cells.len();
2420                        cells.push(LoopCell::Func { unit: FuncUnit::FFloorF, a: a0, b: Src::Const(0) });
2421                        reg.insert(*dst, Src::Val(k));
2422                        FLOAT_REGS.with(|t| { t.borrow_mut().remove(dst); }); // floor yields an Int
2423                    }
2424                    BuiltinId::Floor => {
2425                        reg.insert(*dst, a0); // `floor` of an integer is the identity
2426                        FLOAT_REGS.with(|t| { t.borrow_mut().remove(dst); });
2427                    }
2428                    // `ceil(x) = -floor(-x)`: negate the float, take the FFloorF int bridge, negate the integer.
2429                    BuiltinId::Ceil if is_f => {
2430                        let kneg = cells.len();
2431                        cells.push(LoopCell::Func { unit: FuncUnit::FSubF, a: Src::Const(0), b: a0 }); // 0.0 - x
2432                        let kfl = cells.len();
2433                        cells.push(LoopCell::Func { unit: FuncUnit::FFloorF, a: Src::Val(kneg), b: Src::Const(0) });
2434                        let k = cells.len();
2435                        cells.push(LoopCell::Arith { op: AluOp::Sub, a: Src::Const(0), b: Src::Val(kfl) }); // -floor(-x)
2436                        reg.insert(*dst, Src::Val(k));
2437                        FLOAT_REGS.with(|t| { t.borrow_mut().remove(dst); }); // ceil yields an Int
2438                    }
2439                    BuiltinId::Ceil => {
2440                        reg.insert(*dst, a0); // `ceil` of an integer is the identity
2441                        FLOAT_REGS.with(|t| { t.borrow_mut().remove(dst); });
2442                    }
2443                    // `round(x)` — IEEE round-HALF-AWAY-FROM-ZERO (Rust `f64::round`): `floor(|x| + 0.5)` gives the
2444                    // magnitude (a tie like 2.5 rounds UP to 3, never to even), then the sign of `x` is applied via
2445                    // a float-compare `x < 0` select. `LoopCell::Select` only carries an integer compare, so the
2446                    // sign test is an `FLtF` unit feeding an integer 0/1 predicate.
2447                    BuiltinId::Round if is_f => {
2448                        let kabs = cells.len();
2449                        cells.push(LoopCell::Arith { op: AluOp::And, a: a0, b: Src::Const(0x7FFF_FFFF_FFFF_FFFF) }); // |x|
2450                        let ksum = cells.len();
2451                        cells.push(LoopCell::Func { unit: FuncUnit::FAddF, a: Src::Val(kabs), b: Src::Const(0.5f64.to_bits() as i64) });
2452                        let kfl = cells.len();
2453                        cells.push(LoopCell::Func { unit: FuncUnit::FFloorF, a: Src::Val(ksum), b: Src::Const(0) }); // floor(|x|+0.5) ≥ 0
2454                        let kneg = cells.len();
2455                        cells.push(LoopCell::Arith { op: AluOp::Sub, a: Src::Const(0), b: Src::Val(kfl) }); // -magnitude
2456                        let kpred = cells.len();
2457                        cells.push(LoopCell::Func { unit: FuncUnit::FLtF, a: a0, b: Src::Const(0) }); // x < 0.0
2458                        let ksel = cells.len();
2459                        cells.push(LoopCell::Select {
2460                            cmp: CmpKind::Ne,
2461                            ca: Src::Val(kpred),
2462                            cb: Src::Const(0),
2463                            t: Src::Val(kneg),
2464                            e: Src::Val(kfl),
2465                        });
2466                        reg.insert(*dst, Src::Val(ksel));
2467                        FLOAT_REGS.with(|t| { t.borrow_mut().remove(dst); }); // round yields an Int
2468                    }
2469                    BuiltinId::Round => {
2470                        reg.insert(*dst, a0); // `round` of an integer is the identity
2471                        FLOAT_REGS.with(|t| { t.borrow_mut().remove(dst); });
2472                    }
2473                    BuiltinId::Abs if is_f => {
2474                        let k = cells.len();
2475                        cells.push(LoopCell::Arith { op: AluOp::And, a: a0, b: Src::Const(0x7FFF_FFFF_FFFF_FFFF) });
2476                        reg.insert(*dst, Src::Val(k));
2477                        FLOAT_REGS.with(|t| { t.borrow_mut().insert(*dst); });
2478                    }
2479                    BuiltinId::Abs => {
2480                        let kneg = cells.len();
2481                        cells.push(LoopCell::Arith { op: AluOp::Sub, a: Src::Const(0), b: a0 }); // 0 - a
2482                        let ksel = cells.len();
2483                        cells.push(LoopCell::Select { cmp: CmpKind::Lt, ca: a0, cb: Src::Const(0), t: Src::Val(kneg), e: a0 });
2484                        reg.insert(*dst, Src::Val(ksel));
2485                        FLOAT_REGS.with(|t| { t.borrow_mut().remove(dst); });
2486                    }
2487                    BuiltinId::Min | BuiltinId::Max => {
2488                        let a1 = getb(reg, *args_start + 1);
2489                        let b_f = FLOAT_REGS.with(|t| t.borrow().contains(&(*args_start + 1)));
2490                        let is_min = matches!(builtin, BuiltinId::Min);
2491                        if is_f || b_f {
2492                            // FLOAT min/max: a float-compare functional unit yields the 0/1 predicate
2493                            // (`FLtF` for min, `FGtF` for max), and a `Select` on `pred != 0` muxes the two
2494                            // operands — `LoopCell::Select` only carries an INTEGER compare, so the float
2495                            // comparison must be a Func tile feeding an integer predicate register.
2496                            let unit = if is_min { FuncUnit::FLtF } else { FuncUnit::FGtF };
2497                            let kpred = cells.len();
2498                            cells.push(LoopCell::Func { unit, a: a0, b: a1 });
2499                            let ksel = cells.len();
2500                            cells.push(LoopCell::Select {
2501                                cmp: CmpKind::Ne,
2502                                ca: Src::Val(kpred),
2503                                cb: Src::Const(0),
2504                                t: a0,
2505                                e: a1,
2506                            });
2507                            reg.insert(*dst, Src::Val(ksel));
2508                            FLOAT_REGS.with(|t| { t.borrow_mut().insert(*dst); }); // min/max of floats is float
2509                        } else {
2510                            let cmp = if is_min { CmpKind::Lt } else { CmpKind::Gt };
2511                            let k = cells.len();
2512                            cells.push(LoopCell::Select { cmp, ca: a0, cb: a1, t: a0, e: a1 });
2513                            reg.insert(*dst, Src::Val(k));
2514                            FLOAT_REGS.with(|t| { t.borrow_mut().remove(dst); });
2515                        }
2516                    }
2517                    // WORD32 (ℤ/2³²) lane — the SHA-256 bit-mixing primitives, each self-masking to 32 bits
2518                    // (a word32 value rides a POSITIVE i64 in `0..2³²`, so an arithmetic `Shr` == logical).
2519                    BuiltinId::Word32 => {
2520                        let k = cells.len();
2521                        cells.push(LoopCell::Arith { op: AluOp::And, a: a0, b: Src::Const(0xFFFF_FFFF) });
2522                        reg.insert(*dst, Src::Val(k));
2523                    }
2524                    BuiltinId::IntOfWord32 => {
2525                        reg.insert(*dst, a0); // the unsigned value as an Int (identity)
2526                    }
2527                    BuiltinId::Wand | BuiltinId::Wor => {
2528                        let a1 = getb(reg, *args_start + 1);
2529                        let op = if matches!(builtin, BuiltinId::Wand) { AluOp::And } else { AluOp::Or };
2530                        let k = cells.len();
2531                        cells.push(LoopCell::Arith { op, a: a0, b: a1 });
2532                        reg.insert(*dst, Src::Val(k));
2533                    }
2534                    BuiltinId::Wnot => {
2535                        let kx = cells.len();
2536                        cells.push(LoopCell::Arith { op: AluOp::Xor, a: a0, b: Src::Const(-1) }); // ~a
2537                        let km = cells.len();
2538                        cells.push(LoopCell::Arith { op: AluOp::And, a: Src::Val(kx), b: Src::Const(0xFFFF_FFFF) });
2539                        reg.insert(*dst, Src::Val(km));
2540                    }
2541                    BuiltinId::Word32Shr => {
2542                        let n = getb(reg, *args_start + 1);
2543                        let k = cells.len();
2544                        cells.push(LoopCell::Arith { op: AluOp::Shr, a: a0, b: n });
2545                        reg.insert(*dst, Src::Val(k));
2546                    }
2547                    BuiltinId::Rotr | BuiltinId::Rotl => {
2548                        let n = getb(reg, *args_start + 1);
2549                        // rotr = (w >> n) | (w << (32-n)); rotl swaps the two shift directions. Then mask 32.
2550                        let (dir_a, dir_b) = if matches!(builtin, BuiltinId::Rotr) {
2551                            (AluOp::Shr, AluOp::Shl)
2552                        } else {
2553                            (AluOp::Shl, AluOp::Shr)
2554                        };
2555                        let k1 = cells.len();
2556                        cells.push(LoopCell::Arith { op: dir_a, a: a0, b: n });
2557                        let kcomp = cells.len();
2558                        cells.push(LoopCell::Arith { op: AluOp::Sub, a: Src::Const(32), b: n }); // 32 - n
2559                        let k2 = cells.len();
2560                        cells.push(LoopCell::Arith { op: dir_b, a: a0, b: Src::Val(kcomp) });
2561                        let kor = cells.len();
2562                        cells.push(LoopCell::Arith { op: AluOp::Or, a: Src::Val(k1), b: Src::Val(k2) });
2563                        let km = cells.len();
2564                        cells.push(LoopCell::Arith { op: AluOp::And, a: Src::Val(kor), b: Src::Const(0xFFFF_FFFF) });
2565                        reg.insert(*dst, Src::Val(km));
2566                    }
2567                    // WORD64 (ℤ/2⁶⁴) lane — SHA-512 / Keccak 64-bit mixing. A Word64 value rides the FULL i64 bit
2568                    // pattern, and the VM's `intOfWord64` is `w.to_u64() as i64` (a pure reinterpretation), so
2569                    // `word64`/`intOfWord64` are IDENTITY and wrapping `+`/`-`/`×` need NO mask (native i64 arithmetic
2570                    // already wraps mod 2⁶⁴). The only non-`Arith`-identity op is the LOGICAL right shift, which uses
2571                    // the zero-fill `AluOp::UShr` (the arithmetic `Shr` would sign-extend a bit-63-set value).
2572                    BuiltinId::Word64 | BuiltinId::IntOfWord64 => {
2573                        reg.insert(*dst, a0); // a Word64 IS its i64 bit pattern — no truncation, no mask
2574                    }
2575                    BuiltinId::Word64And => {
2576                        let a1 = getb(reg, *args_start + 1);
2577                        let k = cells.len();
2578                        cells.push(LoopCell::Arith { op: AluOp::And, a: a0, b: a1 });
2579                        reg.insert(*dst, Src::Val(k));
2580                    }
2581                    BuiltinId::Word64Shl => {
2582                        let n = getb(reg, *args_start + 1);
2583                        let k = cells.len();
2584                        cells.push(LoopCell::Arith { op: AluOp::Shl, a: a0, b: n });
2585                        reg.insert(*dst, Src::Val(k));
2586                    }
2587                    BuiltinId::Word64Shr => {
2588                        let n = getb(reg, *args_start + 1);
2589                        let k = cells.len();
2590                        cells.push(LoopCell::Arith { op: AluOp::UShr, a: a0, b: n }); // LOGICAL (zero-fill)
2591                        reg.insert(*dst, Src::Val(k));
2592                    }
2593                    // `count_ones` — the combinational PopCount functional unit (set-bit count of the operand).
2594                    BuiltinId::CountOnes => {
2595                        let k = cells.len();
2596                        cells.push(LoopCell::Func { unit: FuncUnit::PopCount, a: a0, b: Src::Const(0) });
2597                        reg.insert(*dst, Src::Val(k));
2598                        FLOAT_REGS.with(|t| { t.borrow_mut().remove(dst); }); // popcount yields an Int
2599                    }
2600                    // DATE-COMPONENT extractors on a clock value (`the year of today`, `the hour of now`). The arg
2601                    // is a scalar Moment (nanos) or Date (whole days) tracked by REG_TIMEKIND. Calendar fields
2602                    // (year/month/day/quarter/ISO-week) lower to the full Gregorian `civil_from_days` tile chain;
2603                    // time-of-day (hour/minute/second) and weekday are pure div/mod. Every `/` is truncating `DivS`
2604                    // and every `mod` is `ModS` — == the VM's `div_euclid`/`rem_euclid` for the (positive) instants a
2605                    // real clock produces. Bit-identical to `civil_from_unix_nanos` / `iso_week_from_days`.
2606                    BuiltinId::YearOf
2607                    | BuiltinId::MonthOf
2608                    | BuiltinId::DayOf
2609                    | BuiltinId::QuarterOf
2610                    | BuiltinId::WeekOf
2611                    | BuiltinId::HourOf
2612                    | BuiltinId::MinuteOf
2613                    | BuiltinId::SecondOf
2614                    | BuiltinId::WeekdayOf => {
2615                        use logicaffeine_base::temporal::{NANOS_PER_DAY, NANOS_PER_SECOND};
2616                        let kind = REG_TIMEKIND
2617                            .with(|m| m.borrow().get(args_start).copied())
2618                            .unwrap_or_else(|| {
2619                                panic!(
2620                                    "fabric CallBuiltin {builtin:?}: register {args_start} is not a tracked clock \
2621                                     value — a date-component extractor needs a `now`/`today` value"
2622                                )
2623                            });
2624                        // days since epoch: a Moment divides its nanos by NANOS_PER_DAY; a Date already IS days.
2625                        let days = match kind {
2626                            TimeKind::Moment => t_func(cells, FuncUnit::DivS, a0, Src::Const(NANOS_PER_DAY)),
2627                            TimeKind::Date => a0,
2628                        };
2629                        let result = match builtin {
2630                            BuiltinId::WeekdayOf => iso_weekday_from_days_tiles(cells, days),
2631                            BuiltinId::YearOf => civil_from_days_tiles(cells, days).0,
2632                            BuiltinId::MonthOf => civil_from_days_tiles(cells, days).1,
2633                            BuiltinId::DayOf => civil_from_days_tiles(cells, days).2,
2634                            BuiltinId::QuarterOf => {
2635                                // (month - 1) / 3 + 1.
2636                                let month = civil_from_days_tiles(cells, days).1;
2637                                let m1 = t_arith(cells, AluOp::Sub, month, Src::Const(1));
2638                                let q = t_func(cells, FuncUnit::DivS, m1, Src::Const(3));
2639                                t_arith(cells, AluOp::Add, q, Src::Const(1))
2640                            }
2641                            BuiltinId::WeekOf => {
2642                                // iso_week_from_days(z).1: the ISO year's Thursday names the year, whose Jan-4
2643                                // week-1-Monday anchors the count. week = (z - week1_monday) / 7 + 1.
2644                                let z = days;
2645                                let iso_dow = iso_weekday_from_days_tiles(cells, z);
2646                                let four_minus = t_arith(cells, AluOp::Sub, Src::Const(4), iso_dow);
2647                                let thursday = t_arith(cells, AluOp::Add, z, four_minus);
2648                                let iso_year = civil_from_days_tiles(cells, thursday).0;
2649                                let jan4 = days_from_civil_tiles(cells, iso_year, Src::Const(1), Src::Const(4));
2650                                let dow_jan4 = iso_weekday_from_days_tiles(cells, jan4);
2651                                let dm1 = t_arith(cells, AluOp::Sub, dow_jan4, Src::Const(1));
2652                                let week1_monday = t_arith(cells, AluOp::Sub, jan4, dm1);
2653                                let diff = t_arith(cells, AluOp::Sub, z, week1_monday);
2654                                let wk = t_func(cells, FuncUnit::DivS, diff, Src::Const(7));
2655                                t_arith(cells, AluOp::Add, wk, Src::Const(1))
2656                            }
2657                            BuiltinId::HourOf | BuiltinId::MinuteOf | BuiltinId::SecondOf => {
2658                                assert!(
2659                                    kind == TimeKind::Moment,
2660                                    "fabric CallBuiltin {builtin:?}: a Date has no time-of-day (the VM errors) — \
2661                                     use a Moment"
2662                                );
2663                                // seconds-of-day = (nanos % NANOS_PER_DAY) / NANOS_PER_SECOND.
2664                                let rem = t_func(cells, FuncUnit::ModS, a0, Src::Const(NANOS_PER_DAY));
2665                                let secs = t_func(cells, FuncUnit::DivS, rem, Src::Const(NANOS_PER_SECOND));
2666                                match builtin {
2667                                    BuiltinId::HourOf => t_func(cells, FuncUnit::DivS, secs, Src::Const(3600)),
2668                                    BuiltinId::MinuteOf => {
2669                                        let m = t_func(cells, FuncUnit::ModS, secs, Src::Const(3600));
2670                                        t_func(cells, FuncUnit::DivS, m, Src::Const(60))
2671                                    }
2672                                    BuiltinId::SecondOf => t_func(cells, FuncUnit::ModS, secs, Src::Const(60)),
2673                                    _ => unreachable!(),
2674                                }
2675                            }
2676                            _ => unreachable!(),
2677                        };
2678                        reg.insert(*dst, result);
2679                        FLOAT_REGS.with(|t| { t.borrow_mut().remove(dst); }); // a date component is an Int
2680                    }
2681                    other => panic!(
2682                        "loop body: builtin `{other:?}` unsupported (sqrt/floor/abs/min/max/ceil/round/count_ones \
2683                         + word32 lane + date-component extractors)"
2684                    ),
2685                }
2686                // Track word32-ness: the constructing / bit-op builtins yield a 32-bit value (so a later `+`
2687                // wraps); `intOfWord32` yields a plain Int; everything else (sqrt/abs/…) is not word32.
2688                match builtin {
2689                    BuiltinId::Word32
2690                    | BuiltinId::Wand
2691                    | BuiltinId::Wor
2692                    | BuiltinId::Wnot
2693                    | BuiltinId::Rotr
2694                    | BuiltinId::Rotl
2695                    | BuiltinId::Word32Shr => {
2696                        W32_REGS.with(|t| {
2697                            t.borrow_mut().insert(*dst);
2698                        });
2699                    }
2700                    _ => {
2701                        W32_REGS.with(|t| {
2702                            t.borrow_mut().remove(dst);
2703                        });
2704                    }
2705                }
2706            }
2707            // `ExactDiv` — the Oracle emits it only when the divisor divides the dividend EXACTLY, so the
2708            // truncating `DivS` yields that same whole quotient (the VM downsizes its exact Rational to the
2709            // identical Int). Realized under that exact-operand contract; hand-bytecode witnessed on exact
2710            // operands. (No source emits it on the no-oracle fabric compile path.)
2711            Op::ExactDiv { dst, lhs, rhs } => {
2712                let k = cells.len();
2713                cells.push(LoopCell::Func {
2714                    unit: FuncUnit::DivS,
2715                    a: getb(reg, *lhs),
2716                    b: getb(reg, *rhs),
2717                });
2718                reg.insert(*dst, Src::Val(k));
2719            }
2720            // `DivPow2` — `lhs / 2^k`. The VM computes `Div` by the constant `2^k` (truncating, NOT an
2721            // arithmetic shift — they differ for negative `lhs`), so the fabric divides by that same const.
2722            Op::DivPow2 { dst, lhs, k: kexp } => {
2723                let k = cells.len();
2724                cells.push(LoopCell::Func {
2725                    unit: FuncUnit::DivS,
2726                    a: getb(reg, *lhs),
2727                    b: Src::Const(1i64 << kexp),
2728                });
2729                reg.insert(*dst, Src::Val(k));
2730            }
2731            // `BigIntWide` — the 256-bit BigInt lane. FromInt/Add/Mul are ring-homomorphic on the low bits,
2732            // and `Low` extracts `mod 2^32` — but the VM returns 0 for `Low` of a value that OVERFLOWED to a
2733            // BigInt, so the fabric tracks a `big` flag (BIGINT_BIG) that promotes on signed `i64` overflow.
2734            // Bigness is monotone under Add/Mul (Sub could un-promote, so it — with Cmp/Div — is refused).
2735            Op::BigIntWide { kind, dst, a, b } => {
2736                let big_of = |r: &u16| {
2737                    BIGINT_BIG
2738                        .with(|m| m.borrow().get(r).copied())
2739                        .unwrap_or(Src::Const(0))
2740                };
2741                match kind {
2742                    0 => {
2743                        // FromInt: dst = the Int value, not yet big.
2744                        let v = getb(reg, *a);
2745                        reg.insert(*dst, v);
2746                        BIGINT_BIG.with(|m| { m.borrow_mut().insert(*dst, Src::Const(0)); });
2747                    }
2748                    1 => {
2749                        // Add: value = a + b (i64); big = a.big | b.big | signed_overflow(a, b).
2750                        let (av, bv) = (getb(reg, *a), getb(reg, *b));
2751                        let ksum = cells.len();
2752                        cells.push(LoopCell::Arith { op: AluOp::Add, a: av, b: bv });
2753                        let sum = Src::Val(ksum);
2754                        // signed add overflow: ((a ^ sum) & (b ^ sum)) < 0
2755                        let kxa = cells.len(); cells.push(LoopCell::Arith { op: AluOp::Xor, a: av, b: sum });
2756                        let kxb = cells.len(); cells.push(LoopCell::Arith { op: AluOp::Xor, a: bv, b: sum });
2757                        let kand = cells.len(); cells.push(LoopCell::Arith { op: AluOp::And, a: Src::Val(kxa), b: Src::Val(kxb) });
2758                        let kovf = cells.len(); cells.push(LoopCell::Select { cmp: CmpKind::Lt, ca: Src::Val(kand), cb: Src::Const(0), t: Src::Const(1), e: Src::Const(0) });
2759                        let ko1 = cells.len(); cells.push(LoopCell::Arith { op: AluOp::Or, a: big_of(a), b: big_of(b) });
2760                        let ko2 = cells.len(); cells.push(LoopCell::Arith { op: AluOp::Or, a: Src::Val(ko1), b: Src::Val(kovf) });
2761                        reg.insert(*dst, sum);
2762                        BIGINT_BIG.with(|m| { m.borrow_mut().insert(*dst, Src::Val(ko2)); });
2763                    }
2764                    6 => {
2765                        // Mul: value = a * b (i64 low); big = a.big | b.big | (128-bit product overflows i64).
2766                        let (av, bv) = (getb(reg, *a), getb(reg, *b));
2767                        let kmul = cells.len(); cells.push(LoopCell::Arith { op: AluOp::Mul, a: av, b: bv });
2768                        let kovf = cells.len(); cells.push(LoopCell::Func { unit: FuncUnit::MulOvfS, a: av, b: bv });
2769                        let ko1 = cells.len(); cells.push(LoopCell::Arith { op: AluOp::Or, a: big_of(a), b: big_of(b) });
2770                        let ko2 = cells.len(); cells.push(LoopCell::Arith { op: AluOp::Or, a: Src::Val(ko1), b: Src::Val(kovf) });
2771                        reg.insert(*dst, Src::Val(kmul));
2772                        BIGINT_BIG.with(|m| { m.borrow_mut().insert(*dst, Src::Val(ko2)); });
2773                    }
2774                    4 => {
2775                        // Low: 0 if the value is a BigInt, else its low 32 bits. The result is a plain Int.
2776                        let av = getb(reg, *a);
2777                        let kmask = cells.len(); cells.push(LoopCell::Arith { op: AluOp::And, a: av, b: Src::Const(0xFFFF_FFFF) });
2778                        let klow = cells.len(); cells.push(LoopCell::Select { cmp: CmpKind::Ne, ca: big_of(a), cb: Src::Const(0), t: Src::Const(0), e: Src::Val(kmask) });
2779                        reg.insert(*dst, Src::Val(klow));
2780                        BIGINT_BIG.with(|m| { m.borrow_mut().remove(dst); }); // a plain Int now
2781                    }
2782                    _ => panic!(
2783                        "fabric BigIntWide: kind {kind} (Sub/Cmp/Div) is a follow-on — Sub can UN-promote a \
2784                         BigInt to Int (needs the wide value), Cmp/Div need the full magnitude; \
2785                         FromInt/Add/Mul/Low are realized"
2786                    ),
2787                }
2788            }
2789            // `a is approximately b` — the numeric approximate-equality FUNCTIONAL UNIT (both operands to
2790            // f64, `logos_approx_eq`). Float operands need the float lane (refused at the Float load), so only
2791            // the numeric-int case reaches here; yields a 0/1 Bool.
2792            Op::ApproxEq { dst, lhs, rhs } => {
2793                let k = cells.len();
2794                cells.push(LoopCell::Func {
2795                    unit: FuncUnit::ApproxEqF,
2796                    a: getb(reg, *lhs),
2797                    b: getb(reg, *rhs),
2798                });
2799                reg.insert(*dst, Src::Val(k));
2800            }
2801            // `MagicDivU` — division/modulo by a CONSTANT via the precomputed magic reciprocal (emitted for a
2802            // proven non-negative dividend). This reconstructs `compiler::magic_eval` gate-for-gate as tiles:
2803            // `hi = mulhi_u(magic, n)`, then the shift/add reduction (all shifts LOGICAL — `logical_shr_const`
2804            // — since the magic path is `u64`), then `mul_back == 0 ? q : n − q·c` (quotient vs remainder).
2805            // `magic`/`more`/`mul_back` are compile-time immediates, so the branch is resolved at BUILD time.
2806            Op::MagicDivU {
2807                dst,
2808                lhs,
2809                magic,
2810                more,
2811                mul_back,
2812                divisor: _,
2813                wrap: _,
2814            } => {
2815                const SHIFT_MASK: u8 = 0x3F;
2816                const ADD_MARKER: u8 = 0x40;
2817                const SHIFT_PATH: u8 = 0x80;
2818                let n = getb(reg, *lhs);
2819                let shift = (*more & SHIFT_MASK) as i64;
2820                let q = if *more & SHIFT_PATH != 0 {
2821                    logical_shr_const(cells, n, shift) // pure power-of-two path: q = n >> shift
2822                } else {
2823                    let khi = cells.len(); // hi = high 64 bits of the unsigned magic·n product
2824                    cells.push(LoopCell::Func {
2825                        unit: FuncUnit::MulHiU,
2826                        a: Src::Const(*magic as i64),
2827                        b: n,
2828                    });
2829                    let hi = Src::Val(khi);
2830                    if *more & ADD_MARKER != 0 {
2831                        // the "add" correction: t = ((n − hi) >>1) + hi ; q = t >> shift
2832                        let ksub = cells.len();
2833                        cells.push(LoopCell::Arith {
2834                            op: AluOp::Sub,
2835                            a: n,
2836                            b: hi,
2837                        });
2838                        let half = logical_shr_const(cells, Src::Val(ksub), 1);
2839                        let kadd = cells.len();
2840                        cells.push(LoopCell::Arith {
2841                            op: AluOp::Add,
2842                            a: half,
2843                            b: hi,
2844                        });
2845                        logical_shr_const(cells, Src::Val(kadd), shift)
2846                    } else {
2847                        logical_shr_const(cells, hi, shift) // q = hi >> shift
2848                    }
2849                };
2850                let result = if *mul_back == 0 {
2851                    q // the quotient x / c
2852                } else {
2853                    // the remainder x % c = x − q·c (mul_back carries the divisor c)
2854                    let kmul = cells.len();
2855                    cells.push(LoopCell::Arith {
2856                        op: AluOp::Mul,
2857                        a: q,
2858                        b: Src::Const(*mul_back),
2859                    });
2860                    let kres = cells.len();
2861                    cells.push(LoopCell::Arith {
2862                        op: AluOp::Sub,
2863                        a: n,
2864                        b: Src::Val(kmul),
2865                    });
2866                    Src::Val(kres)
2867                };
2868                reg.insert(*dst, result);
2869            }
2870            // ── state-plane READS: the heap mirrors the VM's `[len, e1..eN]` (1-based) layout ──
2871            // `length of xs` = heap[ptr]; `item i of xs` = heap[ptr + i].
2872            Op::Length { dst, collection } => {
2873                let ptr = getb(reg, *collection);
2874                let k = cells.len();
2875                cells.push(LoopCell::HeapRead { addr: ptr });
2876                reg.insert(*dst, Src::Val(k));
2877            }
2878            Op::Index {
2879                dst,
2880                collection,
2881                index,
2882            }
2883            | Op::IndexUnchecked {
2884                dst,
2885                collection,
2886                index,
2887            } => {
2888                let ptr = getb(reg, *collection);
2889                let idx = getb(reg, *index);
2890                let ka = cells.len(); // addr = ptr + i (1-based element i lives at heap[ptr + i])
2891                cells.push(LoopCell::Arith {
2892                    op: AluOp::Add,
2893                    a: ptr,
2894                    b: idx,
2895                });
2896                let kr = cells.len();
2897                cells.push(LoopCell::HeapRead { addr: Src::Val(ka) });
2898                reg.insert(*dst, Src::Val(kr));
2899            }
2900            // ── state-plane ALLOC + WRITE: compose HeapAlloc/HeapWrite into fresh lists + in-place set ──
2901            // `[a, b, ..]` = alloc(count+1), write len header, write each element at heap[ptr + j] (1-based).
2902            Op::NewList { dst, start, count } => {
2903                let n = *count;
2904                let ka = cells.len();
2905                cells.push(LoopCell::HeapAlloc {
2906                    size: Src::Const(n as i64 + 1),
2907                });
2908                let ptr = Src::Val(ka);
2909                cells.push(LoopCell::HeapWrite {
2910                    addr: ptr,
2911                    val: Src::Const(n as i64),
2912                }); // length header
2913                for j in 0..n {
2914                    let elem = getb(reg, *start + j);
2915                    let kadd = cells.len(); // element j (0-based) is 1-based item j+1 at heap[ptr + (j+1)]
2916                    cells.push(LoopCell::Arith {
2917                        op: AluOp::Add,
2918                        a: ptr,
2919                        b: Src::Const(j as i64 + 1),
2920                    });
2921                    cells.push(LoopCell::HeapWrite {
2922                        addr: Src::Val(kadd),
2923                        val: elem,
2924                    });
2925                }
2926                reg.insert(*dst, ptr);
2927                COLL_LEN.with(|m| m.borrow_mut().insert(*dst, n as i64));
2928            }
2929            Op::Args { dst } => {
2930                let ptr = heap_argv(cells, 0);
2931                reg.insert(*dst, ptr);
2932                TEXT_REGS.with(|t| {
2933                    t.borrow_mut().remove(dst);
2934                });
2935                COLL_LEN.with(|m| m.borrow_mut().insert(*dst, 1));
2936            }
2937            // An empty list, set, OR map — the same `[0]` (length-0) heap region. (Dedup / key structure only
2938            // matters once something is added; the empty collection's length read is identical to a list's.)
2939            Op::NewEmptyListFixed { dst, capacity } => {
2940                let ka = cells.len();
2941                cells.push(LoopCell::HeapAlloc {
2942                    size: Src::Const(i64::from(*capacity) + 1),
2943                });
2944                let ptr = Src::Val(ka);
2945                cells.push(LoopCell::HeapWrite {
2946                    addr: ptr,
2947                    val: Src::Const(0),
2948                });
2949                reg.insert(*dst, ptr);
2950                COLL_LEN.with(|m| m.borrow_mut().insert(*dst, 0));
2951            }
2952            Op::NewEmptyList { dst }
2953            | Op::NewEmptyListWithCapacity { dst, .. }
2954            | Op::NewEmptyListI32 { dst }
2955            | Op::NewEmptySet { dst }
2956            | Op::NewEmptyMap { dst }
2957            | Op::NewEmptyMapWithCapacity { dst, .. } => {
2958                let ka = cells.len();
2959                cells.push(LoopCell::HeapAlloc {
2960                    size: Src::Const(1),
2961                });
2962                let ptr = Src::Val(ka);
2963                cells.push(LoopCell::HeapWrite {
2964                    addr: ptr,
2965                    val: Src::Const(0),
2966                }); // empty: length 0
2967                reg.insert(*dst, ptr);
2968                COLL_LEN.with(|m| m.borrow_mut().insert(*dst, 0));
2969            }
2970            // A tuple `(a, b, c)` shares the list heap shape `[count, e0..e_{count-1}]`; its `count` is in
2971            // the op, so no type lookup is needed.
2972            Op::NewTuple { dst, start, count } => {
2973                let n = *count;
2974                let ka = cells.len();
2975                cells.push(LoopCell::HeapAlloc {
2976                    size: Src::Const(n as i64 + 1),
2977                });
2978                let ptr = Src::Val(ka);
2979                cells.push(LoopCell::HeapWrite {
2980                    addr: ptr,
2981                    val: Src::Const(n as i64),
2982                });
2983                for j in 0..n {
2984                    let elem = getb(reg, *start + j);
2985                    let kadd = cells.len();
2986                    cells.push(LoopCell::Arith {
2987                        op: AluOp::Add,
2988                        a: ptr,
2989                        b: Src::Const(j as i64 + 1),
2990                    });
2991                    cells.push(LoopCell::HeapWrite {
2992                        addr: Src::Val(kadd),
2993                        val: elem,
2994                    });
2995                }
2996                reg.insert(*dst, ptr);
2997                COLL_LEN.with(|m| m.borrow_mut().insert(*dst, n as i64));
2998            }
2999            // `Let (x, y, ..) be t` — read each 0-based component `heap[ptr + 1 + j]` into `R[start + j]`.
3000            Op::DestructureTuple { src, start, count } => {
3001                let ptr = getb(reg, *src);
3002                for j in 0..*count {
3003                    let kadd = cells.len();
3004                    cells.push(LoopCell::Arith {
3005                        op: AluOp::Add,
3006                        a: ptr,
3007                        b: Src::Const(j as i64 + 1),
3008                    });
3009                    let kr = cells.len();
3010                    cells.push(LoopCell::HeapRead {
3011                        addr: Src::Val(kadd),
3012                    });
3013                    reg.insert(*start + j, Src::Val(kr));
3014                }
3015            }
3016            // `SetIndex s[name] = v` on a struct is a field write by NAME — how a place mutation clears a field
3017            // and writes the mutated leaf back — at the slot `GetField`/`StructInsert` resolve for that name.
3018            Op::SetIndex {
3019                collection,
3020                index,
3021                value,
3022            }
3023            | Op::SetIndexUnchecked {
3024                collection,
3025                index,
3026                value,
3027            } if REG_TYPE.with(|m| m.borrow().contains_key(collection)) => {
3028                let ptr = getb(reg, *collection);
3029                let val = getb(reg, *value);
3030                let name = NAME_REGS
3031                    .with(|m| m.borrow().get(index).copied())
3032                    .unwrap_or_else(|| {
3033                        panic!("struct SetIndex keyed by R{index}, which holds no constant name")
3034                    });
3035                let slot = struct_field_slot_named(*collection, name, consts);
3036                let kadd = cells.len();
3037                cells.push(LoopCell::Arith {
3038                    op: AluOp::Add,
3039                    a: ptr,
3040                    b: Src::Const(slot as i64 + 1),
3041                });
3042                cells.push(LoopCell::HeapWrite {
3043                    addr: Src::Val(kadd),
3044                    val,
3045                });
3046            }
3047            // `Set item i of xs to v` — an in-place write (value-semantics COW is upstream DeepClone).
3048            Op::SetIndex {
3049                collection,
3050                index,
3051                value,
3052            }
3053            | Op::SetIndexUnchecked {
3054                collection,
3055                index,
3056                value,
3057            } => {
3058                let ptr = getb(reg, *collection);
3059                let idx = getb(reg, *index);
3060                let val = getb(reg, *value);
3061                let kadd = cells.len(); // 1-based: item i lives at heap[ptr + i]
3062                cells.push(LoopCell::Arith {
3063                    op: AluOp::Add,
3064                    a: ptr,
3065                    b: idx,
3066                });
3067                cells.push(LoopCell::HeapWrite {
3068                    addr: Src::Val(kadd),
3069                    val,
3070                });
3071            }
3072            // `Pop xs into v` — read the last element (heap[ptr + len]), then shrink the header to len-1.
3073            Op::ListPop { list, dst } => {
3074                let ptr = getb(reg, *list);
3075                let klen = cells.len();
3076                cells.push(LoopCell::HeapRead { addr: ptr }); // len = heap[ptr]
3077                let len = Src::Val(klen);
3078                let kaddr = cells.len(); // last element (1-based item `len`) at heap[ptr + len]
3079                cells.push(LoopCell::Arith {
3080                    op: AluOp::Add,
3081                    a: ptr,
3082                    b: len,
3083                });
3084                let kr = cells.len();
3085                cells.push(LoopCell::HeapRead {
3086                    addr: Src::Val(kaddr),
3087                }); // the popped value
3088                let kdec = cells.len();
3089                cells.push(LoopCell::Arith {
3090                    op: AluOp::Sub,
3091                    a: len,
3092                    b: Src::Const(1),
3093                });
3094                cells.push(LoopCell::HeapWrite {
3095                    addr: ptr,
3096                    val: Src::Val(kdec),
3097                }); // heap[ptr] = len - 1
3098                reg.insert(*dst, Src::Val(kr));
3099                COLL_LEN.with(|m| {
3100                    let mut m = m.borrow_mut();
3101                    if let Some(l) = m.get(list).copied() {
3102                        m.insert(*list, l - 1);
3103                    }
3104                });
3105            }
3106            // `Push v to ys` — grow a (top-of-heap) list: write the new element after the last, bump the len.
3107            Op::ListPush { list, value } => {
3108                let ptr = getb(reg, *list);
3109                let val = getb(reg, *value);
3110                let klen = cells.len();
3111                cells.push(LoopCell::HeapRead { addr: ptr }); // len = heap[ptr]
3112                let len = Src::Val(klen);
3113                let kp1 = cells.len();
3114                cells.push(LoopCell::Arith {
3115                    op: AluOp::Add,
3116                    a: ptr,
3117                    b: Src::Const(1),
3118                });
3119                let kaddr = cells.len(); // new element at heap[ptr + 1 + len]
3120                cells.push(LoopCell::Arith {
3121                    op: AluOp::Add,
3122                    a: Src::Val(kp1),
3123                    b: len,
3124                });
3125                cells.push(LoopCell::HeapWrite {
3126                    addr: Src::Val(kaddr),
3127                    val,
3128                });
3129                let knl = cells.len();
3130                cells.push(LoopCell::Arith {
3131                    op: AluOp::Add,
3132                    a: len,
3133                    b: Src::Const(1),
3134                });
3135                cells.push(LoopCell::HeapWrite {
3136                    addr: ptr,
3137                    val: Src::Val(knl),
3138                }); // heap[ptr] = len + 1
3139                COLL_LEN.with(|m| {
3140                    let mut m = m.borrow_mut();
3141                    if let Some(l) = m.get(list).copied() {
3142                        m.insert(*list, l + 1);
3143                    }
3144                });
3145            }
3146            // `Append v to xs` — for a LIST seq this is exactly `list_push` (the nested-Crdt seq case only
3147            // arises with an uncovered `NewCrdt`, which the fabric refuses first — so here `seq` is a list).
3148            Op::CrdtAppend { seq, value } => {
3149                let ptr = getb(reg, *seq);
3150                let val = getb(reg, *value);
3151                let klen = cells.len();
3152                cells.push(LoopCell::HeapRead { addr: ptr });
3153                let len = Src::Val(klen);
3154                let kp1 = cells.len();
3155                cells.push(LoopCell::Arith {
3156                    op: AluOp::Add,
3157                    a: ptr,
3158                    b: Src::Const(1),
3159                });
3160                let kaddr = cells.len();
3161                cells.push(LoopCell::Arith {
3162                    op: AluOp::Add,
3163                    a: Src::Val(kp1),
3164                    b: len,
3165                });
3166                cells.push(LoopCell::HeapWrite {
3167                    addr: Src::Val(kaddr),
3168                    val,
3169                });
3170                let knl = cells.len();
3171                cells.push(LoopCell::Arith {
3172                    op: AluOp::Add,
3173                    a: len,
3174                    b: Src::Const(1),
3175                });
3176                cells.push(LoopCell::HeapWrite {
3177                    addr: ptr,
3178                    val: Src::Val(knl),
3179                });
3180                COLL_LEN.with(|m| {
3181                    let mut m = m.borrow_mut();
3182                    if let Some(l) = m.get(seq).copied() {
3183                        m.insert(*seq, l + 1);
3184                    }
3185                });
3186            }
3187            // `Push v to s's field` — the struct field HOLDS a list pointer; read it, then push v to that list
3188            // (using the list's RUNTIME length, so no COLL_LEN needed for the nested list).
3189            Op::ListPushField { obj, field, src } => {
3190                let optr = getb(reg, *obj);
3191                let slot = struct_field_slot(*obj, *field);
3192                let val = getb(reg, *src);
3193                let kfa = cells.len(); // &field = optr + 1 + slot
3194                cells.push(LoopCell::Arith {
3195                    op: AluOp::Add,
3196                    a: optr,
3197                    b: Src::Const(slot as i64 + 1),
3198                });
3199                let klp = cells.len();
3200                cells.push(LoopCell::HeapRead {
3201                    addr: Src::Val(kfa),
3202                }); // the list pointer stored in the field
3203                let lptr = Src::Val(klp);
3204                let klen = cells.len();
3205                cells.push(LoopCell::HeapRead { addr: lptr }); // list len = heap[lptr]
3206                let len = Src::Val(klen);
3207                let kp1 = cells.len();
3208                cells.push(LoopCell::Arith {
3209                    op: AluOp::Add,
3210                    a: lptr,
3211                    b: Src::Const(1),
3212                });
3213                let kaddr = cells.len(); // new element at heap[lptr + 1 + len]
3214                cells.push(LoopCell::Arith {
3215                    op: AluOp::Add,
3216                    a: Src::Val(kp1),
3217                    b: len,
3218                });
3219                cells.push(LoopCell::HeapWrite {
3220                    addr: Src::Val(kaddr),
3221                    val,
3222                });
3223                let knl = cells.len();
3224                cells.push(LoopCell::Arith {
3225                    op: AluOp::Add,
3226                    a: len,
3227                    b: Src::Const(1),
3228                });
3229                cells.push(LoopCell::HeapWrite {
3230                    addr: lptr,
3231                    val: Src::Val(knl),
3232                }); // heap[lptr] = len + 1
3233            }
3234            // `xs contains v` — membership as an UNROLLED OR-scan over the collection's static length:
3235            // `OR_{i=0}^{L-1} (heap[ptr+1+i] == v)` yielding a 0/1 Bool. L (from COLL_LEN) is EXACT for the
3236            // straight-line construction the combinational heap model already requires, so no runtime-length
3237            // guard is needed. A collection whose length is not statically known is refused loudly.
3238            Op::Contains {
3239                dst,
3240                collection,
3241                value,
3242            } => {
3243                let ptr = getb(reg, *collection);
3244                let val = getb(reg, *value);
3245                let max = COLL_LEN
3246                    .with(|m| m.borrow().get(collection).copied())
3247                    .expect("fabric Contains: collection length not statically known (a runtime-length scan needs a sequential unit)");
3248                // Scan up to the STATIC max, but count only slots below the RUNTIME count (heap[ptr]). For a
3249                // list `count == max` (all pass); for a set after dedup/remove `count < max`, so the
3250                // uncounted tail slots are masked out — membership is exact for both.
3251                let klen = cells.len();
3252                cells.push(LoopCell::HeapRead { addr: ptr });
3253                let rlen = Src::Val(klen);
3254                let mut acc = Src::Const(0);
3255                for i in 0..max {
3256                    let kaddr = cells.len(); // 1-based item i+1 at heap[ptr + 1 + i]
3257                    cells.push(LoopCell::Arith {
3258                        op: AluOp::Add,
3259                        a: ptr,
3260                        b: Src::Const(i + 1),
3261                    });
3262                    let kelem = cells.len();
3263                    cells.push(LoopCell::HeapRead {
3264                        addr: Src::Val(kaddr),
3265                    });
3266                    let kg = cells.len(); // i < runtime_count ? 1 : 0
3267                    cells.push(LoopCell::Select {
3268                        cmp: CmpKind::Lt,
3269                        ca: Src::Const(i),
3270                        cb: rlen,
3271                        t: Src::Const(1),
3272                        e: Src::Const(0),
3273                    });
3274                    let keq = cells.len(); // elem == v ? 1 : 0
3275                    cells.push(LoopCell::Select {
3276                        cmp: CmpKind::Eq,
3277                        ca: Src::Val(kelem),
3278                        cb: val,
3279                        t: Src::Const(1),
3280                        e: Src::Const(0),
3281                    });
3282                    let kpres = cells.len(); // present = guard AND eq
3283                    cells.push(LoopCell::Arith {
3284                        op: AluOp::And,
3285                        a: Src::Val(kg),
3286                        b: Src::Val(keq),
3287                    });
3288                    let kor = cells.len();
3289                    cells.push(LoopCell::Arith {
3290                        op: AluOp::Or,
3291                        a: acc,
3292                        b: Src::Val(kpres),
3293                    });
3294                    acc = Src::Val(kor);
3295                }
3296                reg.insert(*dst, acc);
3297            }
3298            // `Add v to s` (SET) — DEDUP append. The set's live count is at heap[ptr] (RUNTIME, data-
3299            // dependent), bounded above by the static max (COLL_LEN = number of adds so far). Scan the
3300            // counted region for v — each element guarded by `i < runtime_count`; if absent, write v at the
3301            // top and bump the count. A duplicate lands beyond the count (uncounted, overwritten by the next
3302            // add), so the deduped LENGTH stays exact. A bounded unroll + runtime guards — combinational.
3303            Op::SetAdd { set, value } => {
3304                let ptr = getb(reg, *set);
3305                let val = getb(reg, *value);
3306                let max = COLL_LEN
3307                    .with(|m| m.borrow().get(set).copied())
3308                    .expect("fabric SetAdd: set max-length not tracked (build the set from a literal/empty)");
3309                let klen = cells.len();
3310                cells.push(LoopCell::HeapRead { addr: ptr }); // runtime count = heap[ptr]
3311                let rlen = Src::Val(klen);
3312                let mut found = Src::Const(0);
3313                for i in 0..max {
3314                    let kaddr = cells.len();
3315                    cells.push(LoopCell::Arith {
3316                        op: AluOp::Add,
3317                        a: ptr,
3318                        b: Src::Const(i + 1),
3319                    });
3320                    let kelem = cells.len();
3321                    cells.push(LoopCell::HeapRead {
3322                        addr: Src::Val(kaddr),
3323                    });
3324                    let kguard = cells.len(); // i < runtime_count ? 1 : 0
3325                    cells.push(LoopCell::Select {
3326                        cmp: CmpKind::Lt,
3327                        ca: Src::Const(i),
3328                        cb: rlen,
3329                        t: Src::Const(1),
3330                        e: Src::Const(0),
3331                    });
3332                    let keq = cells.len(); // elem == v ? 1 : 0
3333                    cells.push(LoopCell::Select {
3334                        cmp: CmpKind::Eq,
3335                        ca: Src::Val(kelem),
3336                        cb: val,
3337                        t: Src::Const(1),
3338                        e: Src::Const(0),
3339                    });
3340                    let kpres = cells.len(); // present = guard AND eq
3341                    cells.push(LoopCell::Arith {
3342                        op: AluOp::And,
3343                        a: Src::Val(kguard),
3344                        b: Src::Val(keq),
3345                    });
3346                    let kor = cells.len();
3347                    cells.push(LoopCell::Arith {
3348                        op: AluOp::Or,
3349                        a: found,
3350                        b: Src::Val(kpres),
3351                    });
3352                    found = Src::Val(kor);
3353                }
3354                // write v at heap[ptr + 1 + runtime_count] (uncounted if already present)
3355                let kp1 = cells.len();
3356                cells.push(LoopCell::Arith {
3357                    op: AluOp::Add,
3358                    a: ptr,
3359                    b: Src::Const(1),
3360                });
3361                let kwaddr = cells.len();
3362                cells.push(LoopCell::Arith {
3363                    op: AluOp::Add,
3364                    a: Src::Val(kp1),
3365                    b: rlen,
3366                });
3367                cells.push(LoopCell::HeapWrite {
3368                    addr: Src::Val(kwaddr),
3369                    val,
3370                });
3371                // new count = found ? runtime_count : runtime_count + 1
3372                let krlen1 = cells.len();
3373                cells.push(LoopCell::Arith {
3374                    op: AluOp::Add,
3375                    a: rlen,
3376                    b: Src::Const(1),
3377                });
3378                let knew = cells.len();
3379                cells.push(LoopCell::Select {
3380                    cmp: CmpKind::Eq,
3381                    ca: found,
3382                    cb: Src::Const(0),
3383                    t: Src::Val(krlen1),
3384                    e: rlen,
3385                });
3386                cells.push(LoopCell::HeapWrite {
3387                    addr: ptr,
3388                    val: Src::Val(knew),
3389                });
3390                COLL_LEN.with(|m| {
3391                    let mut m = m.borrow_mut();
3392                    m.insert(*set, max + 1); // the static max grows by one per add (regardless of dedup)
3393                });
3394            }
3395            // `Remove v from s` — COMPACTING removal of the (first/only, for a set) occurrence. A running
3396            // `past` flag turns on at the match and shifts every later slot down by one (`slot i ← slot i+1`);
3397            // the runtime count drops by one iff found. Read-ahead + write-current (increasing order) keeps
3398            // reads on the original values. `retain`-equivalent for a unique-element set.
3399            Op::RemoveFrom { collection, value } => {
3400                let ptr = getb(reg, *collection);
3401                let val = getb(reg, *value);
3402                let max = COLL_LEN
3403                    .with(|m| m.borrow().get(collection).copied())
3404                    .expect("fabric RemoveFrom: collection max-length not tracked");
3405                let klen = cells.len();
3406                cells.push(LoopCell::HeapRead { addr: ptr }); // runtime count = heap[ptr]
3407                let rlen = Src::Val(klen);
3408                let mut past = Src::Const(0);
3409                for i in 0..max {
3410                    let kaddr = cells.len(); // slot i address (heap[ptr + 1 + i])
3411                    cells.push(LoopCell::Arith {
3412                        op: AluOp::Add,
3413                        a: ptr,
3414                        b: Src::Const(i + 1),
3415                    });
3416                    let kelem = cells.len();
3417                    cells.push(LoopCell::HeapRead {
3418                        addr: Src::Val(kaddr),
3419                    });
3420                    let kg = cells.len(); // i < runtime_count ? 1 : 0
3421                    cells.push(LoopCell::Select {
3422                        cmp: CmpKind::Lt,
3423                        ca: Src::Const(i),
3424                        cb: rlen,
3425                        t: Src::Const(1),
3426                        e: Src::Const(0),
3427                    });
3428                    let ke = cells.len(); // elem == v ? 1 : 0
3429                    cells.push(LoopCell::Select {
3430                        cmp: CmpKind::Eq,
3431                        ca: Src::Val(kelem),
3432                        cb: val,
3433                        t: Src::Const(1),
3434                        e: Src::Const(0),
3435                    });
3436                    let km = cells.len(); // match = guard AND eq
3437                    cells.push(LoopCell::Arith {
3438                        op: AluOp::And,
3439                        a: Src::Val(kg),
3440                        b: Src::Val(ke),
3441                    });
3442                    let kpast = cells.len(); // past = past OR match (turns on at the match, stays on)
3443                    cells.push(LoopCell::Arith {
3444                        op: AluOp::Or,
3445                        a: past,
3446                        b: Src::Val(km),
3447                    });
3448                    past = Src::Val(kpast);
3449                    let kaddr2 = cells.len(); // slot i+1 address (heap[ptr + 2 + i]) — read-ahead, still original
3450                    cells.push(LoopCell::Arith {
3451                        op: AluOp::Add,
3452                        a: ptr,
3453                        b: Src::Const(i + 2),
3454                    });
3455                    let knext = cells.len();
3456                    cells.push(LoopCell::HeapRead {
3457                        addr: Src::Val(kaddr2),
3458                    });
3459                    let knew = cells.len(); // past ? next : elem
3460                    cells.push(LoopCell::Select {
3461                        cmp: CmpKind::Ne,
3462                        ca: past,
3463                        cb: Src::Const(0),
3464                        t: Src::Val(knext),
3465                        e: Src::Val(kelem),
3466                    });
3467                    cells.push(LoopCell::HeapWrite {
3468                        addr: Src::Val(kaddr),
3469                        val: Src::Val(knew),
3470                    });
3471                }
3472                let krlen1 = cells.len(); // count - 1
3473                cells.push(LoopCell::Arith {
3474                    op: AluOp::Sub,
3475                    a: rlen,
3476                    b: Src::Const(1),
3477                });
3478                let knewc = cells.len(); // new count = found ? count-1 : count
3479                cells.push(LoopCell::Select {
3480                    cmp: CmpKind::Ne,
3481                    ca: past,
3482                    cb: Src::Const(0),
3483                    t: Src::Val(krlen1),
3484                    e: rlen,
3485                });
3486                cells.push(LoopCell::HeapWrite {
3487                    addr: ptr,
3488                    val: Src::Val(knewc),
3489                });
3490            }
3491            // `s union t` — a FRESH set = lhs's elements then each rhs element NOT already in lhs (VM order).
3492            // Copy lhs, then for each rhs element run a nested lhs-membership scan and conditionally append at
3493            // the running write-count. Bounded (static maxes) → combinational.
3494            Op::UnionOp { dst, lhs, rhs } => {
3495                let lptr = getb(reg, *lhs);
3496                let rptr = getb(reg, *rhs);
3497                let lmax = COLL_LEN
3498                    .with(|m| m.borrow().get(lhs).copied())
3499                    .expect("fabric UnionOp: lhs max-length not tracked");
3500                let rmax = COLL_LEN
3501                    .with(|m| m.borrow().get(rhs).copied())
3502                    .expect("fabric UnionOp: rhs max-length not tracked");
3503                let klc = cells.len();
3504                cells.push(LoopCell::HeapRead { addr: lptr });
3505                let lcount = Src::Val(klc);
3506                let krc = cells.len();
3507                cells.push(LoopCell::HeapRead { addr: rptr });
3508                let rcount = Src::Val(krc);
3509                let ka = cells.len();
3510                cells.push(LoopCell::HeapAlloc {
3511                    size: Src::Const(1 + lmax + rmax),
3512                });
3513                let nptr = Src::Val(ka);
3514                // copy all lmax lhs slots (counted ones matter; the tail is overwritten by rhs appends)
3515                for i in 0..lmax {
3516                    let ksa = cells.len();
3517                    cells.push(LoopCell::Arith {
3518                        op: AluOp::Add,
3519                        a: lptr,
3520                        b: Src::Const(i + 1),
3521                    });
3522                    let ks = cells.len();
3523                    cells.push(LoopCell::HeapRead {
3524                        addr: Src::Val(ksa),
3525                    });
3526                    let kda = cells.len();
3527                    cells.push(LoopCell::Arith {
3528                        op: AluOp::Add,
3529                        a: nptr,
3530                        b: Src::Const(i + 1),
3531                    });
3532                    cells.push(LoopCell::HeapWrite {
3533                        addr: Src::Val(kda),
3534                        val: Src::Val(ks),
3535                    });
3536                }
3537                let mut new_count = lcount; // start after lhs's counted elements
3538                for j in 0..rmax {
3539                    let kra = cells.len();
3540                    cells.push(LoopCell::Arith {
3541                        op: AluOp::Add,
3542                        a: rptr,
3543                        b: Src::Const(j + 1),
3544                    });
3545                    let kre = cells.len();
3546                    cells.push(LoopCell::HeapRead {
3547                        addr: Src::Val(kra),
3548                    });
3549                    let rhs_elem = Src::Val(kre);
3550                    // in_lhs = OR_i ((i < lcount) AND (lhs[1+i] == rhs_elem))
3551                    let mut in_lhs = Src::Const(0);
3552                    for i in 0..lmax {
3553                        let ksa = cells.len();
3554                        cells.push(LoopCell::Arith {
3555                            op: AluOp::Add,
3556                            a: lptr,
3557                            b: Src::Const(i + 1),
3558                        });
3559                        let ks = cells.len();
3560                        cells.push(LoopCell::HeapRead {
3561                            addr: Src::Val(ksa),
3562                        });
3563                        let kg = cells.len();
3564                        cells.push(LoopCell::Select {
3565                            cmp: CmpKind::Lt,
3566                            ca: Src::Const(i),
3567                            cb: lcount,
3568                            t: Src::Const(1),
3569                            e: Src::Const(0),
3570                        });
3571                        let keq = cells.len();
3572                        cells.push(LoopCell::Select {
3573                            cmp: CmpKind::Eq,
3574                            ca: Src::Val(ks),
3575                            cb: rhs_elem,
3576                            t: Src::Const(1),
3577                            e: Src::Const(0),
3578                        });
3579                        let kp = cells.len();
3580                        cells.push(LoopCell::Arith {
3581                            op: AluOp::And,
3582                            a: Src::Val(kg),
3583                            b: Src::Val(keq),
3584                        });
3585                        let kor = cells.len();
3586                        cells.push(LoopCell::Arith {
3587                            op: AluOp::Or,
3588                            a: in_lhs,
3589                            b: Src::Val(kp),
3590                        });
3591                        in_lhs = Src::Val(kor);
3592                    }
3593                    let kgj = cells.len();
3594                    cells.push(LoopCell::Select {
3595                        cmp: CmpKind::Lt,
3596                        ca: Src::Const(j),
3597                        cb: rcount,
3598                        t: Src::Const(1),
3599                        e: Src::Const(0),
3600                    });
3601                    let knil = cells.len();
3602                    cells.push(LoopCell::Select {
3603                        cmp: CmpKind::Eq,
3604                        ca: in_lhs,
3605                        cb: Src::Const(0),
3606                        t: Src::Const(1),
3607                        e: Src::Const(0),
3608                    });
3609                    let kap = cells.len();
3610                    cells.push(LoopCell::Arith {
3611                        op: AluOp::And,
3612                        a: Src::Val(kgj),
3613                        b: Src::Val(knil),
3614                    });
3615                    let append = Src::Val(kap);
3616                    // write rhs_elem at new[1 + new_count] (uncounted if not appending)
3617                    let kwa1 = cells.len();
3618                    cells.push(LoopCell::Arith {
3619                        op: AluOp::Add,
3620                        a: nptr,
3621                        b: Src::Const(1),
3622                    });
3623                    let kwa2 = cells.len();
3624                    cells.push(LoopCell::Arith {
3625                        op: AluOp::Add,
3626                        a: Src::Val(kwa1),
3627                        b: new_count,
3628                    });
3629                    cells.push(LoopCell::HeapWrite {
3630                        addr: Src::Val(kwa2),
3631                        val: rhs_elem,
3632                    });
3633                    let knc1 = cells.len();
3634                    cells.push(LoopCell::Arith {
3635                        op: AluOp::Add,
3636                        a: new_count,
3637                        b: Src::Const(1),
3638                    });
3639                    let knc = cells.len();
3640                    cells.push(LoopCell::Select {
3641                        cmp: CmpKind::Ne,
3642                        ca: append,
3643                        cb: Src::Const(0),
3644                        t: Src::Val(knc1),
3645                        e: new_count,
3646                    });
3647                    new_count = Src::Val(knc);
3648                }
3649                cells.push(LoopCell::HeapWrite {
3650                    addr: nptr,
3651                    val: new_count,
3652                }); // header = union size
3653                reg.insert(*dst, nptr);
3654                COLL_LEN.with(|m| m.borrow_mut().insert(*dst, lmax + rmax));
3655            }
3656            // `s intersection t` — a FRESH set = lhs's elements that are ALSO in rhs (lhs order). Filter each
3657            // lhs element by a nested rhs-membership scan; keep it (append) iff present. Bounded → combinational.
3658            Op::IntersectOp { dst, lhs, rhs } => {
3659                let lptr = getb(reg, *lhs);
3660                let rptr = getb(reg, *rhs);
3661                let lmax = COLL_LEN
3662                    .with(|m| m.borrow().get(lhs).copied())
3663                    .expect("fabric IntersectOp: lhs max-length not tracked");
3664                let rmax = COLL_LEN
3665                    .with(|m| m.borrow().get(rhs).copied())
3666                    .expect("fabric IntersectOp: rhs max-length not tracked");
3667                let klc = cells.len();
3668                cells.push(LoopCell::HeapRead { addr: lptr });
3669                let lcount = Src::Val(klc);
3670                let krc = cells.len();
3671                cells.push(LoopCell::HeapRead { addr: rptr });
3672                let rcount = Src::Val(krc);
3673                let ka = cells.len();
3674                cells.push(LoopCell::HeapAlloc {
3675                    size: Src::Const(1 + lmax),
3676                }); // intersection ⊆ lhs
3677                let nptr = Src::Val(ka);
3678                let mut new_count = Src::Const(0);
3679                for i in 0..lmax {
3680                    let ksa = cells.len();
3681                    cells.push(LoopCell::Arith {
3682                        op: AluOp::Add,
3683                        a: lptr,
3684                        b: Src::Const(i + 1),
3685                    });
3686                    let ks = cells.len();
3687                    cells.push(LoopCell::HeapRead {
3688                        addr: Src::Val(ksa),
3689                    });
3690                    let lhs_elem = Src::Val(ks);
3691                    // in_rhs = OR_j ((j < rcount) AND (rhs[1+j] == lhs_elem))
3692                    let mut in_rhs = Src::Const(0);
3693                    for j in 0..rmax {
3694                        let kra = cells.len();
3695                        cells.push(LoopCell::Arith {
3696                            op: AluOp::Add,
3697                            a: rptr,
3698                            b: Src::Const(j + 1),
3699                        });
3700                        let kr = cells.len();
3701                        cells.push(LoopCell::HeapRead {
3702                            addr: Src::Val(kra),
3703                        });
3704                        let kg = cells.len();
3705                        cells.push(LoopCell::Select {
3706                            cmp: CmpKind::Lt,
3707                            ca: Src::Const(j),
3708                            cb: rcount,
3709                            t: Src::Const(1),
3710                            e: Src::Const(0),
3711                        });
3712                        let keq = cells.len();
3713                        cells.push(LoopCell::Select {
3714                            cmp: CmpKind::Eq,
3715                            ca: Src::Val(kr),
3716                            cb: lhs_elem,
3717                            t: Src::Const(1),
3718                            e: Src::Const(0),
3719                        });
3720                        let kp = cells.len();
3721                        cells.push(LoopCell::Arith {
3722                            op: AluOp::And,
3723                            a: Src::Val(kg),
3724                            b: Src::Val(keq),
3725                        });
3726                        let kor = cells.len();
3727                        cells.push(LoopCell::Arith {
3728                            op: AluOp::Or,
3729                            a: in_rhs,
3730                            b: Src::Val(kp),
3731                        });
3732                        in_rhs = Src::Val(kor);
3733                    }
3734                    let kgi = cells.len();
3735                    cells.push(LoopCell::Select {
3736                        cmp: CmpKind::Lt,
3737                        ca: Src::Const(i),
3738                        cb: lcount,
3739                        t: Src::Const(1),
3740                        e: Src::Const(0),
3741                    });
3742                    let kkeep = cells.len();
3743                    cells.push(LoopCell::Arith {
3744                        op: AluOp::And,
3745                        a: Src::Val(kgi),
3746                        b: in_rhs,
3747                    });
3748                    let keep = Src::Val(kkeep);
3749                    // write lhs_elem at new[1 + new_count] (uncounted if not kept)
3750                    let kwa1 = cells.len();
3751                    cells.push(LoopCell::Arith {
3752                        op: AluOp::Add,
3753                        a: nptr,
3754                        b: Src::Const(1),
3755                    });
3756                    let kwa2 = cells.len();
3757                    cells.push(LoopCell::Arith {
3758                        op: AluOp::Add,
3759                        a: Src::Val(kwa1),
3760                        b: new_count,
3761                    });
3762                    cells.push(LoopCell::HeapWrite {
3763                        addr: Src::Val(kwa2),
3764                        val: lhs_elem,
3765                    });
3766                    let knc1 = cells.len();
3767                    cells.push(LoopCell::Arith {
3768                        op: AluOp::Add,
3769                        a: new_count,
3770                        b: Src::Const(1),
3771                    });
3772                    let knc = cells.len();
3773                    cells.push(LoopCell::Select {
3774                        cmp: CmpKind::Ne,
3775                        ca: keep,
3776                        cb: Src::Const(0),
3777                        t: Src::Val(knc1),
3778                        e: new_count,
3779                    });
3780                    new_count = Src::Val(knc);
3781                }
3782                cells.push(LoopCell::HeapWrite {
3783                    addr: nptr,
3784                    val: new_count,
3785                }); // header = intersection size
3786                reg.insert(*dst, nptr);
3787                COLL_LEN.with(|m| m.borrow_mut().insert(*dst, lmax));
3788            }
3789            // `Concat` — string interpolation's builder: render each operand to a char-list (text: its own;
3790            // int: `push_int_to_decimal`) and concatenate into a fresh TEXT region. Runtime lengths, so the
3791            // rhs copy lands at the runtime offset `llen`; the lhs copy's tail junk is overwritten by it.
3792            Op::Concat { dst, lhs, rhs } => {
3793                let (lptr, lmax) = {
3794                    let is_text = TEXT_REGS.with(|t| t.borrow().contains(lhs));
3795                    if is_text {
3796                        (
3797                            getb(reg, *lhs),
3798                            COLL_LEN.with(|m| m.borrow().get(lhs).copied().unwrap_or(0)),
3799                        )
3800                    } else {
3801                        (push_int_to_decimal(cells, getb(reg, *lhs)), 20)
3802                    }
3803                };
3804                let (rptr, rmax) = {
3805                    let is_text = TEXT_REGS.with(|t| t.borrow().contains(rhs));
3806                    if is_text {
3807                        (
3808                            getb(reg, *rhs),
3809                            COLL_LEN.with(|m| m.borrow().get(rhs).copied().unwrap_or(0)),
3810                        )
3811                    } else {
3812                        (push_int_to_decimal(cells, getb(reg, *rhs)), 20)
3813                    }
3814                };
3815                let kll = cells.len();
3816                cells.push(LoopCell::HeapRead { addr: lptr });
3817                let llen = Src::Val(kll);
3818                let krl = cells.len();
3819                cells.push(LoopCell::HeapRead { addr: rptr });
3820                let rlen = Src::Val(krl);
3821                let ka = cells.len();
3822                cells.push(LoopCell::HeapAlloc {
3823                    size: Src::Const(1 + lmax + rmax),
3824                });
3825                let nptr = Src::Val(ka);
3826                for i in 0..lmax {
3827                    let ksa = cells.len();
3828                    cells.push(LoopCell::Arith {
3829                        op: AluOp::Add,
3830                        a: lptr,
3831                        b: Src::Const(i + 1),
3832                    });
3833                    let ks = cells.len();
3834                    cells.push(LoopCell::HeapRead {
3835                        addr: Src::Val(ksa),
3836                    });
3837                    let kda = cells.len();
3838                    cells.push(LoopCell::Arith {
3839                        op: AluOp::Add,
3840                        a: nptr,
3841                        b: Src::Const(i + 1),
3842                    });
3843                    cells.push(LoopCell::HeapWrite {
3844                        addr: Src::Val(kda),
3845                        val: Src::Val(ks),
3846                    });
3847                }
3848                for j in 0..rmax {
3849                    let ksa = cells.len();
3850                    cells.push(LoopCell::Arith {
3851                        op: AluOp::Add,
3852                        a: rptr,
3853                        b: Src::Const(j + 1),
3854                    });
3855                    let ks = cells.len();
3856                    cells.push(LoopCell::HeapRead {
3857                        addr: Src::Val(ksa),
3858                    });
3859                    let kd1 = cells.len();
3860                    cells.push(LoopCell::Arith {
3861                        op: AluOp::Add,
3862                        a: nptr,
3863                        b: Src::Const(j + 1),
3864                    });
3865                    let kd2 = cells.len();
3866                    cells.push(LoopCell::Arith {
3867                        op: AluOp::Add,
3868                        a: Src::Val(kd1),
3869                        b: llen,
3870                    });
3871                    cells.push(LoopCell::HeapWrite {
3872                        addr: Src::Val(kd2),
3873                        val: Src::Val(ks),
3874                    });
3875                }
3876                let knl = cells.len();
3877                cells.push(LoopCell::Arith {
3878                    op: AluOp::Add,
3879                    a: llen,
3880                    b: rlen,
3881                });
3882                cells.push(LoopCell::HeapWrite {
3883                    addr: nptr,
3884                    val: Src::Val(knl),
3885                });
3886                reg.insert(*dst, nptr);
3887                TEXT_REGS.with(|t| {
3888                    t.borrow_mut().insert(*dst);
3889                });
3890                COLL_LEN.with(|m| {
3891                    m.borrow_mut().insert(*dst, lmax + rmax);
3892                });
3893            }
3894            // `FormatValue` — render a value to TEXT. The DEFAULT case (no `spec`, no debug prefix) is exactly
3895            // `to_display_string`: a Text value formats to itself, a scalar renders int→decimal (the same
3896            // `push_int_to_decimal` interpolation uses). Format-SPECS (width/precision/fill) are a follow-on
3897            // (they matter for floats/alignment) — the fabric refuses them loudly rather than misformat.
3898            Op::FormatValue {
3899                dst,
3900                src,
3901                spec,
3902                debug_prefix,
3903            } => {
3904                assert!(
3905                    *spec == u32::MAX && *debug_prefix == u32::MAX,
3906                    "fabric FormatValue: format-specs / debug prefixes are a follow-on (default rendering only)"
3907                );
3908                if TEXT_REGS.with(|t| t.borrow().contains(src)) {
3909                    let s = getb(reg, *src); // already Text — formats to itself
3910                    reg.insert(*dst, s);
3911                    TEXT_REGS.with(|t| {
3912                        t.borrow_mut().insert(*dst);
3913                    });
3914                    COLL_LEN.with(|m| {
3915                        let mut m = m.borrow_mut();
3916                        if let Some(l) = m.get(src).copied() {
3917                            m.insert(*dst, l);
3918                        }
3919                    });
3920                } else {
3921                    let ptr = push_int_to_decimal(cells, getb(reg, *src)); // scalar → decimal char-list
3922                    reg.insert(*dst, ptr);
3923                    TEXT_REGS.with(|t| {
3924                        t.borrow_mut().insert(*dst);
3925                    });
3926                }
3927            }
3928            // A channel is a heap FIFO `[read_cursor, count, e0, e1, ...]`. For SEQUENTIAL (spawn-free) use —
3929            // sends before recvs, in the straight-line flow — this reproduces the scheduler's VecDeque exactly
3930            // (a well-formed program never hits the empty-parking / cross-task-handoff paths). Bounded depth,
3931            // like a real hardware FIFO. (Concurrent COORDINATION — Spawn/Select/Await — stays uncovered.)
3932            Op::ChanNew { dst, .. } => {
3933                let ka = cells.len();
3934                cells.push(LoopCell::HeapAlloc {
3935                    size: Src::Const(2 + 64),
3936                }); // read_cursor, count, + depth 64
3937                let ptr = Src::Val(ka);
3938                cells.push(LoopCell::HeapWrite {
3939                    addr: ptr,
3940                    val: Src::Const(0),
3941                }); // read_cursor = 0
3942                let kh1 = cells.len();
3943                cells.push(LoopCell::Arith {
3944                    op: AluOp::Add,
3945                    a: ptr,
3946                    b: Src::Const(1),
3947                });
3948                cells.push(LoopCell::HeapWrite {
3949                    addr: Src::Val(kh1),
3950                    val: Src::Const(0),
3951                }); // count = 0
3952                reg.insert(*dst, ptr);
3953            }
3954            // `Send v into ch` — enqueue at heap[ptr + 2 + count], then bump the count.
3955            Op::ChanSend { chan, val } => {
3956                let ptr = getb(reg, *chan);
3957                let v = getb(reg, *val);
3958                let kca = cells.len(); // &count = ptr + 1
3959                cells.push(LoopCell::Arith {
3960                    op: AluOp::Add,
3961                    a: ptr,
3962                    b: Src::Const(1),
3963                });
3964                let kc = cells.len();
3965                cells.push(LoopCell::HeapRead {
3966                    addr: Src::Val(kca),
3967                });
3968                let count = Src::Val(kc);
3969                let kp2 = cells.len(); // elem address = ptr + 2 + count
3970                cells.push(LoopCell::Arith {
3971                    op: AluOp::Add,
3972                    a: ptr,
3973                    b: Src::Const(2),
3974                });
3975                let kaddr = cells.len();
3976                cells.push(LoopCell::Arith {
3977                    op: AluOp::Add,
3978                    a: Src::Val(kp2),
3979                    b: count,
3980                });
3981                cells.push(LoopCell::HeapWrite {
3982                    addr: Src::Val(kaddr),
3983                    val: v,
3984                });
3985                let knc = cells.len();
3986                cells.push(LoopCell::Arith {
3987                    op: AluOp::Add,
3988                    a: count,
3989                    b: Src::Const(1),
3990                });
3991                cells.push(LoopCell::HeapWrite {
3992                    addr: Src::Val(kca),
3993                    val: Src::Val(knc),
3994                }); // count += 1
3995            }
3996            // `Receive x from ch` — dequeue at heap[ptr + 2 + read_cursor], then advance the cursor.
3997            // `ChanRecv` dequeues the FIFO's front element. `ChanTryRecv` on a NON-EMPTY channel returns that
3998            // same value (the FIFO-model contract — the empty→Nothing case is the straight-line model's
3999            // non-empty assumption, exactly as `ChanRecv` assumes a value is present).
4000            Op::ChanRecv { dst, chan } | Op::ChanTryRecv { dst, chan } => {
4001                let ptr = getb(reg, *chan);
4002                let krc = cells.len();
4003                cells.push(LoopCell::HeapRead { addr: ptr }); // read_cursor = heap[ptr]
4004                let rc = Src::Val(krc);
4005                let kp2 = cells.len(); // elem address = ptr + 2 + read_cursor
4006                cells.push(LoopCell::Arith {
4007                    op: AluOp::Add,
4008                    a: ptr,
4009                    b: Src::Const(2),
4010                });
4011                let kaddr = cells.len();
4012                cells.push(LoopCell::Arith {
4013                    op: AluOp::Add,
4014                    a: Src::Val(kp2),
4015                    b: rc,
4016                });
4017                let kv = cells.len();
4018                cells.push(LoopCell::HeapRead {
4019                    addr: Src::Val(kaddr),
4020                });
4021                let knc = cells.len();
4022                cells.push(LoopCell::Arith {
4023                    op: AluOp::Add,
4024                    a: rc,
4025                    b: Src::Const(1),
4026                });
4027                cells.push(LoopCell::HeapWrite {
4028                    addr: ptr,
4029                    val: Src::Val(knc),
4030                }); // read_cursor += 1
4031                reg.insert(*dst, Src::Val(kv));
4032            }
4033            // `Try send v into ch` — non-blocking send: always succeeds on the depth-bounded FIFO, so it
4034            // enqueues exactly like `ChanSend` and reports `true`.
4035            Op::ChanTrySend { dst, chan, val } => {
4036                let ptr = getb(reg, *chan);
4037                let v = getb(reg, *val);
4038                let kca = cells.len();
4039                cells.push(LoopCell::Arith {
4040                    op: AluOp::Add,
4041                    a: ptr,
4042                    b: Src::Const(1),
4043                });
4044                let kc = cells.len();
4045                cells.push(LoopCell::HeapRead {
4046                    addr: Src::Val(kca),
4047                });
4048                let count = Src::Val(kc);
4049                let kp2 = cells.len();
4050                cells.push(LoopCell::Arith {
4051                    op: AluOp::Add,
4052                    a: ptr,
4053                    b: Src::Const(2),
4054                });
4055                let kaddr = cells.len();
4056                cells.push(LoopCell::Arith {
4057                    op: AluOp::Add,
4058                    a: Src::Val(kp2),
4059                    b: count,
4060                });
4061                cells.push(LoopCell::HeapWrite {
4062                    addr: Src::Val(kaddr),
4063                    val: v,
4064                });
4065                let knc = cells.len();
4066                cells.push(LoopCell::Arith {
4067                    op: AluOp::Add,
4068                    a: count,
4069                    b: Src::Const(1),
4070                });
4071                cells.push(LoopCell::HeapWrite {
4072                    addr: Src::Val(kca),
4073                    val: Src::Val(knc),
4074                });
4075                reg.insert(*dst, Src::Const(1)); // always succeeds (Bool true)
4076            }
4077            // `ChanClose` — mark a channel closed. It is OUTPUT-TRANSPARENT on the deterministic scheduler
4078            // (already-buffered values stay receivable; only a recv on an empty+closed channel would see the
4079            // close, a path the straight-line FIFO model does not take), so on the combinational fabric it is
4080            // a structural no-op — exactly as `rtl.rs`'s hand-bytecode oracle proves (Show is unaffected).
4081            Op::ChanClose { .. } => {}
4082            // OFFLINE networking (single node, no relay) — the deterministic model the AOT already proves
4083            // (tw == VM == AOT). Connect/Stream have no observable effect; `NetSync` is offline-identity (the
4084            // local CRDT value stands, so `dst` keeps what it already holds → no cell).
4085            Op::NetConnect { .. } | Op::NetStream { .. } | Op::NetSync { .. } => {}
4086            // `NetListen` ESTABLISHES the loopback inbox — a FIFO (same shape as a channel). Idempotent.
4087            Op::NetListen { .. } => {
4088                if NET_INBOX.with(|n| n.borrow().is_none()) {
4089                    let ka = cells.len();
4090                    cells.push(LoopCell::HeapAlloc {
4091                        size: Src::Const(2 + 64),
4092                    });
4093                    let ptr = Src::Val(ka);
4094                    cells.push(LoopCell::HeapWrite {
4095                        addr: ptr,
4096                        val: Src::Const(0),
4097                    }); // read_cursor = 0
4098                    let kh1 = cells.len();
4099                    cells.push(LoopCell::Arith {
4100                        op: AluOp::Add,
4101                        a: ptr,
4102                        b: Src::Const(1),
4103                    });
4104                    cells.push(LoopCell::HeapWrite {
4105                        addr: Src::Val(kh1),
4106                        val: Src::Const(0),
4107                    }); // count = 0
4108                    NET_INBOX.with(|n| *n.borrow_mut() = Some(ptr));
4109                }
4110            }
4111            // `NetSend` — the offline loopback PUBLISH: enqueue the message into the inbox (like ChanTrySend).
4112            // Output-transparent until an `Await` drains it; the peer/topic key is ignored (single node).
4113            Op::NetSend { msg, .. } => {
4114                if let Some(ptr) = NET_INBOX.with(|n| *n.borrow()) {
4115                    let v = getb(reg, *msg);
4116                    let kca = cells.len(); // count at ptr+1
4117                    cells.push(LoopCell::Arith {
4118                        op: AluOp::Add,
4119                        a: ptr,
4120                        b: Src::Const(1),
4121                    });
4122                    let kc = cells.len();
4123                    cells.push(LoopCell::HeapRead {
4124                        addr: Src::Val(kca),
4125                    });
4126                    let count = Src::Val(kc);
4127                    let kp2 = cells.len(); // element addr = ptr + 2 + count
4128                    cells.push(LoopCell::Arith {
4129                        op: AluOp::Add,
4130                        a: ptr,
4131                        b: Src::Const(2),
4132                    });
4133                    let kaddr = cells.len();
4134                    cells.push(LoopCell::Arith {
4135                        op: AluOp::Add,
4136                        a: Src::Val(kp2),
4137                        b: count,
4138                    });
4139                    cells.push(LoopCell::HeapWrite {
4140                        addr: Src::Val(kaddr),
4141                        val: v,
4142                    });
4143                    let knc = cells.len();
4144                    cells.push(LoopCell::Arith {
4145                        op: AluOp::Add,
4146                        a: count,
4147                        b: Src::Const(1),
4148                    });
4149                    cells.push(LoopCell::HeapWrite {
4150                        addr: Src::Val(kca),
4151                        val: Src::Val(knc),
4152                    }); // count += 1
4153                }
4154            }
4155            // `NetAwait` — DRAIN the loopback inbox (like ChanRecv): dequeue the front message into `dst`.
4156            Op::NetAwait { dst, .. } => {
4157                let ptr = NET_INBOX
4158                    .with(|n| *n.borrow())
4159                    .expect("fabric NetAwait: no inbox — an Await needs a prior Listen");
4160                let krc = cells.len();
4161                cells.push(LoopCell::HeapRead { addr: ptr }); // read_cursor = heap[ptr]
4162                let rc = Src::Val(krc);
4163                let kp2 = cells.len(); // elem addr = ptr + 2 + read_cursor
4164                cells.push(LoopCell::Arith {
4165                    op: AluOp::Add,
4166                    a: ptr,
4167                    b: Src::Const(2),
4168                });
4169                let kaddr = cells.len();
4170                cells.push(LoopCell::Arith {
4171                    op: AluOp::Add,
4172                    a: Src::Val(kp2),
4173                    b: rc,
4174                });
4175                let kv = cells.len();
4176                cells.push(LoopCell::HeapRead {
4177                    addr: Src::Val(kaddr),
4178                });
4179                let knc = cells.len();
4180                cells.push(LoopCell::Arith {
4181                    op: AluOp::Add,
4182                    a: rc,
4183                    b: Src::Const(1),
4184                });
4185                cells.push(LoopCell::HeapWrite {
4186                    addr: ptr,
4187                    val: Src::Val(knc),
4188                }); // read_cursor += 1
4189                reg.insert(*dst, Src::Val(kv));
4190            }
4191            // `NetMakePeer` — an opaque peer HANDLE derived from an address; offline it is only ever passed
4192            // to send/await (which ignore the key on the single node), so it wires through from its address.
4193            Op::NetMakePeer { dst, addr } => {
4194                let a = getb(reg, *addr);
4195                reg.insert(*dst, a);
4196            }
4197            // `select` — register each arm (a `Receive` binds `(channel, var)`, a timeout binds nothing); the
4198            // `SelectWait` below resolves them. No cell — just recorded pending state, in declaration order.
4199            Op::SelectArmRecv { chan, var } => {
4200                let ch = getb(reg, *chan);
4201                SELECT_ARMS.with(|a| a.borrow_mut().push(Some((ch, *var))));
4202            }
4203            Op::SelectArmTimeout { .. } => {
4204                SELECT_ARMS.with(|a| a.borrow_mut().push(None));
4205            }
4206            // `SelectWait` — the PRIORITY SELECT: the winner is the FIRST arm whose channel is non-empty
4207            // (`count > read_cursor`), else the timeout arm. `dst_arm` gets the winner's index; ONLY the
4208            // winner dequeues (a guarded `read_cursor += 1`), and each recv arm's `var` is wired to its front
4209            // value (dead for the losers — the dispatch runs only the winner's body). Combinational.
4210            Op::SelectWait { dst_arm } => {
4211                let arms = SELECT_ARMS.with(|a| std::mem::take(&mut *a.borrow_mut()));
4212                // Per recv arm: (ptr, read_cursor, ready, front_value, var).
4213                let mut info: Vec<Option<(Src, Src, Src, Src, u16)>> = Vec::new();
4214                for arm in &arms {
4215                    match arm {
4216                        Some((ptr, var)) => {
4217                            let ptr = *ptr;
4218                            let krc = cells.len();
4219                            cells.push(LoopCell::HeapRead { addr: ptr }); // read_cursor
4220                            let rc = Src::Val(krc);
4221                            let kca = cells.len();
4222                            cells.push(LoopCell::Arith {
4223                                op: AluOp::Add,
4224                                a: ptr,
4225                                b: Src::Const(1),
4226                            });
4227                            let kc = cells.len();
4228                            cells.push(LoopCell::HeapRead {
4229                                addr: Src::Val(kca),
4230                            }); // count
4231                            let krdy = cells.len(); // ready = count > read_cursor
4232                            cells.push(LoopCell::Select {
4233                                cmp: CmpKind::Gt,
4234                                ca: Src::Val(kc),
4235                                cb: rc,
4236                                t: Src::Const(1),
4237                                e: Src::Const(0),
4238                            });
4239                            let kp2 = cells.len();
4240                            cells.push(LoopCell::Arith {
4241                                op: AluOp::Add,
4242                                a: ptr,
4243                                b: Src::Const(2),
4244                            });
4245                            let kaddr = cells.len();
4246                            cells.push(LoopCell::Arith {
4247                                op: AluOp::Add,
4248                                a: Src::Val(kp2),
4249                                b: rc,
4250                            });
4251                            let kval = cells.len();
4252                            cells.push(LoopCell::HeapRead {
4253                                addr: Src::Val(kaddr),
4254                            }); // front value
4255                            info.push(Some((ptr, rc, Src::Val(krdy), Src::Val(kval), *var)));
4256                        }
4257                        None => info.push(None),
4258                    }
4259                }
4260                // winner index: build from the back so earlier arms take priority; default = the timeout arm
4261                // (or arm 0 if none — the witness always makes a recv ready, so that fallback is never hit).
4262                let timeout_idx = arms.iter().position(Option::is_none).map(|i| i as i64);
4263                let mut winner = Src::Const(timeout_idx.unwrap_or(0));
4264                for (i, arm) in info.iter().enumerate().rev() {
4265                    if let Some((_, _, ready, _, _)) = arm {
4266                        let kw = cells.len();
4267                        cells.push(LoopCell::Select {
4268                            cmp: CmpKind::Ne,
4269                            ca: *ready,
4270                            cb: Src::Const(0),
4271                            t: Src::Const(i as i64),
4272                            e: winner,
4273                        });
4274                        winner = Src::Val(kw);
4275                    }
4276                }
4277                reg.insert(*dst_arm, winner);
4278                // deliver each recv arm's front value to its var; guarded dequeue (only the winner advances).
4279                for (i, arm) in info.iter().enumerate() {
4280                    if let Some((ptr, rc, _, val, var)) = arm {
4281                        reg.insert(*var, *val);
4282                        let kwon = cells.len(); // won = (winner == i)
4283                        cells.push(LoopCell::Select {
4284                            cmp: CmpKind::Eq,
4285                            ca: winner,
4286                            cb: Src::Const(i as i64),
4287                            t: Src::Const(1),
4288                            e: Src::Const(0),
4289                        });
4290                        let krc1 = cells.len();
4291                        cells.push(LoopCell::Arith {
4292                            op: AluOp::Add,
4293                            a: *rc,
4294                            b: Src::Const(1),
4295                        });
4296                        let knew = cells.len(); // new read_cursor = won ? rc+1 : rc
4297                        cells.push(LoopCell::Select {
4298                            cmp: CmpKind::Ne,
4299                            ca: Src::Val(kwon),
4300                            cb: Src::Const(0),
4301                            t: Src::Val(krc1),
4302                            e: *rc,
4303                        });
4304                        cells.push(LoopCell::HeapWrite {
4305                            addr: *ptr,
4306                            val: Src::Val(knew),
4307                        });
4308                    }
4309                }
4310            }
4311            // `EnsureOwned` — the call-site COW barrier: before a mutable-borrow callee mutates a collection,
4312            // isolate it so the write can't leak to an ALIAS (`Let b be a. grow(a). b unchanged`). ALWAYS
4313            // cloning is observably identical to the VM's clone-iff-shared: an extra copy of a unique
4314            // collection is invisible (LOGOS has no pointer identity), and rewiring `reg` to the copy is
4315            // correct either way. A scalar (no tracked length) has no aliasing → no-op.
4316            Op::EnsureOwned { reg: r } => {
4317                if let Some(len) = COLL_LEN.with(|m| m.borrow().get(r).copied()) {
4318                    let sptr = getb(reg, *r);
4319                    let ka = cells.len();
4320                    cells.push(LoopCell::HeapAlloc {
4321                        size: Src::Const(len + 1),
4322                    });
4323                    let dptr = Src::Val(ka);
4324                    cells.push(LoopCell::HeapWrite {
4325                        addr: dptr,
4326                        val: Src::Const(len),
4327                    }); // length header
4328                    for i in 0..len {
4329                        let ksa = cells.len();
4330                        cells.push(LoopCell::Arith {
4331                            op: AluOp::Add,
4332                            a: sptr,
4333                            b: Src::Const(i + 1),
4334                        });
4335                        let ks = cells.len();
4336                        cells.push(LoopCell::HeapRead {
4337                            addr: Src::Val(ksa),
4338                        });
4339                        let kda = cells.len();
4340                        cells.push(LoopCell::Arith {
4341                            op: AluOp::Add,
4342                            a: dptr,
4343                            b: Src::Const(i + 1),
4344                        });
4345                        cells.push(LoopCell::HeapWrite {
4346                            addr: Src::Val(kda),
4347                            val: Src::Val(ks),
4348                        });
4349                    }
4350                    reg.insert(*r, dptr); // rewire to the owned copy
4351                }
4352            }
4353            // `copy of xs` — DEEP CLONE into a FRESH heap region: a mutation of the clone can't reach the
4354            // original (value semantics). Unrolled element copy over the static length `[len, e0..]`.
4355            Op::DeepClone { dst, src } => {
4356                let sptr = getb(reg, *src);
4357                let len = COLL_LEN
4358                    .with(|m| m.borrow().get(src).copied())
4359                    .expect("fabric DeepClone: source length not statically known");
4360                let ka = cells.len();
4361                cells.push(LoopCell::HeapAlloc {
4362                    size: Src::Const(len + 1),
4363                });
4364                let dptr = Src::Val(ka);
4365                cells.push(LoopCell::HeapWrite {
4366                    addr: dptr,
4367                    val: Src::Const(len),
4368                }); // length header
4369                for i in 0..len {
4370                    let ksa = cells.len(); // src item i+1 at heap[sptr + 1 + i]
4371                    cells.push(LoopCell::Arith {
4372                        op: AluOp::Add,
4373                        a: sptr,
4374                        b: Src::Const(i + 1),
4375                    });
4376                    let ks = cells.len();
4377                    cells.push(LoopCell::HeapRead {
4378                        addr: Src::Val(ksa),
4379                    });
4380                    let kda = cells.len(); // dst item i+1 at heap[dptr + 1 + i]
4381                    cells.push(LoopCell::Arith {
4382                        op: AluOp::Add,
4383                        a: dptr,
4384                        b: Src::Const(i + 1),
4385                    });
4386                    cells.push(LoopCell::HeapWrite {
4387                        addr: Src::Val(kda),
4388                        val: Src::Val(ks),
4389                    });
4390                }
4391                reg.insert(*dst, dptr);
4392                COLL_LEN.with(|m| m.borrow_mut().insert(*dst, len));
4393            }
4394            // `xs followed by ys` — SEQUENCE CONCATENATION into a fresh `[len_l+len_r, xs.., ys..]` region.
4395            // Both operand lengths are statically known, so the two element copies unroll.
4396            Op::SeqConcat { dst, lhs, rhs } => {
4397                let lptr = getb(reg, *lhs);
4398                let rptr = getb(reg, *rhs);
4399                let ll = COLL_LEN
4400                    .with(|m| m.borrow().get(lhs).copied())
4401                    .expect("fabric SeqConcat: left length not statically known");
4402                let lr = COLL_LEN
4403                    .with(|m| m.borrow().get(rhs).copied())
4404                    .expect("fabric SeqConcat: right length not statically known");
4405                let total = ll + lr;
4406                let ka = cells.len();
4407                cells.push(LoopCell::HeapAlloc {
4408                    size: Src::Const(total + 1),
4409                });
4410                let dptr = Src::Val(ka);
4411                cells.push(LoopCell::HeapWrite {
4412                    addr: dptr,
4413                    val: Src::Const(total),
4414                });
4415                // copy lhs → dst[0..ll], then rhs → dst[ll..ll+lr]
4416                for (base_ptr, off, count) in [(lptr, 0i64, ll), (rptr, ll, lr)] {
4417                    for i in 0..count {
4418                        let ksa = cells.len();
4419                        cells.push(LoopCell::Arith {
4420                            op: AluOp::Add,
4421                            a: base_ptr,
4422                            b: Src::Const(i + 1),
4423                        });
4424                        let ks = cells.len();
4425                        cells.push(LoopCell::HeapRead {
4426                            addr: Src::Val(ksa),
4427                        });
4428                        let kda = cells.len();
4429                        cells.push(LoopCell::Arith {
4430                            op: AluOp::Add,
4431                            a: dptr,
4432                            b: Src::Const(off + i + 1),
4433                        });
4434                        cells.push(LoopCell::HeapWrite {
4435                            addr: Src::Val(kda),
4436                            val: Src::Val(ks),
4437                        });
4438                    }
4439                }
4440                reg.insert(*dst, dptr);
4441                COLL_LEN.with(|m| m.borrow_mut().insert(*dst, total));
4442            }
4443            // `items s through e of xs` — SLICE into a fresh region. VM: 1-based `s` → 0-based
4444            // `(s-1).saturating`, valid iff `start0 < e && e <= len`, result `v[start0 ..= e-1]` (count
4445            // `e - start0`) else empty. Const bounds unroll; a runtime bound is refused (needs a seq unit).
4446            Op::SliceOp {
4447                dst,
4448                collection,
4449                start,
4450                end,
4451            } => {
4452                let cptr = getb(reg, *collection);
4453                let len = COLL_LEN
4454                    .with(|m| m.borrow().get(collection).copied())
4455                    .expect("fabric SliceOp: collection length not statically known");
4456                let s = match getb(reg, *start) {
4457                    Src::Const(v) => v,
4458                    _ => panic!("fabric SliceOp: non-constant start bound (a runtime bound needs a sequential unit)"),
4459                };
4460                let e = match getb(reg, *end) {
4461                    Src::Const(v) => v,
4462                    _ => panic!("fabric SliceOp: non-constant end bound (a runtime bound needs a sequential unit)"),
4463                };
4464                let start0 = (s - 1).max(0);
4465                let count = if start0 < e && e <= len {
4466                    e - start0
4467                } else {
4468                    0
4469                };
4470                let ka = cells.len();
4471                cells.push(LoopCell::HeapAlloc {
4472                    size: Src::Const(count + 1),
4473                });
4474                let dptr = Src::Val(ka);
4475                cells.push(LoopCell::HeapWrite {
4476                    addr: dptr,
4477                    val: Src::Const(count),
4478                });
4479                for j in 0..count {
4480                    let ksa = cells.len(); // src 0-based (start0+j) → 1-based item at heap[cptr + start0 + j + 1]
4481                    cells.push(LoopCell::Arith {
4482                        op: AluOp::Add,
4483                        a: cptr,
4484                        b: Src::Const(start0 + j + 1),
4485                    });
4486                    let ks = cells.len();
4487                    cells.push(LoopCell::HeapRead {
4488                        addr: Src::Val(ksa),
4489                    });
4490                    let kda = cells.len();
4491                    cells.push(LoopCell::Arith {
4492                        op: AluOp::Add,
4493                        a: dptr,
4494                        b: Src::Const(j + 1),
4495                    });
4496                    cells.push(LoopCell::HeapWrite {
4497                        addr: Src::Val(kda),
4498                        val: Src::Val(ks),
4499                    });
4500                }
4501                reg.insert(*dst, dptr);
4502                COLL_LEN.with(|m| m.borrow_mut().insert(*dst, count));
4503            }
4504            // Structs share the list heap shape `[n, f0..f_{n-1}]`. NewStruct's field count comes from the
4505            // program context (STRUCT_FIELDS); its fields default to 0 (the HeapAlloc zero-fills, matching the
4506            // VM). GetField/StructInsert carry the 0-based field index directly → heap[ptr + 1 + field].
4507            Op::NewStruct { dst, type_name } => {
4508                let n = STRUCT_FIELDS.with(|m| m.borrow().get(type_name).copied().unwrap_or(0));
4509                let ka = cells.len();
4510                cells.push(LoopCell::HeapAlloc {
4511                    size: Src::Const(n as i64 + 1),
4512                });
4513                let ptr = Src::Val(ka);
4514                cells.push(LoopCell::HeapWrite {
4515                    addr: ptr,
4516                    val: Src::Const(n as i64),
4517                }); // header = field count
4518                reg.insert(*dst, ptr);
4519                REG_TYPE.with(|m| m.borrow_mut().insert(*dst, *type_name)); // this reg now holds a `type_name`
4520            }
4521            // `field` is a CONSTANT INDEX to the field NAME; register type-tracking (REG_TYPE, set at
4522            // NewStruct) recovers the object's struct type, and STRUCT_FIELD_POS maps (type, name)→slot.
4523            // heap[ptr + 1 + slot] — the same slot NewStruct zero-filled and StructInsert writes.
4524            Op::GetField { dst, obj, field } => {
4525                let ptr = getb(reg, *obj);
4526                let slot = struct_field_slot(*obj, *field);
4527                let kadd = cells.len();
4528                cells.push(LoopCell::Arith {
4529                    op: AluOp::Add,
4530                    a: ptr,
4531                    b: Src::Const(slot as i64 + 1),
4532                });
4533                let kr = cells.len();
4534                cells.push(LoopCell::HeapRead {
4535                    addr: Src::Val(kadd),
4536                });
4537                reg.insert(*dst, Src::Val(kr));
4538            }
4539            Op::StructInsert { obj, field, value } => {
4540                let ptr = getb(reg, *obj);
4541                let val = getb(reg, *value);
4542                let slot = struct_field_slot(*obj, *field);
4543                let kadd = cells.len();
4544                cells.push(LoopCell::Arith {
4545                    op: AluOp::Add,
4546                    a: ptr,
4547                    b: Src::Const(slot as i64 + 1),
4548                });
4549                cells.push(LoopCell::HeapWrite {
4550                    addr: Src::Val(kadd),
4551                    val,
4552                });
4553            }
4554            // `Increase/Decrease s's field by amount` — a CRDT counter bump on a `Shared` struct field:
4555            // `field ±= amount` (crdt_counter_bump is a wrapping add; a fresh field 0 matches the VM's
4556            // Nothing→amount). The single-replica value is just the running sum — replica ids don't affect it.
4557            Op::CrdtBump {
4558                obj,
4559                field,
4560                amount,
4561                negate,
4562            } => {
4563                let ptr = getb(reg, *obj);
4564                let slot = struct_field_slot(*obj, *field);
4565                let amt = getb(reg, *amount);
4566                let kaddr = cells.len();
4567                cells.push(LoopCell::Arith {
4568                    op: AluOp::Add,
4569                    a: ptr,
4570                    b: Src::Const(slot as i64 + 1),
4571                });
4572                let kcur = cells.len();
4573                cells.push(LoopCell::HeapRead {
4574                    addr: Src::Val(kaddr),
4575                });
4576                let knew = cells.len();
4577                cells.push(LoopCell::Arith {
4578                    op: if *negate { AluOp::Sub } else { AluOp::Add },
4579                    a: Src::Val(kcur),
4580                    b: amt,
4581                });
4582                cells.push(LoopCell::HeapWrite {
4583                    addr: Src::Val(kaddr),
4584                    val: Src::Val(knew),
4585                });
4586            }
4587            // `Resolve s's field to value` — resolve a plain field: the VM OVERWRITES it (value semantics),
4588            // so it is exactly a struct field write.
4589            Op::CrdtResolve { obj, field, value } => {
4590                let ptr = getb(reg, *obj);
4591                let slot = struct_field_slot(*obj, *field);
4592                let val = getb(reg, *value);
4593                let kadd = cells.len();
4594                cells.push(LoopCell::Arith {
4595                    op: AluOp::Add,
4596                    a: ptr,
4597                    b: Src::Const(slot as i64 + 1),
4598                });
4599                cells.push(LoopCell::HeapWrite {
4600                    addr: Src::Val(kadd),
4601                    val,
4602                });
4603            }
4604            // `Merge source into target` — for a plain-Int-field `Shared` struct, `crdt_merge_field` per
4605            // field is a wrapping ADD (arith.rs:1089), so merge = per-field `target[i] += source[i]` over the
4606            // struct's fields. (Nested G-Counter/Set/Map fields need an uncovered NewCrdt, refused first.)
4607            Op::CrdtMerge { target, source } => {
4608                let tptr = getb(reg, *target);
4609                let sptr = getb(reg, *source);
4610                let tn = REG_TYPE
4611                    .with(|m| m.borrow().get(target).copied())
4612                    .expect("fabric CrdtMerge: target struct type not tracked");
4613                let fcount = STRUCT_FIELDS
4614                    .with(|m| m.borrow().get(&tn).copied())
4615                    .expect("fabric CrdtMerge: struct field count not known");
4616                for i in 0..fcount {
4617                    let kta = cells.len(); // target slot i at heap[tptr + 1 + i]
4618                    cells.push(LoopCell::Arith {
4619                        op: AluOp::Add,
4620                        a: tptr,
4621                        b: Src::Const(i as i64 + 1),
4622                    });
4623                    let ktc = cells.len();
4624                    cells.push(LoopCell::HeapRead {
4625                        addr: Src::Val(kta),
4626                    });
4627                    let ksa = cells.len(); // source slot i
4628                    cells.push(LoopCell::Arith {
4629                        op: AluOp::Add,
4630                        a: sptr,
4631                        b: Src::Const(i as i64 + 1),
4632                    });
4633                    let ksv = cells.len();
4634                    cells.push(LoopCell::HeapRead {
4635                        addr: Src::Val(ksa),
4636                    });
4637                    let ksum = cells.len();
4638                    cells.push(LoopCell::Arith {
4639                        op: AluOp::Add,
4640                        a: Src::Val(ktc),
4641                        b: Src::Val(ksv),
4642                    });
4643                    cells.push(LoopCell::HeapWrite {
4644                        addr: Src::Val(kta),
4645                        val: Src::Val(ksum),
4646                    });
4647                }
4648            }
4649            // `CrdtJoin` — the state-based G-Counter LATTICE JOIN: per-field MAX (idempotent), the fixed-lane
4650            // join `crdt_merge` computes. Unlike CrdtMerge's ADD, this never double-counts a redelivered
4651            // state. No LOGOS source emits it (hand-bytecode witnessed) — same struct heap walk, `max` tile.
4652            Op::CrdtJoin { target, source } => {
4653                let tptr = getb(reg, *target);
4654                let sptr = getb(reg, *source);
4655                let tn = REG_TYPE
4656                    .with(|m| m.borrow().get(target).copied())
4657                    .expect("fabric CrdtJoin: target struct type not tracked");
4658                let fcount = STRUCT_FIELDS
4659                    .with(|m| m.borrow().get(&tn).copied())
4660                    .expect("fabric CrdtJoin: struct field count not known");
4661                for i in 0..fcount {
4662                    let kta = cells.len(); // target slot i at heap[tptr + 1 + i]
4663                    cells.push(LoopCell::Arith {
4664                        op: AluOp::Add,
4665                        a: tptr,
4666                        b: Src::Const(i as i64 + 1),
4667                    });
4668                    let ktc = cells.len();
4669                    cells.push(LoopCell::HeapRead {
4670                        addr: Src::Val(kta),
4671                    });
4672                    let ksa = cells.len(); // source slot i
4673                    cells.push(LoopCell::Arith {
4674                        op: AluOp::Add,
4675                        a: sptr,
4676                        b: Src::Const(i as i64 + 1),
4677                    });
4678                    let ksv = cells.len();
4679                    cells.push(LoopCell::HeapRead {
4680                        addr: Src::Val(ksa),
4681                    });
4682                    let kmax = cells.len(); // per-field lattice MAX: target[i] = max(target[i], source[i])
4683                    cells.push(LoopCell::Select {
4684                        cmp: CmpKind::Gt,
4685                        ca: Src::Val(ktc),
4686                        cb: Src::Val(ksv),
4687                        t: Src::Val(ktc),
4688                        e: Src::Val(ksv),
4689                    });
4690                    cells.push(LoopCell::HeapWrite {
4691                        addr: Src::Val(kta),
4692                        val: Src::Val(kmax),
4693                    });
4694                }
4695            }
4696            // Enums (algebraic data types) share the list/struct heap shape: `[tag, arg0..arg_{count-1}]`.
4697            // The tag is the constructor's canonical name-id (`canonical_text_tag`), so a `TestArm` tag
4698            // comparison is exactly the VM's `constructor == variant_name`.
4699            Op::NewInductive {
4700                dst,
4701                type_name: _,
4702                ctor,
4703                args_start,
4704                count,
4705            } => {
4706                let ka = cells.len();
4707                cells.push(LoopCell::HeapAlloc {
4708                    size: Src::Const(*count as i64 + 1),
4709                });
4710                let ptr = Src::Val(ka);
4711                let tag = canonical_text_tag(consts, *ctor);
4712                cells.push(LoopCell::HeapWrite {
4713                    addr: ptr,
4714                    val: Src::Const(tag),
4715                }); // header = variant tag
4716                for k in 0..*count {
4717                    let av = getb(reg, *args_start + k);
4718                    let kadd = cells.len();
4719                    cells.push(LoopCell::Arith {
4720                        op: AluOp::Add,
4721                        a: ptr,
4722                        b: Src::Const(k as i64 + 1),
4723                    });
4724                    cells.push(LoopCell::HeapWrite {
4725                        addr: Src::Val(kadd),
4726                        val: av,
4727                    }); // arg k at ptr+1+k
4728                }
4729                reg.insert(*dst, ptr);
4730            }
4731            // TestArm `(target is variant)` → the tag read compared to the variant's tag, registered as a
4732            // pending predicate so the enclosing match's `JumpIfFalse` if-converts it into a `Select`.
4733            Op::TestArm {
4734                dst,
4735                target,
4736                variant,
4737            } => {
4738                let ptr = getb(reg, *target);
4739                let ktag = cells.len();
4740                cells.push(LoopCell::HeapRead { addr: ptr }); // tag = heap[ptr]
4741                let vtag = canonical_text_tag(consts, *variant);
4742                preds.insert(*dst, (CmpKind::Eq, Src::Val(ktag), Src::Const(vtag)));
4743            }
4744            // BindArm extracts a payload: an Inductive binds by positional `index`; a Struct target (a
4745            // `When` on a struct) binds by field NAME via `struct_field_slot`. `heap[ptr + 1 + slot]`.
4746            Op::BindArm {
4747                dst,
4748                target,
4749                field,
4750                index,
4751            } => {
4752                let ptr = getb(reg, *target);
4753                let is_struct = REG_TYPE.with(|m| m.borrow().contains_key(target));
4754                let slot = if is_struct {
4755                    struct_field_slot(*target, *field)
4756                } else {
4757                    *index as usize
4758                };
4759                let kadd = cells.len();
4760                cells.push(LoopCell::Arith {
4761                    op: AluOp::Add,
4762                    a: ptr,
4763                    b: Src::Const(slot as i64 + 1),
4764                });
4765                let kr = cells.len();
4766                cells.push(LoopCell::HeapRead {
4767                    addr: Src::Val(kadd),
4768                });
4769                reg.insert(*dst, Src::Val(kr));
4770            }
4771            // Direct call → INLINE the callee's decision fabric (Futamura). The caller's argument registers
4772            // become the callee's parameter seeds; the callee's datapath emits into the shared `cells`.
4773            Op::Call {
4774                dst,
4775                func,
4776                args_start,
4777                arg_count,
4778            } => {
4779                let arg_srcs: Vec<Src> = (0..*arg_count)
4780                    .map(|k| getb(reg, *args_start + k))
4781                    .collect();
4782                let result = inline_callee(*func as usize, &arg_srcs, cells, consts);
4783                reg.insert(*dst, result);
4784            }
4785            // `MakeClosure` — capture the closure's free variables in the body's `captures` order: a LOCAL
4786            // capture reads `locals_start + k`; a GLOBAL capture snapshots `GLOBALS` (present iff defined).
4787            // The body's function index is STATIC, so `CallValue` can resolve + inline it (dynamic dispatch
4788            // as a Futamura composition). No cell — a closure is tracked dataflow, like a Move.
4789            Op::MakeClosure {
4790                dst,
4791                func,
4792                locals_start,
4793            } => {
4794                let layout = CLOSURE_CAPTURES
4795                    .with(|m| m.borrow().get(*func as usize).cloned().unwrap_or_default());
4796                let mut captures: Vec<(Src, bool)> = Vec::with_capacity(layout.len());
4797                let mut local_k: u16 = 0;
4798                for cap in &layout {
4799                    match cap {
4800                        None => {
4801                            captures.push((getb(reg, *locals_start + local_k), true)); // a local, always present
4802                            local_k += 1;
4803                        }
4804                        Some(gidx) => match GLOBALS.with(|g| g.borrow().get(gidx).copied()) {
4805                            Some(slot) => captures.push((slot.src, true)),
4806                            None => captures.push((Src::Const(0), false)), // undefined global → not captured
4807                        },
4808                    }
4809                }
4810                CLOSURES.with(|c| c.borrow_mut().insert(*dst, (*func as usize, captures)));
4811            }
4812            // `CallValue` — call a closure value: resolve its (static body, captures) and INLINE with the
4813            // args seeding the params and the captures seeding the two capture bands of the frame.
4814            Op::CallValue {
4815                dst,
4816                callee,
4817                args_start,
4818                arg_count,
4819                ..
4820            } => {
4821                let (func, captures) = CLOSURES
4822                    .with(|c| c.borrow().get(callee).cloned())
4823                    .expect("fabric CallValue: callee is not a tracked closure (dynamic-from-data dispatch is unsupported)");
4824                let arg_srcs: Vec<Src> = (0..*arg_count)
4825                    .map(|k| getb(reg, *args_start + k))
4826                    .collect();
4827                let result = inline_closure(func, &arg_srcs, &captures, cells, consts);
4828                reg.insert(*dst, result);
4829            }
4830            // `Launch f(..)` — SPAWN a child: inline its straight-line side effects at the spawn point (a
4831            // producer's channel enqueues fill the buffer the parent then drains — the same observable as the
4832            // cooperative schedule). `SpawnHandle` additionally yields an opaque task handle (never observed
4833            // as data). Their `TaskAwait` join is instant (the child already ran); `TaskAbort` is a no-op.
4834            Op::Spawn {
4835                func,
4836                args_start,
4837                arg_count,
4838            } => {
4839                let args: Vec<Src> = (0..*arg_count)
4840                    .map(|k| getb(reg, *args_start + k))
4841                    .collect();
4842                inline_spawn(*func as usize, &args, cells, consts);
4843            }
4844            Op::SpawnHandle {
4845                dst,
4846                func,
4847                args_start,
4848                arg_count,
4849            } => {
4850                let args: Vec<Src> = (0..*arg_count)
4851                    .map(|k| getb(reg, *args_start + k))
4852                    .collect();
4853                inline_spawn(*func as usize, &args, cells, consts);
4854                reg.insert(*dst, Src::Const(0)); // opaque task handle
4855            }
4856            Op::TaskAwait { dst, .. } => {
4857                // the spawned child ran to completion at the spawn point; the VM delivers `Nothing` as the
4858                // (unpropagated) task result, so the join passes control through with a sentinel.
4859                reg.insert(*dst, Src::Const(0));
4860            }
4861            Op::TaskAbort { .. } => {}
4862            // A promoted global's DEFINITION — remember the definition's wire + its type metadata under the
4863            // global index. No cell: a global is dataflow, not storage (the straight-line set-before-use
4864            // invariant makes it a plain wire — the fabric analog of the VM's `globals[idx] = v`).
4865            Op::GlobalSet { idx, src } => {
4866                let s = getb(reg, *src);
4867                let len = COLL_LEN.with(|m| m.borrow().get(src).copied());
4868                let text = TEXT_REGS.with(|t| t.borrow().contains(src));
4869                let ty = REG_TYPE.with(|m| m.borrow().get(src).copied());
4870                GLOBALS.with(|g| {
4871                    g.borrow_mut().insert(
4872                        *idx,
4873                        GSlot {
4874                            src: s,
4875                            len,
4876                            text,
4877                            ty,
4878                        },
4879                    )
4880                });
4881            }
4882            // A promoted global's USE — wire the remembered definition (and its metadata) into `dst`.
4883            Op::GlobalGet { dst, idx } => {
4884                let slot = GLOBALS
4885                    .with(|g| g.borrow().get(idx).copied())
4886                    .unwrap_or_else(|| {
4887                        panic!("fabric GlobalGet: global #{idx} read before it was set")
4888                    });
4889                reg.insert(*dst, slot.src);
4890                if let Some(l) = slot.len {
4891                    COLL_LEN.with(|m| {
4892                        m.borrow_mut().insert(*dst, l);
4893                    });
4894                }
4895                if slot.text {
4896                    TEXT_REGS.with(|t| {
4897                        t.borrow_mut().insert(*dst);
4898                    });
4899                }
4900                if let Some(t) = slot.ty {
4901                    REG_TYPE.with(|m| {
4902                        m.borrow_mut().insert(*dst, t);
4903                    });
4904                }
4905            }
4906            // `Sleep n` — a temporal delay. The combinational fabric has no clock, so in the deterministic
4907            // execution model a delay is vacuous: no data effect, no cell (the concurrent VM confirms the
4908            // output is identical with or without it). A structural no-op, exactly like the VM's yield.
4909            Op::Sleep { .. } => {}
4910            // `RegionBoundsGuard` — an Oracle bounds-check assertion (the loop index stays in `[min, max]`).
4911            // It has NO data effect (the VM executes it as `pc += 1`), so on the fabric it is a structural
4912            // no-op — the guarantee it encodes is what let `IndexUnchecked` drop its check in the first place.
4913            Op::RegionBoundsGuard { .. } => {}
4914            // `IterPop` — pop the iterator stack at a loop's end. The fabric's loop model carries the loop
4915            // index explicitly (no iterator-stack object), so popping it is vacuous — a structural no-op,
4916            // exactly as the loop-fabric extractor already treats it.
4917            Op::IterPop => {}
4918            // `Obs` — a Wave-13 observability event (span/message). It NEVER writes stdout — it feeds a
4919            // separate trace sink, and only when one is capturing (off by default). So it has no effect on
4920            // the program's OUTPUT, ever: a structural no-op on the fabric's output-tape model.
4921            Op::Obs { .. } => {}
4922            // `FailWith msg` — raise an error: the VM returns `Err` here, so the program produces NO further
4923            // output. On the fabric this HALTS the output tape (a control effect modeled on the observable):
4924            // every later `Show` is suppressed, so the tape equals the VM's output up to the failure.
4925            Op::FailWith { .. } => {
4926                HALTED.with(|h| h.set(true));
4927            }
4928            // `Show v` — append the value + its text-ness to the WHOLE-PROGRAM output tape (straight-line
4929            // only). Suppressed once a `FailWith` has halted the program (see `HALTED`).
4930            Op::Show { src } => {
4931                assert!(
4932                    !FLOAT_REGS.with(|t| t.borrow().contains(src)),
4933                    "fabric Show: rendering a FLOAT to a string (IEEE float→decimal) is a follow-on — the \
4934                     float lane is witnessed through int-returning results (floor/compare), not Show-of-float"
4935                );
4936                assert!(
4937                    !BIGINT_BIG.with(|m| m.borrow().contains_key(src)),
4938                    "fabric Show: rendering a BigInt to a string is a follow-on — the BigInt lane is witnessed \
4939                     through `Low` (an Int), not Show-of-BigInt"
4940                );
4941                if !HALTED.with(std::cell::Cell::get) {
4942                    let s = getb(reg, *src);
4943                    let is_text = TEXT_REGS.with(|t| t.borrow().contains(src));
4944                    // The policy GRANT guard in force at THIS Show: a failed prior `Check` makes it eval
4945                    // false, and `eval_whole_program` halts the tape here (VM `Err` semantics). `None` = no
4946                    // check gates this Show, so it always prints.
4947                    let guard = POLICY_GUARD.with(|g| g.borrow().clone());
4948                    SHOW_OUTPUTS.with(|o| o.borrow_mut().push((s, is_text, guard)));
4949                }
4950            }
4951            // `Check subject is/can predicate` — a SECURITY guard. The VM evaluates the policy condition and
4952            // returns `Err` on denial (so NOTHING prints after a failed check); a granted check is
4953            // transparent. On the fabric the condition is compiled to a GRANT predicate `subject.field ==
4954            // expected` (a 0/1 `Select`), AND-chained into the running `POLICY_GUARD`; each later `Show`
4955            // carries that guard and `eval_whole_program` halts the tape at the first false one — the same
4956            // observable as the VM's `Err`. Only the SINGLE-FIELD conditions extracted into
4957            // `POLICY_CONDITIONS` are modeled; an object-bearing (cross-field) or otherwise-unmodeled check
4958            // PANICS rather than silently granting (a wrong grant is a security hole — rule 8).
4959            Op::CheckPolicy {
4960                subject,
4961                predicate,
4962                is_capability,
4963                object,
4964                source_text,
4965            } => {
4966                let _ = source_text; // the VM's `Err` text; the fabric models the OBSERVABLE (tape halts).
4967                assert!(
4968                    *object == crate::vm::Reg::MAX,
4969                    "fabric CheckPolicy: an object-bearing (cross-field) capability is a follow-on — the \
4970                     modeled conditions are single-field FieldBool/FieldEquals only; refusing rather than \
4971                     silently granting"
4972                );
4973                let tn = REG_TYPE
4974                    .with(|m| m.borrow().get(subject).copied())
4975                    .expect("fabric CheckPolicy: subject register has no tracked struct type");
4976                let key = (tn, predicate.index() as u32, *is_capability);
4977                let spec = POLICY_CONDITIONS
4978                    .with(|m| m.borrow().get(&key).cloned())
4979                    .unwrap_or_else(|| {
4980                        panic!(
4981                            "fabric CheckPolicy: no MODELED condition for (type-const {tn}, predicate \
4982                             {predicate:?}, capability {is_capability}) — a compound (And/Or) / object-cross- \
4983                             field / predicate-indirection condition is a follow-on; refusing rather than \
4984                             silently granting (a wrong grant is a security hole)"
4985                        )
4986                    });
4987                // Read `subject.field` (`heap[ptr + 1 + slot]`) — shared by the Int and Text grants.
4988                let field_val = |cells: &mut Vec<LoopCell>, slot: usize| -> Src {
4989                    let ptr = getb(reg, *subject);
4990                    let kaddr = cells.len();
4991                    cells.push(LoopCell::Arith {
4992                        op: AluOp::Add,
4993                        a: ptr,
4994                        b: Src::Const(slot as i64 + 1),
4995                    });
4996                    let kfield = cells.len();
4997                    cells.push(LoopCell::HeapRead {
4998                        addr: Src::Val(kaddr),
4999                    });
5000                    Src::Val(kfield)
5001                };
5002                let grant = match spec {
5003                    // Always-deny: the VM's compare is unsatisfiable, so a constant-0 grant.
5004                    PolicyGrant::AlwaysDeny => Src::Const(0),
5005                    // `subject.field == expected` (Bool/Int).
5006                    PolicyGrant::IntEq { slot, expected } => {
5007                        let fv = field_val(cells, slot);
5008                        let kg = cells.len();
5009                        cells.push(LoopCell::Select {
5010                            cmp: CmpKind::Eq,
5011                            ca: fv,
5012                            cb: Src::Const(expected),
5013                            t: Src::Const(1),
5014                            e: Src::Const(0),
5015                        });
5016                        Src::Val(kg)
5017                    }
5018                    // Text field: `subject.field` is a char-list pointer `[len, b0, b1, …]`; grant ⇔ the length
5019                    // matches AND every byte matches `expected`. Each check is a 0/1 `Select`, AND-chained by
5020                    // multiply — a bounded, combinational string-equality, bit-identical to the VM's `==`.
5021                    PolicyGrant::TextEq { slot, expected } => {
5022                        let strptr = field_val(cells, slot);
5023                        let klen = cells.len();
5024                        cells.push(LoopCell::HeapRead { addr: strptr }); // heap[strptr] = length
5025                        let klenchk = cells.len();
5026                        cells.push(LoopCell::Select {
5027                            cmp: CmpKind::Eq,
5028                            ca: Src::Val(klen),
5029                            cb: Src::Const(expected.len() as i64),
5030                            t: Src::Const(1),
5031                            e: Src::Const(0),
5032                        });
5033                        let mut acc = Src::Val(klenchk);
5034                        for (i, b) in expected.iter().enumerate() {
5035                            let kba = cells.len();
5036                            cells.push(LoopCell::Arith {
5037                                op: AluOp::Add,
5038                                a: strptr,
5039                                b: Src::Const(i as i64 + 1),
5040                            });
5041                            let kbv = cells.len();
5042                            cells.push(LoopCell::HeapRead {
5043                                addr: Src::Val(kba),
5044                            });
5045                            let kbc = cells.len();
5046                            cells.push(LoopCell::Select {
5047                                cmp: CmpKind::Eq,
5048                                ca: Src::Val(kbv),
5049                                cb: Src::Const(i64::from(*b)),
5050                                t: Src::Const(1),
5051                                e: Src::Const(0),
5052                            });
5053                            let kand = cells.len();
5054                            cells.push(LoopCell::Arith {
5055                                op: AluOp::Mul,
5056                                a: acc,
5057                                b: Src::Val(kbc),
5058                            });
5059                            acc = Src::Val(kand);
5060                        }
5061                        acc
5062                    }
5063                };
5064                POLICY_GUARD.with(|g| {
5065                    let mut g = g.borrow_mut();
5066                    *g = Some(match g.take() {
5067                        None => grant,
5068                        // AND two 0/1 guards = multiply (1·1 = 1, else 0) — one more residual cell.
5069                        Some(prev) => {
5070                            let kand = cells.len();
5071                            cells.push(LoopCell::Arith {
5072                                op: AluOp::Mul,
5073                                a: prev,
5074                                b: grant,
5075                            });
5076                            Src::Val(kand)
5077                        }
5078                    });
5079                });
5080            }
5081            other => panic!(
5082                "loop body arm: `{}` unsupported (nested control flow is future work)",
5083                other.name()
5084            ),
5085        }
5086    }
5087}
5088
5089/// If-CONVERT a loop body: run any leading straight-line ops, then — if the body branches —
5090/// execute BOTH arms from the SAME entry state and merge each carried register with a predicated
5091/// `Select` (the mux tile). Mutates `reg` to the post-body state; returns the body cells. Control
5092/// becomes data; no divergence. Shared by the counted-loop and `while`-loop extractors.
5093fn if_convert_body(
5094    body: &[(usize, crate::vm::Op)],
5095    reg: &mut std::collections::HashMap<u16, Src>,
5096    carried: &[u16],
5097    consts: &[crate::vm::Constant],
5098    // Pre-seeded cells: a SEQUENCE loop seeds cell 0 = `MemRead(addr = iv)` so the body's element `x`
5099    // (mapped to `Src::Val(0)`) reads `seq[iv]`. A numeric loop / `while` passes an empty vec.
5100    seed_cells: Vec<LoopCell>,
5101) -> Vec<LoopCell> {
5102    use crate::vm::Op;
5103    let ops_of = |seg: &[(usize, Op)]| -> Vec<Op> { seg.iter().map(|(_, o)| *o).collect() };
5104    let idx_of_pc = |target: usize| {
5105        body.iter()
5106            .position(|(p, _)| *p == target)
5107            .unwrap_or(body.len())
5108    };
5109
5110    let mut cells: Vec<LoopCell> = seed_cells;
5111    let mut preds: std::collections::HashMap<u16, (CmpKind, Src, Src)> =
5112        std::collections::HashMap::new();
5113    let mut i = 0;
5114    while i < body.len() {
5115        if let (_pc, Op::JumpIfFalse { cond, target }) = body[i] {
5116            let (cmp, ca, cb) = *preds
5117                .get(&cond)
5118                .expect("loop fabric: JumpIfFalse condition is not a comparison");
5119            // The JumpIfFalse target is the merge (if-only) or the else start (if/else, when the
5120            // op just before it is the then→merge `Jump`). PC-resolved so TRAILING code after the
5121            // branch is processed too (the cursor resumes at the merge).
5122            let target_idx = idx_of_pc(target);
5123            let (then_seg, else_seg, merge_idx) =
5124                if target_idx > i + 1 && matches!(body[target_idx - 1].1, Op::Jump { .. }) {
5125                    let m = match body[target_idx - 1].1 {
5126                        Op::Jump { target } => target,
5127                        _ => unreachable!(),
5128                    };
5129                    (
5130                        &body[i + 1..target_idx - 1],
5131                        &body[target_idx..idx_of_pc(m)],
5132                        idx_of_pc(m),
5133                    )
5134                } else {
5135                    (&body[i + 1..target_idx], &body[0..0], target_idx)
5136                };
5137            let entry = reg.clone();
5138            let mut then_reg = entry.clone();
5139            exec_ops(
5140                &ops_of(then_seg),
5141                &mut then_reg,
5142                &mut cells,
5143                &mut preds,
5144                consts,
5145            );
5146            let mut else_reg = entry;
5147            exec_ops(
5148                &ops_of(else_seg),
5149                &mut else_reg,
5150                &mut cells,
5151                &mut preds,
5152                consts,
5153            );
5154            for &r in carried {
5155                let t = then_reg[&r];
5156                let e = else_reg[&r];
5157                if t != e {
5158                    let k = cells.len();
5159                    cells.push(LoopCell::Select { cmp, ca, cb, t, e });
5160                    reg.insert(r, Src::Val(k));
5161                } else {
5162                    reg.insert(r, t);
5163                }
5164            }
5165            i = merge_idx;
5166        } else {
5167            exec_ops(&[body[i].1], reg, &mut cells, &mut preds, consts);
5168            i += 1;
5169        }
5170    }
5171    cells
5172}
5173
5174// ===========================================================================================
5175// DECISION FABRIC — a branchy straight-line function (no loop) if-CONVERTED to a pure select-DAG.
5176//
5177// The HFT strategy's DECISION — given the market-derived scalars (microprice, fair, edge, size,
5178// position, risk caps) — is nested `If/Otherwise/Set` over mutable state. That whole decision is a
5179// COMBINATIONAL datapath: every branch becomes a predicated `Select` (mux) tile, so control is data
5180// and the answer settles in ONE clock — no program counter, no fetch, no dispatch, no loop counter.
5181// This is the fetch/decode-free "fast path" the soft-core's 866 fetch-decode-execute cycles pay for.
5182//
5183// It reuses the CGRA's `CmpKind`/`LoopCell`/`exec_ops`, but adds RECURSIVE if-conversion so NESTED
5184// branches (the loop-body converter's "future work") lower too — an `if` inside an `if` becomes a
5185// `Select` whose arms are themselves `Select`s.
5186// ===========================================================================================
5187
5188/// A combinational decision datapath: SSA `LoopCell`s (arith + predicated select) over the function
5189/// parameters (`Src::In`), plus the `Src` that drives the single output.
5190#[derive(Clone, Debug)]
5191pub struct DecisionFabric {
5192    pub n_in: usize,
5193    pub cells: Vec<LoopCell>,
5194    pub result: Src,
5195    /// A branch in the captured body jumps PAST the first `Return` (an early-return diamond /
5196    /// multi-return match). The fabric is then the FIRST-RETURN PATH only — sound when the
5197    /// escaping arm is statically dead (diff-verified corpora prove theirs ≡VM), and a
5198    /// REFUSAL trigger for tolerance-verified consumers (the analog lane), which cannot
5199    /// detect a dropped branch by diffing.
5200    pub escaped_first_return: bool,
5201}
5202
5203/// Recursively if-convert a straight-line-with-branches segment into `cells`, merging every carried
5204/// register at each branch join with a `Select`. Nested branches recurse; `preds` carries pending
5205/// comparisons. Mutates `reg` to the post-segment state.
5206fn dec_convert(
5207    body: &[(usize, crate::vm::Op)],
5208    reg: &mut std::collections::HashMap<u16, Src>,
5209    cells: &mut Vec<LoopCell>,
5210    preds: &mut std::collections::HashMap<u16, (CmpKind, Src, Src)>,
5211    carried: &[u16],
5212    consts: &[crate::vm::Constant],
5213) {
5214    use crate::vm::Op;
5215    let idx_of_pc = |t: usize| body.iter().position(|(p, _)| *p == t).unwrap_or(body.len());
5216    let mut i = 0;
5217    while i < body.len() {
5218        match body[i].1 {
5219            Op::JumpIfFalse { cond, target } => {
5220                // A comparison condition folds its (cmp, a, b) directly; a NON-comparison condition (a Bool
5221                // VALUE — from `Not`, a Bool literal, or a Bool parameter) branches on `cond != 0`, exactly
5222                // the VM's `is_truthy` test. So control on a computed Bool is data on the fabric too.
5223                let (cmp, ca, cb) = preds.get(&cond).copied().unwrap_or_else(|| {
5224                    (
5225                        CmpKind::Ne,
5226                        reg.get(&cond).copied().unwrap_or(Src::In(cond as usize)),
5227                        Src::Const(0),
5228                    )
5229                });
5230                let target_idx = idx_of_pc(target);
5231                let (then_seg, else_seg, merge_idx) =
5232                    if target_idx > i + 1 && matches!(body[target_idx - 1].1, Op::Jump { .. }) {
5233                        let m = match body[target_idx - 1].1 {
5234                            Op::Jump { target } => target,
5235                            _ => unreachable!(),
5236                        };
5237                        (
5238                            &body[i + 1..target_idx - 1],
5239                            &body[target_idx..idx_of_pc(m)],
5240                            idx_of_pc(m),
5241                        )
5242                    } else {
5243                        (&body[i + 1..target_idx], &body[0..0], target_idx)
5244                    };
5245                let entry = reg.clone();
5246                let mut then_reg = entry.clone();
5247                dec_convert(then_seg, &mut then_reg, cells, preds, carried, consts);
5248                let mut else_reg = entry.clone();
5249                dec_convert(else_seg, &mut else_reg, cells, preds, carried, consts);
5250                for &r in carried {
5251                    // Each arm began as a clone of the entry state, so a register an arm did not
5252                    // write still holds its entry value; a register present in one arm map but not
5253                    // the other is defined only on that path (an arm-local variable, dead after the
5254                    // join and later pruned). The predicated Select fires only when the two paths
5255                    // genuinely disagree.
5256                    match (then_reg.get(&r).copied(), else_reg.get(&r).copied()) {
5257                        (Some(t), Some(e)) if t != e => {
5258                            let k = cells.len();
5259                            cells.push(LoopCell::Select { cmp, ca, cb, t, e });
5260                            reg.insert(r, Src::Val(k));
5261                        }
5262                        (Some(v), Some(_)) => {
5263                            reg.insert(r, v);
5264                        }
5265                        (Some(v), None) | (None, Some(v)) => {
5266                            reg.insert(r, v);
5267                        }
5268                        (None, None) => {}
5269                    }
5270                }
5271                i = merge_idx;
5272            }
5273            // `JumpIfTrue` — the MIRROR of `JumpIfFalse`, emitted by short-circuit `or`/`and`, which
5274            // materialise a Bool TEMP via control flow (`R = (a==0) or (b==0)`) that a later `If` then
5275            // branches on. Here the jump is taken when the condition is TRUE, so the TARGET segment is the
5276            // true-arm and the FALL-THROUGH is the false-arm (the two arms swap vs `JumpIfFalse`). Nested
5277            // short-circuits recurse; the merged Bool temp then drives the real `If` as a value condition.
5278            Op::JumpIfTrue { cond, target } => {
5279                let (cmp, ca, cb) = preds.get(&cond).copied().unwrap_or_else(|| {
5280                    (
5281                        CmpKind::Ne,
5282                        reg.get(&cond).copied().unwrap_or(Src::In(cond as usize)),
5283                        Src::Const(0),
5284                    )
5285                });
5286                let target_idx = idx_of_pc(target);
5287                // Fall-through (cond FALSE) is `body[i+1..]`; if it ends in a `Jump` just before the target,
5288                // it is a two-armed diamond and the target is the TRUE arm. Otherwise the target is the
5289                // merge and the true-arm is empty (a bare forward skip).
5290                let (false_seg, true_seg, merge_idx) =
5291                    if target_idx > i + 1 && matches!(body[target_idx - 1].1, Op::Jump { .. }) {
5292                        let m = match body[target_idx - 1].1 {
5293                            Op::Jump { target } => target,
5294                            _ => unreachable!(),
5295                        };
5296                        (
5297                            &body[i + 1..target_idx - 1],
5298                            &body[target_idx..idx_of_pc(m)],
5299                            idx_of_pc(m),
5300                        )
5301                    } else {
5302                        (&body[i + 1..target_idx], &body[0..0], target_idx)
5303                    };
5304                let entry = reg.clone();
5305                let mut true_reg = entry.clone();
5306                dec_convert(true_seg, &mut true_reg, cells, preds, carried, consts);
5307                let mut false_reg = entry.clone();
5308                dec_convert(false_seg, &mut false_reg, cells, preds, carried, consts);
5309                for &r in carried {
5310                    // `t` = the TRUE-arm value, `e` = the FALSE-arm value — so the Select still fires
5311                    // `cmp ? t : e`, identical polarity to `JumpIfFalse`, only the segments are swapped.
5312                    match (true_reg.get(&r).copied(), false_reg.get(&r).copied()) {
5313                        (Some(t), Some(e)) if t != e => {
5314                            let k = cells.len();
5315                            cells.push(LoopCell::Select { cmp, ca, cb, t, e });
5316                            reg.insert(r, Src::Val(k));
5317                        }
5318                        (Some(v), Some(_)) => {
5319                            reg.insert(r, v);
5320                        }
5321                        (Some(v), None) | (None, Some(v)) => {
5322                            reg.insert(r, v);
5323                        }
5324                        (None, None) => {}
5325                    }
5326                }
5327                i = merge_idx;
5328            }
5329            Op::Jump { .. } | Op::Return { .. } | Op::ReturnNothing => {
5330                i += 1;
5331            }
5332            _ => {
5333                let start = i;
5334                while i < body.len()
5335                    && !matches!(
5336                        body[i].1,
5337                        Op::JumpIfFalse { .. }
5338                            | Op::JumpIfTrue { .. }
5339                            | Op::Jump { .. }
5340                            | Op::Return { .. }
5341                            | Op::ReturnNothing
5342                    )
5343                {
5344                    i += 1;
5345                }
5346                let run: Vec<Op> = body[start..i].iter().map(|(_, o)| *o).collect();
5347                exec_ops(&run, reg, cells, preds, consts);
5348            }
5349        }
5350    }
5351}
5352
5353/// The op mnemonics a function's decision body USES (entry `..` first `Return`) — the SAME segment
5354/// [`decision_fabric_from_source`] lowers. This is the ground truth for "which ops a fabric built from this
5355/// function actually exercises", so a coverage-corpus program's realized-op claim is welded to a program
5356/// that genuinely contains the op (not merely declared).
5357pub fn function_op_names(src: &str, fn_name: &str) -> Vec<&'static str> {
5358    crate::ui_bridge::with_parsed_program(src, |parsed, interner| {
5359        let (stmts, types, _pol) = parsed.expect("parse failed");
5360        // Inspect the same typed compilation that decision_fabric_from_source lowers.
5361        // The registry also supplies the fixed-capacity sequence plan.
5362        let program = crate::vm::Compiler::compile_with_types(stmts, interner, Some(types))
5363            .expect("compile failed");
5364        let f = program
5365            .functions
5366            .iter()
5367            .find(|f| interner.resolve(f.name) == fn_name)
5368            .unwrap_or_else(|| panic!("function `{fn_name}` not found"));
5369        let mut names = Vec::new();
5370        for op in &program.code[f.entry_pc..] {
5371            names.push(op.name());
5372            if matches!(
5373                op,
5374                crate::vm::Op::Return { .. } | crate::vm::Op::ReturnNothing
5375            ) {
5376                break;
5377            }
5378        }
5379        names
5380    })
5381}
5382
5383/// Like [`function_op_names`], but scans the WHOLE compiled program — all functions plus the module-init
5384/// prologue. Used to weld coverage for a compiler-emitted op that a user function never directly contains:
5385/// `NewCrdt` is emitted in a `Shared` struct's default-fill, which runs in global struct-init, not the
5386/// user function's own op stream. So `function_op_names(f)` cannot surface it; this can.
5387pub fn whole_program_op_names(src: &str) -> Vec<&'static str> {
5388    crate::ui_bridge::with_parsed_program(src, |parsed, interner| {
5389        let (stmts, types, _pol) = parsed.expect("parse failed");
5390        // `compile_with_types` (not plain `compile`): the discovery-pass type registry resolves the `Shared`
5391        // struct CRDT field types, which is what makes the default-fill emit `NewCrdt` at all.
5392        let program = crate::vm::Compiler::compile_with_types(stmts, interner, Some(types))
5393            .expect("compile failed");
5394        program.code.iter().map(|op| op.name()).collect()
5395    })
5396}
5397
5398/// **The Futamura P1 lowering for a branchy decision.** Compile `src`, take function `fn_name`'s
5399/// straight-line-with-branches body (entry `..` `Return`), and if-convert it to a combinational
5400/// select-DAG: parameters become circuit inputs, every `If` a `Select`. Panics on a shape outside
5401/// the compare/arith/branch family (the honest boundary — loops map to a CGRA, division to a
5402/// separate functional unit).
5403pub fn decision_fabric_from_source(src: &str, fn_name: &str) -> DecisionFabric {
5404    use crate::vm::Op;
5405    use std::collections::HashMap;
5406
5407    crate::ui_bridge::with_parsed_program(src, |parsed, interner| {
5408        let (stmts, types, _pol) = parsed.expect("parse failed");
5409        // `compile_with_types` (not plain `compile`): only the discovery-pass type registry resolves the
5410        // full struct field LAYOUT into `program.struct_types` — the source of the field name→slot map.
5411        let program = crate::vm::Compiler::compile_with_types(stmts, interner, Some(types))
5412            .expect("compile failed");
5413        // struct field-count + (type,field)→slot maps, register-type reset, whole-program code for `Call`
5414        // inlining, collection-length + output-tape resets — the shared per-program fabric context.
5415        setup_program_thread_locals(&program);
5416        let f = program
5417            .functions
5418            .iter()
5419            .find(|f| interner.resolve(f.name) == fn_name)
5420            .unwrap_or_else(|| panic!("function `{fn_name}` not found"));
5421
5422        // (pc, op) body from entry to the FIRST Return (inclusive); capture the returned register.
5423        let mut body: Vec<(usize, Op)> = Vec::new();
5424        let mut ret_src: Option<u16> = None;
5425        for (off, op) in program.code[f.entry_pc..].iter().enumerate() {
5426            if let Op::Return { src } = op {
5427                ret_src = Some(*src);
5428                break;
5429            }
5430            if matches!(op, Op::ReturnNothing) {
5431                break;
5432            }
5433            body.push((f.entry_pc + off, *op));
5434        }
5435        let ret_src = ret_src.expect("decision fabric: function does not Return a value");
5436
5437        // Record the early-return-diamond shape (`If …: Return X. … Return Y.`): a jump
5438        // escaping the captured body means the fabric covers the FIRST-RETURN PATH only.
5439        // Diff-verified consumers prove their corpora ≡VM anyway; tolerance-verified
5440        // consumers (the analog lane) refuse on this flag.
5441        let body_end = f.entry_pc + body.len();
5442        let escaped_first_return = body.iter().any(|(_, op)| {
5443            let target = match op {
5444                Op::Jump { target } => Some(*target as usize),
5445                Op::JumpIfFalse { target, .. } | Op::JumpIfTrue { target, .. } => {
5446                    Some(*target as usize)
5447                }
5448                _ => None,
5449            };
5450            target.is_some_and(|t| t > body_end)
5451        });
5452
5453        let carried = carried_regs(&body);
5454
5455        let mut reg: HashMap<u16, Src> = HashMap::new();
5456        for p in 0..f.param_count {
5457            reg.insert(p, Src::In(p as usize));
5458            // A `Float`-typed parameter carries an f64 BIT-PATTERN in its i64 slot, so mark it float —
5459            // this routes its `+ - * /` (and `sqrt`) through the float functional units. Unlocks the float
5460            // lane over PARAMETERS (a battery of float inputs to one datapath), not just float constants.
5461            if matches!(
5462                f.param_kinds.get(p as usize),
5463                Some(Some(crate::vm::ParamKind::Scalar(
5464                    crate::vm::SlotKind::Float
5465                )))
5466            ) {
5467                FLOAT_REGS.with(|t| {
5468                    t.borrow_mut().insert(p);
5469                });
5470            }
5471        }
5472        let mut cells: Vec<LoopCell> = Vec::new();
5473        let mut preds: HashMap<u16, (CmpKind, Src, Src)> = HashMap::new();
5474        dec_convert(
5475            &body,
5476            &mut reg,
5477            &mut cells,
5478            &mut preds,
5479            &carried,
5480            &program.constants,
5481        );
5482
5483        let result = *reg.get(&ret_src).unwrap_or_else(|| {
5484            panic!("decision fabric: returned register R{ret_src} is unwritten")
5485        });
5486        let (cells, result) = prune_dead_cells(&cells, result);
5487        DecisionFabric {
5488            n_in: f.param_count as usize,
5489            cells,
5490            result,
5491            escaped_first_return,
5492        }
5493    })
5494}
5495
5496/// Drop cells not reachable from the output and compact the SSA numbering — if-conversion can leave
5497/// an intermediate `Select` dead once an outer branch re-merges the same register. Keeps the emitted
5498/// circuit tight and lint-clean (no unused wires) without changing what it computes.
5499fn prune_dead_cells(cells: &[LoopCell], result: Src) -> (Vec<LoopCell>, Src) {
5500    fn mark(s: Src, stack: &mut Vec<usize>) {
5501        if let Src::Val(k) = s {
5502            stack.push(k);
5503        }
5504    }
5505    let mut live = vec![false; cells.len()];
5506    let mut stack = Vec::new();
5507    mark(result, &mut stack);
5508    // Heap WRITES/ALLOCS are SIDE-EFFECTING: their effect flows through the heap, not through an SSA value,
5509    // so the dataflow reachability above cannot see it. Force them live (and thus their operands too) — a
5510    // dropped HeapWrite would silently leave a collection unpopulated.
5511    for (k, c) in cells.iter().enumerate() {
5512        if matches!(c, LoopCell::HeapWrite { .. } | LoopCell::HeapAlloc { .. }) {
5513            stack.push(k);
5514        }
5515    }
5516    while let Some(k) = stack.pop() {
5517        if live[k] {
5518            continue;
5519        }
5520        live[k] = true;
5521        match cells[k] {
5522            LoopCell::Arith { a, b, .. } | LoopCell::VectorArith { a, b, .. } => {
5523                mark(a, &mut stack);
5524                mark(b, &mut stack);
5525            }
5526            LoopCell::Func { a, b, .. } => {
5527                mark(a, &mut stack);
5528                mark(b, &mut stack);
5529            }
5530            LoopCell::MemRead { addr, .. } | LoopCell::HeapRead { addr, .. } => {
5531                mark(addr, &mut stack);
5532            }
5533            LoopCell::HeapWrite { addr, val } => {
5534                mark(addr, &mut stack);
5535                mark(val, &mut stack);
5536            }
5537            LoopCell::HeapAlloc { size } => {
5538                mark(size, &mut stack);
5539            }
5540            LoopCell::Select { ca, cb, t, e, .. } => {
5541                mark(ca, &mut stack);
5542                mark(cb, &mut stack);
5543                mark(t, &mut stack);
5544                mark(e, &mut stack);
5545            }
5546        }
5547    }
5548    let mut remap = vec![usize::MAX; cells.len()];
5549    let mut kept: Vec<LoopCell> = Vec::new();
5550    for (k, c) in cells.iter().enumerate() {
5551        if live[k] {
5552            remap[k] = kept.len();
5553            kept.push(*c);
5554        }
5555    }
5556    fn relabel(s: Src, remap: &[usize]) -> Src {
5557        match s {
5558            Src::Val(k) => Src::Val(remap[k]),
5559            other => other,
5560        }
5561    }
5562    for c in kept.iter_mut() {
5563        match c {
5564            LoopCell::Arith { a, b, .. } | LoopCell::VectorArith { a, b, .. } => {
5565                *a = relabel(*a, &remap);
5566                *b = relabel(*b, &remap);
5567            }
5568            LoopCell::Func { a, b, .. } => {
5569                *a = relabel(*a, &remap);
5570                *b = relabel(*b, &remap);
5571            }
5572            LoopCell::MemRead { addr, .. } | LoopCell::HeapRead { addr, .. } => {
5573                *addr = relabel(*addr, &remap);
5574            }
5575            LoopCell::HeapWrite { addr, val } => {
5576                *addr = relabel(*addr, &remap);
5577                *val = relabel(*val, &remap);
5578            }
5579            LoopCell::HeapAlloc { size } => {
5580                *size = relabel(*size, &remap);
5581            }
5582            LoopCell::Select { ca, cb, t, e, .. } => {
5583                *ca = relabel(*ca, &remap);
5584                *cb = relabel(*cb, &remap);
5585                *t = relabel(*t, &remap);
5586                *e = relabel(*e, &remap);
5587            }
5588        }
5589    }
5590    (kept, relabel(result, &remap))
5591}
5592
5593/// Evaluate the decision DAG (arith + select, 64-bit wrapping) — the reference the emitted circuit
5594/// is checked against, bit-identical to the VM's `Int`.
5595pub fn eval_decision_fabric(df: &DecisionFabric, inputs: &[i64]) -> i64 {
5596    let mut vals: Vec<i64> = Vec::with_capacity(df.cells.len());
5597    for c in &df.cells {
5598        let v = match c {
5599            LoopCell::Arith { op, a, b } => op.eval(a.eval(inputs, &vals), b.eval(inputs, &vals)),
5600            LoopCell::VectorArith { op, ty, a, b } => op.eval(
5601                *ty,
5602                a.eval(inputs, &vals) as u64,
5603                b.eval(inputs, &vals) as u64,
5604            ) as i64,
5605            LoopCell::Func { unit, a, b } => {
5606                unit.eval(a.eval(inputs, &vals), b.eval(inputs, &vals))
5607            }
5608            LoopCell::MemRead { .. }
5609            | LoopCell::HeapRead { .. }
5610            | LoopCell::HeapWrite { .. }
5611            | LoopCell::HeapAlloc { .. } => panic!(
5612                "MemRead reached an evaluator that does not thread the iterated sequence — \
5613                 a sequence loop must be evaluated via eval_loop_fabric_seq"
5614            ),
5615            LoopCell::Select { cmp, ca, cb, t, e } => {
5616                if cmp.eval(ca.eval(inputs, &vals), cb.eval(inputs, &vals)) {
5617                    t.eval(inputs, &vals)
5618                } else {
5619                    e.eval(inputs, &vals)
5620                }
5621            }
5622        };
5623        vals.push(v);
5624    }
5625    df.result.eval(inputs, &vals)
5626}
5627
5628/// Evaluate a decision fabric that reads the shared HEAP (the state plane). `heap` mirrors the VM's exact
5629/// layout — a collection at pointer `p` is `heap[p] = length` then its elements at `heap[p+1..=p+len]`
5630/// (1-based) — so this evaluation is bit-identical to the VM's `Int`. A [`LoopCell::HeapRead`] reads
5631/// `heap[addr]`; `length of xs` is `HeapRead(p)`, `item i of xs` is `HeapRead(p + i)`. Reads are pure given
5632/// the input heap, so the fabric stays a combinational DAG (heap WRITES/ALLOC are the next increment).
5633pub fn eval_decision_fabric_heap(df: &DecisionFabric, inputs: &[i64], heap: &mut Vec<i64>) -> i64 {
5634    let vals = eval_cells(&df.cells, inputs, heap);
5635    df.result.eval(inputs, &vals)
5636}
5637
5638/// Evaluate a straight-line SSA cell list in order into `vals` (one value per cell), threading heap side
5639/// effects (a `HeapRead` sees every prior `HeapWrite`/`HeapAlloc`) — the imperative reduction the VM does.
5640/// Returns the per-cell value array so any `Src` (a fabric result, or a `Show`'d output) can then be read.
5641fn eval_cells(cells: &[LoopCell], inputs: &[i64], heap: &mut Vec<i64>) -> Vec<i64> {
5642    let mut vals: Vec<i64> = Vec::with_capacity(cells.len());
5643    for c in cells {
5644        let v = match c {
5645            LoopCell::Arith { op, a, b } => op.eval(a.eval(inputs, &vals), b.eval(inputs, &vals)),
5646            LoopCell::VectorArith { op, ty, a, b } => op.eval(
5647                *ty,
5648                a.eval(inputs, &vals) as u64,
5649                b.eval(inputs, &vals) as u64,
5650            ) as i64,
5651            LoopCell::Func { unit, a, b } => {
5652                unit.eval(a.eval(inputs, &vals), b.eval(inputs, &vals))
5653            }
5654            LoopCell::HeapRead { addr } => {
5655                let a = addr.eval(inputs, &vals);
5656                heap.get(a as usize).copied().unwrap_or(0)
5657            }
5658            LoopCell::HeapWrite { addr, val } => {
5659                let a = addr.eval(inputs, &vals) as usize;
5660                let v = val.eval(inputs, &vals);
5661                if a >= heap.len() {
5662                    heap.resize(a + 1, 0);
5663                }
5664                heap[a] = v;
5665                v
5666            }
5667            LoopCell::HeapAlloc { size } => {
5668                let s = size.eval(inputs, &vals).max(0) as usize;
5669                let ptr = heap.len() as i64;
5670                heap.resize(heap.len() + s, 0);
5671                ptr
5672            }
5673            LoopCell::MemRead { .. } => panic!(
5674                "MemRead (loop-sequence read) has no meaning in a straight-line heap eval — \
5675                 sequence loops evaluate via eval_loop_fabric_seq"
5676            ),
5677            LoopCell::Select { cmp, ca, cb, t, e } => {
5678                if cmp.eval(ca.eval(inputs, &vals), cb.eval(inputs, &vals)) {
5679                    t.eval(inputs, &vals)
5680                } else {
5681                    e.eval(inputs, &vals)
5682                }
5683            }
5684        };
5685        vals.push(v);
5686    }
5687    vals
5688}
5689
5690/// The faithful MODEL of the whole-program SEQUENCED fabric ([`emit_seq_program_verilog`]): walk blocks by a PC,
5691/// evaluating each block's cells in order (threading ONE shared heap = the state plane) and taking the terminator
5692/// (Jump/Branch/Return), exactly as the block-walk FSM does in RTL. This is the sequenced fabric's own oracle —
5693/// distinct from [`run_program_cfg_on_grid`](crate::codegen_sva::fabric_grid::run_program_cfg_on_grid) (which models the GRID realization and cannot place a looping heap
5694/// body). Bit-identical to the VM's `Int` for a lowered program. `budget` bounds the block walk (a loop that
5695/// never returns panics, catching a lowering back-edge bug — never a silent hang).
5696///
5697/// [`run_program_cfg_on_grid`]: crate::codegen_sva::fabric_grid::run_program_cfg_on_grid
5698pub fn eval_seq_program(pf: &ProgramCfgFabric, params: &[i64]) -> i64 {
5699    use std::collections::HashMap;
5700    let mut file: HashMap<u16, i64> = (0..pf.n_params)
5701        .map(|p| (p as u16, params.get(p).copied().unwrap_or(0)))
5702        .collect();
5703    let mut heap: Vec<i64> = Vec::new();
5704    let mut blk = 0usize;
5705    for _ in 0..10_000_000 {
5706        let b = &pf.blocks[blk];
5707        let inputs: Vec<i64> = b
5708            .inputs
5709            .iter()
5710            .map(|r| *file.get(r).unwrap_or(&0))
5711            .collect();
5712        let vals = eval_cells(&b.body, &inputs, &mut heap);
5713        for (r, s) in &b.writes {
5714            file.insert(*r, s.eval(&inputs, &vals));
5715        }
5716        match &b.term {
5717            Term::Jump(t) => blk = *t,
5718            Term::Branch {
5719                cond,
5720                then_blk,
5721                else_blk,
5722            } => {
5723                blk = if cond.eval(&inputs, &vals) != 0 {
5724                    *then_blk
5725                } else {
5726                    *else_blk
5727                }
5728            }
5729            Term::Return(s) => return s.eval(&inputs, &vals),
5730        }
5731    }
5732    panic!("eval_seq_program: block walk did not return within the step budget (a back-edge slipped through?)")
5733}
5734
5735/// The whole-program fabric: a straight-line `Main`'s cells plus the ordered `Src`s it `Show`s. Evaluating
5736/// it yields the program's output TAPE, compared against the VM's stdout — this is what covers `Show` and
5737/// validates the whole fabric end-to-end (calls inlined, collections/structs/enums on the shared heap).
5738#[derive(Clone, Debug)]
5739pub struct WholeProgramFabric {
5740    pub cells: Vec<LoopCell>,
5741    /// Each `Show`n value's driving `Src`, whether it renders as TEXT (a char-list) vs an Int, and the
5742    /// policy GRANT guard (`Some(0/1 Src)`) in force at that `Show` — `eval_whole_program` halts the tape at
5743    /// the first guard that evaluates false (a denied `Check`). `None` = ungated (always prints).
5744    pub outputs: Vec<(Src, bool, Option<Src>)>,
5745}
5746
5747/// Lower a program's `Main` (`code[0..]` up to its terminating `Halt`) to a whole-program fabric.
5748/// STRAIGHT-LINE only: a top-level branch would make a `Show` conditional, which the combinational output
5749/// tape cannot honor, so it is refused loudly. Calls inside Main inline normally (their branches if-convert).
5750pub fn whole_program_fabric_from_source(src: &str) -> WholeProgramFabric {
5751    crate::ui_bridge::with_parsed_program(src, |parsed, interner| {
5752        let (stmts, types, pol) = parsed.expect("parse failed");
5753        let mut program = crate::vm::Compiler::compile_with_types(stmts, interner, Some(types))
5754            .expect("compile failed");
5755        // The `PolicyRegistry` lives in the PARSE result, not the `CompiledProgram`, so a `Check`'s
5756        // condition is extracted to the owned key form HERE (where the interner + registry are in scope)
5757        // and threaded into the build; without it a `CheckPolicy` would have no condition to model.
5758        let policies = extract_policy_conditions(&pol, interner, &program);
5759        // Coalesce the VM compiler's register-move cascades before spatial lowering — the fabric places
5760        // final RTL with no downstream optimizer. Applied on the SOURCE→fabric path only (not the public
5761        // `whole_program_fabric_from_program`, whose hand-built-bytecode tests assert exact op streams).
5762        // Semantics-preserving IR→IR (peephole_soundness_lock proves `== VM`).
5763        crate::vm::peephole::peephole(&mut program);
5764        whole_program_fabric_from_program_with_policies(&program, &policies)
5765    })
5766}
5767
5768/// Extract the SINGLE-FIELD policy conditions the fabric can model from a program's parse-time
5769/// `PolicyRegistry` into the owned key form the `CheckPolicy` arm looks up: `(subject type-name const idx,
5770/// predicate/action symbol index, is_capability) -> (subject field slot, expected value)`. A condition is
5771/// modeled iff it is `FieldBool`, or `FieldEquals` on a Bool/Int field (expected value type-directed by the
5772/// field's declared `BoundaryType`, matching the VM's type-directed compare); every compound / text /
5773/// object-cross-field / predicate-indirection condition is DROPPED, so the `CheckPolicy` arm panics on it
5774/// rather than silently granting (a wrong grant is a security hole — rule 8).
5775fn extract_policy_conditions(
5776    registry: &crate::analysis::PolicyRegistry,
5777    interner: &crate::intern::Interner,
5778    program: &crate::vm::CompiledProgram,
5779) -> Vec<((u32, u32, bool), PolicyGrant)> {
5780    let mut out = Vec::new();
5781    for (subj_type, preds) in registry.iter_predicates() {
5782        for pred in preds {
5783            push_policy_condition(
5784                &mut out,
5785                interner,
5786                program,
5787                *subj_type,
5788                pred.predicate_name,
5789                false,
5790                &pred.condition,
5791            );
5792        }
5793    }
5794    for (subj_type, caps) in registry.iter_capabilities() {
5795        for cap in caps {
5796            push_policy_condition(
5797                &mut out,
5798                interner,
5799                program,
5800                *subj_type,
5801                cap.action,
5802                true,
5803                &cap.condition,
5804            );
5805        }
5806    }
5807    out
5808}
5809
5810/// Resolve one policy condition to its modeled `(field slot, expected i64)` and push an entry for EVERY
5811/// const index that names the subject type (a type name can be interned at more than one index — mirror
5812/// `STRUCT_FIELD_POS`), so whichever const `REG_TYPE` holds for the subject register resolves. Bool→0/1,
5813/// Int→the value, type-directed by the field's declared `BoundaryType` exactly as the VM's `match field_val`
5814/// is. Anything unmodeled (text/float/compound/indirect) returns without pushing — the arm then refuses.
5815#[allow(clippy::too_many_arguments)]
5816fn push_policy_condition(
5817    out: &mut Vec<((u32, u32, bool), PolicyGrant)>,
5818    interner: &crate::intern::Interner,
5819    program: &crate::vm::CompiledProgram,
5820    subj_type: crate::intern::Symbol,
5821    predicate: crate::intern::Symbol,
5822    is_capability: bool,
5823    cond: &crate::analysis::PolicyCondition,
5824) {
5825    use crate::analysis::PolicyCondition;
5826    use crate::vm::BoundaryType;
5827    let tname = interner.resolve(subj_type);
5828    let sd = match program.struct_types.iter().find(|s| s.name == tname) {
5829        Some(sd) => sd,
5830        None => return,
5831    };
5832    // Resolve a field NAME symbol to its 0-based slot in this struct type.
5833    let slot_of = |field: crate::intern::Symbol| -> Option<usize> {
5834        let fname = interner.resolve(field);
5835        sd.fields.iter().position(|(n, _)| n == fname)
5836    };
5837    let grant = match cond {
5838        // `field` (a Bool) is implicitly `== value`. (Not currently emitted by the parser — every bool
5839        // condition arrives as `FieldEquals … equals true` — but modeled for completeness.)
5840        PolicyCondition::FieldBool { field, value } => match slot_of(*field) {
5841            Some(slot) => PolicyGrant::IntEq {
5842                slot,
5843                expected: i64::from(*value),
5844            },
5845            None => return,
5846        },
5847        PolicyCondition::FieldEquals {
5848            field,
5849            value,
5850            is_string_literal,
5851        } => {
5852            let slot = match slot_of(*field) {
5853                Some(s) => s,
5854                None => return,
5855            };
5856            let expected = interner.resolve(*value);
5857            match &sd.fields[slot].1 {
5858                // The VM refuses a quoted string against a Bool/Int field (`if *is_string_literal { false }`)
5859                // and denies any literal it can't parse to that type (`.unwrap_or(false)`) — both always-deny.
5860                BoundaryType::Bool => {
5861                    if *is_string_literal {
5862                        PolicyGrant::AlwaysDeny
5863                    } else {
5864                        match expected.parse::<bool>() {
5865                            Ok(b) => PolicyGrant::IntEq {
5866                                slot,
5867                                expected: i64::from(b),
5868                            },
5869                            Err(_) => PolicyGrant::AlwaysDeny,
5870                        }
5871                    }
5872                }
5873                BoundaryType::Int => {
5874                    if *is_string_literal {
5875                        PolicyGrant::AlwaysDeny
5876                    } else {
5877                        match expected.parse::<i64>() {
5878                            Ok(n) => PolicyGrant::IntEq { slot, expected: n },
5879                            Err(_) => PolicyGrant::AlwaysDeny,
5880                        }
5881                    }
5882                }
5883                // A Text field ALWAYS string-compares on the VM (`s.as_str() == expected`), regardless of
5884                // `is_string_literal` — a bounded char-list equality on the fabric.
5885                BoundaryType::Text => PolicyGrant::TextEq {
5886                    slot,
5887                    expected: expected.as_bytes().to_vec(),
5888                },
5889                _ => return, // Float / handle-typed field equality — a follow-on
5890            }
5891        }
5892        _ => return, // And / Or / Predicate / ObjectFieldEquals / SubjectFieldEqualsObjectField
5893    };
5894    for (i, c) in program.constants.iter().enumerate() {
5895        if matches!(c, crate::vm::Constant::Text(n) if n == tname) {
5896            out.push((
5897                (i as u32, predicate.index() as u32, is_capability),
5898                grant.clone(),
5899            ));
5900        }
5901    }
5902}
5903
5904/// Lower an already-COMPILED program to a whole-program fabric. Shared by the source path and by the
5905/// HAND-AUTHORED-bytecode witnesses (`codegen_sva/rtl.rs`'s precedent): the reversible ALU ops
5906/// (`RevXor`/`RevNeg`/`RevAdd`/`RevSub`) and the scheduler/CRDT-internal ops (`ChanClose`/`CrdtJoin`) that
5907/// no LOGOS source emits are still real ISA ops the soft-core executes, so their fabric tile is proven by
5908/// building a raw `CompiledProgram` (op + `Show`) and differencing the fabric tape against the VM oracle
5909/// (`Vm::new(&prog).run(); vm.output()`) — never a hand-coded golden. Hand-authored bytecode carries no
5910/// `PolicyRegistry`, so it runs with NO modeled conditions (a `CheckPolicy` in it would refuse loudly).
5911pub fn whole_program_fabric_from_program(
5912    program: &crate::vm::CompiledProgram,
5913) -> WholeProgramFabric {
5914    whole_program_fabric_from_program_with_policies(program, &[])
5915}
5916
5917/// [`whole_program_fabric_from_program`] plus the program's extracted policy conditions (see
5918/// [`extract_policy_conditions`]). `setup_program_thread_locals` clears `POLICY_CONDITIONS`, then these are
5919/// installed so the `CheckPolicy` arm can compile each `Check`'s grant predicate.
5920fn whole_program_fabric_from_program_with_policies(
5921    program: &crate::vm::CompiledProgram,
5922    policies: &[((u32, u32, bool), PolicyGrant)],
5923) -> WholeProgramFabric {
5924    use crate::vm::Op;
5925    use std::collections::HashMap;
5926    setup_program_thread_locals(program);
5927    POLICY_CONDITIONS.with(|m| {
5928        let mut m = m.borrow_mut();
5929        for (k, v) in policies {
5930            m.insert(*k, v.clone());
5931        }
5932    });
5933
5934    let mut body: Vec<Op> = Vec::new();
5935    for op in &program.code {
5936        match op {
5937            Op::Halt | Op::ReturnNothing => break,
5938            Op::Jump { .. } | Op::JumpIfFalse { .. } | Op::JumpIfTrue { .. } => panic!(
5939                "whole-program fabric: Main has a branch — a conditional Show cannot ride the \
5940                 combinational output tape (straight-line Main only)"
5941            ),
5942            _ => body.push(*op),
5943        }
5944    }
5945    let mut reg: HashMap<u16, Src> = HashMap::new();
5946    let mut cells: Vec<LoopCell> = Vec::new();
5947    let mut preds: HashMap<u16, (CmpKind, Src, Src)> = HashMap::new();
5948    exec_ops(&body, &mut reg, &mut cells, &mut preds, &program.constants);
5949    let outputs = SHOW_OUTPUTS.with(|o| o.borrow().clone());
5950    WholeProgramFabric { cells, outputs }
5951}
5952
5953/// Run a whole-program fabric to its output tape — each `Show`n value RENDERED as the VM would print it: a
5954/// text char-list decoded to its string, an Int as decimal. The strings are compared to the VM's stdout lines.
5955pub fn eval_whole_program(wpf: &WholeProgramFabric) -> Vec<String> {
5956    let mut heap: Vec<i64> = Vec::new();
5957    let vals = eval_cells(&wpf.cells, &[], &mut heap);
5958    let mut out = Vec::with_capacity(wpf.outputs.len());
5959    for (s, is_text, guard) in &wpf.outputs {
5960        // A DENIED policy `Check` halts the program (VM returns `Err`), so this `Show` and every one after it
5961        // is suppressed. Guards are monotone-ANDed forward, so the first false one is the halt point.
5962        if let Some(g) = guard {
5963            if g.eval(&[], &vals) == 0 {
5964                break;
5965            }
5966        }
5967        let v = s.eval(&[], &vals);
5968        out.push(if *is_text {
5969            let ptr = v.max(0) as usize;
5970            let len = heap.get(ptr).copied().unwrap_or(0).max(0) as usize;
5971            let bytes: Vec<u8> = (0..len)
5972                .map(|i| heap.get(ptr + 1 + i).copied().unwrap_or(0) as u8)
5973                .collect();
5974            String::from_utf8_lossy(&bytes).into_owned()
5975        } else {
5976            format!("{v}")
5977        });
5978    }
5979    out
5980}
5981
5982/// Emit the decision as a fixed COMBINATIONAL dataflow circuit — one `wire` per SSA cell, arith and
5983/// predicated-select tiles, no clock. 64-bit so its wrapping arithmetic is bit-identical to the VM.
5984pub fn emit_decision_fabric_verilog(name: &str, df: &DecisionFabric) -> String {
5985    let name_in = |k: usize| format!("in{k}");
5986    let name_val = |k: usize| format!("v{k}");
5987    let inputs = (0..df.n_in)
5988        .map(|k| format!("input [63:0] in{k}"))
5989        .collect::<Vec<_>>()
5990        .join(", ");
5991    let mut body = String::new();
5992    for (i, c) in df.cells.iter().enumerate() {
5993        let rhs = match c {
5994            LoopCell::Arith { op, a, b } => {
5995                op.verilog(&src_named(a, &name_in, &name_val), &src_named(b, &name_in, &name_val))
5996            }
5997            LoopCell::VectorArith { op, ty, a, b } => {
5998                op.verilog(*ty, &src_named(a, &name_in, &name_val), &src_named(b, &name_in, &name_val))
5999            }
6000            LoopCell::Func { unit, a, b } => unit.verilog(&src_named(a, &name_in, &name_val), &src_named(b, &name_in, &name_val)),
6001            LoopCell::MemRead { .. } | LoopCell::HeapRead { .. } | LoopCell::HeapWrite { .. } | LoopCell::HeapAlloc { .. } => panic!(
6002                "MemRead RTL emission (a scratchpad-read tile driven by the induction counter) is the \
6003                 P2 scratchpad follow-on; the sequence-loop LOWERING is proven via eval_loop_fabric_seq"
6004            ),
6005            LoopCell::Select { cmp, ca, cb, t, e } => format!(
6006                "($signed({}) {} $signed({})) ? {} : {}",
6007                src_named(ca, &name_in, &name_val),
6008                cmp.verilog(),
6009                src_named(cb, &name_in, &name_val),
6010                src_named(t, &name_in, &name_val),
6011                src_named(e, &name_in, &name_val),
6012            ),
6013        };
6014        body.push_str(&format!("  wire [63:0] v{i} = {rhs};\n"));
6015    }
6016    format!(
6017        "// decision_{name} — the HFT decision if-CONVERTED to a combinational select-DAG.\n\
6018         // Every `If` is a predicated Select (mux) tile; the answer settles in ONE clock — no PC,\n\
6019         // no fetch, no dispatch, no loop counter. Futamura P1: the decision IS the circuit.\n\
6020         // Lowered from the REAL compiler; 64-bit, so it wraps identically to the VM's Int; proven\n\
6021         // == the running VM by iverilog.\n\
6022         module decision_{name}({inputs}, output [63:0] result);\n\
6023         {body}  assign result = {res};\n\
6024         endmodule\n",
6025        res = src_named(&df.result, &name_in, &name_val),
6026    )
6027}
6028
6029/// A self-checking testbench for a COMBINATIONAL `decision_{name}` fabric: drive `n` input vectors (each
6030/// `n_in` 64-bit words) from `dec_{name}_in.hex`, compare the settled `result` against the VM oracle in
6031/// `dec_{name}_out.hex`, and print PASS/FAIL. The combinational analog of [`emit_state_plane_testbench`] — no
6032/// clock, one `#1` settle per vector. Lets any Func/Cmp/Arith decision tile be iverilog-exercised ≡ the VM.
6033pub fn emit_decision_fabric_testbench_dummy(name: &str, n_in: usize, n: usize) -> String {
6034    let decls = (0..n_in)
6035        .map(|k| format!("  reg [63:0] in{k};\n"))
6036        .collect::<String>();
6037    let ports = (0..n_in)
6038        .map(|k| format!(".in{k}(in{k}), "))
6039        .collect::<String>();
6040    let drive = (0..n_in)
6041        .map(|k| format!("      in{k} = oin[i * {n_in} + {k}];\n"))
6042        .collect::<String>();
6043    format!(
6044        r#"// tb_dec_{name} — the combinational decision fabric vs the RUNNING VM's per-vector output.
6045`timescale 1ns/1ps
6046module tb_dec_{name};
6047{decls}  reg [63:0] oin  [0:{in_last}];
6048  reg [63:0] oout [0:{last}];
6049  wire [63:0] result;
6050  integer i, errors = 0;
6051  decision_{name} dut({ports}.result(result));
6052  initial begin
6053    $readmemh("dec_{name}_in.hex", oin);
6054    $readmemh("dec_{name}_out.hex", oout);
6055    for (i = 0; i < {n}; i = i + 1) begin
6056{drive}      #1;
6057      if (result !== oout[i]) begin errors = errors + 1; $display("FAIL vec %0d: got %0d", i, $signed(result)); end
6058    end
6059    if (errors == 0) $display("PASS: {n} decision-fabric evaluations match the VM (the tile IS the circuit)");
6060    else $display("FAIL: %0d decision mismatches", errors);
6061    $finish;
6062  end
6063endmodule
6064"#,
6065        in_last = n_in * n - 1,
6066        last = n - 1,
6067    )
6068}
6069
6070/// Emit a general SEQUENCED datapath over a `DecisionFabric`'s cells — the LATENCY-INSENSITIVE fabric. A
6071/// combinational `Arith`/`Select` cell settles in one step; a `Func` DIVISION cell (`DivS`/`ModS`/`FloorDivS`)
6072/// ISSUES the multi-cycle `shared_divider` and STALLS until its `done` (a variable-latency TILE). This is the
6073/// same cell stream `emit_decision_fabric_verilog` wires as a pure combinational DAG, but with the sequential
6074/// functional units as first-class multi-cycle tiles — so it composes with the fpu.v FPU (Phase B) and the
6075/// block-walk sequencer (Phase C). Proven == `eval_decision_fabric` == the VM by iverilog. (Phase A: Arith/
6076/// Select/division; float/pow/mem are the following increments and PANIC here as the honest boundary.)
6077pub fn emit_seq_datapath_verilog(name: &str, df: &DecisionFabric) -> String {
6078    let name_in = |k: usize| format!("in{k}");
6079    let name_val = |k: usize| format!("R[{k}]");
6080    let src = |s: &Src| src_named(s, &name_in, &name_val);
6081    let m = df.cells.len();
6082    assert!(m > 0, "a sequenced datapath needs ≥1 cell");
6083    let inputs = (0..df.n_in)
6084        .map(|k| format!("input [63:0] in{k}"))
6085        .collect::<Vec<_>>()
6086        .join(", ");
6087    let inputs = if inputs.is_empty() {
6088        String::new()
6089    } else {
6090        format!("{inputs}, ")
6091    };
6092
6093    let mut arms = String::new();
6094    for (i, c) in df.cells.iter().enumerate() {
6095        arms.push_str(&format!("      {i}: begin {} end\n", seq_cell_arm(c, &src)));
6096    }
6097
6098    format!(
6099        r#"// seq_{name} — a general SEQUENCED datapath over the decision fabric's cells (LATENCY-INSENSITIVE).
6100// Combinational Arith/Select/IntToF/FFloor settle in one step; a Func DIVISION cell issues the multi-cycle
6101// shared_divider, and a FLOAT arithmetic cell issues the shared fpu.v unit (fp_addsub/mul/div/sqrt) — each a
6102// variable-latency TILE that stalls the datapath until its `done`. Proven == eval_decision_fabric == the VM by
6103// iverilog (fpu.v + shared_divider.v are the real synthesizable units).
6104module seq_{name}(input clk, input rst, {inputs}output [63:0] result, output done);
6105  localparam M = {m};
6106  (* ram_style="registers" *) reg [63:0] R [0:M-1];
6107  reg [31:0] step;
6108  reg running, issued;
6109  reg is_comb; reg [63:0] comb_y; reg [63:0] ua, ub; reg [3:0] usel;
6110{converters}  always @(*) begin
6111    is_comb=1'b1; comb_y=64'd0; ua=64'd0; ub=64'd0; usel=4'd0;
6112    case (step)
6113{arms}      default: ;
6114    endcase
6115  end
6116{units}  always @(posedge clk) begin
6117    if (rst) begin step<=32'd0; running<=(M>0); issued<=1'b0; end
6118    else if (running) begin
6119      if (is_comb) begin
6120        R[step] <= comb_y;
6121        if (step==M-1) running<=1'b0; else step<=step+32'd1;
6122      end else if (!issued) begin
6123        issued <= 1'b1;                       // the start pulse fired this cycle
6124      end else if (mc_done) begin
6125        R[step] <= mc_y; issued<=1'b0;
6126        if (step==M-1) running<=1'b0; else step<=step+32'd1;
6127      end
6128    end
6129  end
6130  assign done   = !running;
6131  assign result = {res};
6132endmodule
6133"#,
6134        res = src(&df.result),
6135        converters = I64_F64_CONVERTERS,
6136        units = seq_units_for(&df.cells),
6137    )
6138}
6139
6140/// The `case(step)` arm body for one `LoopCell` in a sequenced datapath: set `is_comb`/`comb_y` for a
6141/// combinational cell (Arith/Select/IntToF/FFloor) or `ua`/`ub`/`usel` for a multi-cycle cell (division 1/2/3,
6142/// float 5..9). Shared by `emit_seq_datapath_verilog` and `emit_seq_block_verilog`.
6143fn seq_cell_arm(c: &LoopCell, src: &dyn Fn(&Src) -> String) -> String {
6144    match c {
6145        LoopCell::Arith { op, a, b } => {
6146            format!("is_comb=1'b1; comb_y={};", op.verilog(&src(a), &src(b)))
6147        }
6148        LoopCell::Select { cmp, ca, cb, t, e } => format!(
6149            "is_comb=1'b1; comb_y=($signed({}) {} $signed({})) ? {} : {};",
6150            src(ca),
6151            cmp.verilog(),
6152            src(cb),
6153            src(t),
6154            src(e)
6155        ),
6156        LoopCell::Func { unit, a, b } => match unit {
6157            FuncUnit::DivS => format!("is_comb=1'b0; ua={}; ub={}; usel=4'd1;", src(a), src(b)),
6158            FuncUnit::ModS => format!("is_comb=1'b0; ua={}; ub={}; usel=4'd2;", src(a), src(b)),
6159            FuncUnit::FloorDivS => {
6160                format!("is_comb=1'b0; ua={}; ub={}; usel=4'd3;", src(a), src(b))
6161            }
6162            FuncUnit::FAddF => format!("is_comb=1'b0; ua={}; ub={}; usel=4'd5;", src(a), src(b)),
6163            FuncUnit::FSubF => format!("is_comb=1'b0; ua={}; ub={}; usel=4'd6;", src(a), src(b)),
6164            FuncUnit::FMulF => format!("is_comb=1'b0; ua={}; ub={}; usel=4'd7;", src(a), src(b)),
6165            FuncUnit::FDivF => format!("is_comb=1'b0; ua={}; ub={}; usel=4'd8;", src(a), src(b)),
6166            FuncUnit::FSqrtF => format!("is_comb=1'b0; ua={}; ub=64'd0; usel=4'd9;", src(a)),
6167            FuncUnit::IntToF => format!("is_comb=1'b1; comb_y=i64_to_f64({});", src(a)),
6168            FuncUnit::FFloorF => format!("is_comb=1'b1; comb_y=f64_floor_i64({});", src(a)),
6169            // IEEE-754 float COMPARES → the shared combinational fp_cmp (usel 10..15). Operands are held stable
6170            // while the step runs, so the compare resolves as a 1-cycle multi-cycle op (no combinational hazard).
6171            FuncUnit::FLtF => format!("is_comb=1'b0; ua={}; ub={}; usel=4'd10;", src(a), src(b)),
6172            FuncUnit::FGtF => format!("is_comb=1'b0; ua={}; ub={}; usel=4'd11;", src(a), src(b)),
6173            FuncUnit::FLeF => format!("is_comb=1'b0; ua={}; ub={}; usel=4'd12;", src(a), src(b)),
6174            FuncUnit::FGeF => format!("is_comb=1'b0; ua={}; ub={}; usel=4'd13;", src(a), src(b)),
6175            FuncUnit::FEqF => format!("is_comb=1'b0; ua={}; ub={}; usel=4'd14;", src(a), src(b)),
6176            FuncUnit::FNeF => format!("is_comb=1'b0; ua={}; ub={}; usel=4'd15;", src(a), src(b)),
6177            // integer POWER → the sequential repeat-multiply FSM (usel 4): a `b`-step wrapping-64-bit tile that
6178            // stalls the datapath until its `done`, exactly like the divider (a variable-latency tile).
6179            FuncUnit::Pow => format!("is_comb=1'b0; ua={}; ub={}; usel=4'd4;", src(a), src(b)),
6180            // The COMBINATIONAL tiles settle in ONE step (`is_comb`), reusing the grid's already-iverilog-proven
6181            // `FuncUnit::verilog()` verbatim: PopCount (bit-adder tree), MulHiU / MulOvfS (128-bit product), and
6182            // ApproxEqF (exact-rational tolerance). One expression, bit-identical to `FuncUnit::eval`.
6183            FuncUnit::PopCount | FuncUnit::MulHiU | FuncUnit::MulOvfS | FuncUnit::ApproxEqF => {
6184                format!("is_comb=1'b1; comb_y={};", unit.verilog(&src(a), &src(b)))
6185            }
6186        },
6187        other => panic!("seq datapath: {other:?} (Mem/Heap) is a state-plane follow-on"),
6188    }
6189}
6190
6191/// Emit the multi-cycle functional units + result muxing for a sequenced datapath — but ONLY the units the
6192/// `cells` actually use (a division-only fabric gets NO fpu.v, a compare-free float fabric gets no fp_cmp, …),
6193/// so every fabric instantiates exactly its own tiles. Unused unit outputs are stubbed (`_y=0`, `_done=1`) so
6194/// the `mc_done`/`mc_y` muxes stay uniform. usel: 1/2/3 div/mod/floordiv, 5..9 fadd/fsub/fmul/fdiv/fsqrt,
6195/// 10..15 the six IEEE-754 compares (fp_cmp). Drives `mc_done`/`mc_y` off `usel`/`ua`/`ub`/`pulse`.
6196fn seq_units_for(cells: &[LoopCell]) -> String {
6197    let (mut div, mut addsub, mut mul, mut fdiv, mut sqrt, mut cmp, mut pow) =
6198        (false, false, false, false, false, false, false);
6199    for c in cells {
6200        if let LoopCell::Func { unit, .. } = c {
6201            match unit {
6202                FuncUnit::DivS | FuncUnit::ModS | FuncUnit::FloorDivS => div = true,
6203                FuncUnit::Pow => pow = true,
6204                FuncUnit::FAddF | FuncUnit::FSubF => addsub = true,
6205                FuncUnit::FMulF => mul = true,
6206                FuncUnit::FDivF => fdiv = true,
6207                FuncUnit::FSqrtF => sqrt = true,
6208                FuncUnit::FLtF
6209                | FuncUnit::FGtF
6210                | FuncUnit::FLeF
6211                | FuncUnit::FGeF
6212                | FuncUnit::FEqF
6213                | FuncUnit::FNeF => cmp = true,
6214                _ => {}
6215            }
6216        }
6217    }
6218    let mut s = String::new();
6219    s.push_str("  wire is_div   = (usel>=4'd1 && usel<=4'd3);\n");
6220    s.push_str("  wire is_mod   = (usel==4'd2);\n  wire is_floor = (usel==4'd3);\n");
6221    s.push_str("  wire is_pow   = (usel==4'd4);\n");
6222    s.push_str("  wire is_fadd  = (usel==4'd5)||(usel==4'd6);\n  wire is_fsub = (usel==4'd6);\n");
6223    s.push_str("  wire is_fmul  = (usel==4'd7);\n  wire is_fdiv = (usel==4'd8);\n  wire is_fsqrt = (usel==4'd9);\n");
6224    s.push_str("  wire is_cmp   = (usel>=4'd10);\n");
6225    s.push_str("  wire pulse = running && !issued && !is_comb;\n");
6226    if div {
6227        s.push_str("  wire d_done; wire [63:0] d_q, d_r;\n");
6228        s.push_str("  shared_divider #(.W(64)) u_div(.clk(clk), .rst(rst), .start(pulse && is_div && (ub!=64'd0)), .a(ua), .b(ub), .q(d_q), .r(d_r), .done(d_done));\n");
6229        s.push_str("  wire signed [63:0] d_floor_v = $signed(d_q) - (((d_r!=64'd0) && (ua[63]^ub[63])) ? 64'sd1 : 64'sd0);\n");
6230        s.push_str("  wire [63:0] div_y = (ub==64'd0) ? 64'd0 : is_mod ? d_r : is_floor ? d_floor_v : d_q;\n");
6231        s.push_str("  wire div_done_g = (ub==64'd0) ? 1'b1 : d_done;\n");
6232    } else {
6233        s.push_str("  wire [63:0] div_y = 64'd0; wire div_done_g = 1'b1;\n");
6234    }
6235    if addsub {
6236        s.push_str("  wire fa_done; wire [63:0] fa_y;\n  fp_addsub u_fa(.clk(clk),.rst(rst),.start(pulse && is_fadd),.a(ua),.b_in(ub),.sub(is_fsub),.y(fa_y),.done(fa_done));\n");
6237    } else {
6238        s.push_str("  wire [63:0] fa_y = 64'd0; wire fa_done = 1'b1;\n");
6239    }
6240    if mul {
6241        s.push_str("  wire fm_done; wire [63:0] fm_y;\n  fp_mul u_fm(.clk(clk),.rst(rst),.start(pulse && is_fmul),.a(ua),.b(ub),.y(fm_y),.done(fm_done));\n");
6242    } else {
6243        s.push_str("  wire [63:0] fm_y = 64'd0; wire fm_done = 1'b1;\n");
6244    }
6245    if fdiv {
6246        s.push_str("  wire fd_done; wire [63:0] fd_y;\n  fp_div u_fd(.clk(clk),.rst(rst),.start(pulse && is_fdiv),.a(ua),.b(ub),.y(fd_y),.done(fd_done));\n");
6247    } else {
6248        s.push_str("  wire [63:0] fd_y = 64'd0; wire fd_done = 1'b1;\n");
6249    }
6250    if sqrt {
6251        s.push_str("  wire fs_done; wire [63:0] fs_y;\n  fp_sqrt u_fs(.clk(clk),.rst(rst),.start(pulse && is_fsqrt),.a(ua),.y(fs_y),.done(fs_done));\n");
6252    } else {
6253        s.push_str("  wire [63:0] fs_y = 64'd0; wire fs_done = 1'b1;\n");
6254    }
6255    if cmp {
6256        s.push_str("  wire cmp_swap = (usel==4'd11)||(usel==4'd13);\n");
6257        s.push_str(
6258            "  wire [63:0] fcm_a = cmp_swap ? ub : ua; wire [63:0] fcm_b = cmp_swap ? ua : ub;\n",
6259        );
6260        s.push_str(
6261            "  wire fp_lt, fp_eq;\n  fp_cmp u_fcm(.a(fcm_a), .b(fcm_b), .lt(fp_lt), .eq(fp_eq));\n",
6262        );
6263        s.push_str("  wire [63:0] cmp_y = (usel==4'd10||usel==4'd11) ? {63'd0, fp_lt} : (usel==4'd12||usel==4'd13) ? {63'd0, fp_lt|fp_eq} : (usel==4'd14) ? {63'd0, fp_eq} : (usel==4'd15) ? {63'd0, ~fp_eq} : 64'd0;\n");
6264    } else {
6265        s.push_str("  wire [63:0] cmp_y = 64'd0;\n");
6266    }
6267    if pow {
6268        // Sequential integer POWER — a `b`-step repeat-multiply, wrapping 64-bit (bit-identical to the eval
6269        // `a.wrapping_pow(max(0,b) as u32)`). `pw_done` is (re-)armed on the issue `pulse` — for `b<=0` it is
6270        // high the same cycle with `pw_acc==1`, so the stale-done hazard cannot fire (mc_done is first sampled
6271        // the cycle AFTER pulse). For `b>0` it iterates `b` multiplies then raises `pw_done`.
6272        s.push_str("  reg [63:0] pw_acc, pw_base, pw_cnt; reg pw_busy, pw_done;\n");
6273        s.push_str("  always @(posedge clk) begin\n");
6274        s.push_str("    if (rst) begin pw_busy <= 1'b0; pw_done <= 1'b0; end\n");
6275        s.push_str("    else if (pulse && is_pow) begin\n");
6276        s.push_str("      pw_acc  <= 64'd1; pw_base <= ua;\n");
6277        s.push_str("      pw_cnt  <= ($signed(ub) <= 0) ? 64'd0 : ub;\n");
6278        s.push_str("      pw_busy <= ($signed(ub) >  0); pw_done <= ($signed(ub) <= 0);\n");
6279        s.push_str("    end else if (pw_busy) begin\n");
6280        s.push_str("      pw_acc <= pw_acc * pw_base;\n");
6281        s.push_str("      if (pw_cnt <= 64'd1) begin pw_busy <= 1'b0; pw_done <= 1'b1; end else pw_cnt <= pw_cnt - 64'd1;\n");
6282        s.push_str("    end\n  end\n");
6283    } else {
6284        s.push_str("  wire [63:0] pw_acc = 64'd0; wire pw_done = 1'b1;\n");
6285    }
6286    // A compare's fp_cmp is combinational over the held operands, so it is ready one cycle after issue (mc_done=1).
6287    s.push_str("  wire        mc_done = is_div ? div_done_g : is_pow ? pw_done : is_cmp ? 1'b1 : is_fadd ? fa_done : is_fmul ? fm_done : is_fdiv ? fd_done : is_fsqrt ? fs_done : 1'b1;\n");
6288    s.push_str("  wire [63:0] mc_y    = is_div ? div_y : is_pow ? pw_acc : is_cmp ? cmp_y : is_fadd ? fa_y : is_fmul ? fm_y : is_fdiv ? fd_y : is_fsqrt ? fs_y : 64'd0;\n");
6289    s
6290}
6291
6292/// Emit a per-block SEQUENCED datapath (`seqblk_{name}`) for a `CfgBlock`: on a `start` pulse it walks the
6293/// block's SSA cells (combinational in one step, multi-cycle division/float via the shared units), then holds
6294/// `done` high with every `needed` output (each register write's `Src`, then the terminator operand) driven
6295/// from the settled cell/register values. The block-walk sequencer starts it, waits `done`, latches the writes,
6296/// and evaluates the terminator. Multi-cycle TILES are first-class here.
6297pub fn emit_seq_block_verilog(name: &str, block: &CfgBlock) -> String {
6298    emit_seq_block_verilog_sp(name, block, false)
6299}
6300
6301/// True iff any cell is a shared-heap STATE-PLANE cell (needs the BRAM heap port).
6302fn cell_is_heap(c: &LoopCell) -> bool {
6303    matches!(
6304        c,
6305        LoopCell::HeapRead { .. }
6306            | LoopCell::HeapWrite { .. }
6307            | LoopCell::HeapAlloc { .. }
6308            | LoopCell::MemRead { .. }
6309    )
6310}
6311
6312/// [`emit_seq_block_verilog`] with an optional STATE-PLANE port. When `with_heap`, the block exposes a shared-heap
6313/// interface (`heap_raddr`→`heap_rdata` async read, `heap_we`/`heap_waddr`/`heap_wdata` sync write, `alloc_req`/
6314/// `alloc_size`→`hp_in` bump-allocate) that the whole-program top wires to ONE register-file heap threaded across
6315/// the block walk. A `HeapRead` is a combinational step (`R[i]<=heap_rdata` over the driven `heap_raddr`); a
6316/// `HeapWrite` asserts the write port for its step (value = the written word); a `HeapAlloc` returns the current
6317/// top `hp_in` and pulses `alloc_req`. `heap_we`/`alloc_req` are gated by `running`, so an inactive block never
6318/// mutates the heap. Bit-identical to the VM's `[len, e1..eN]` 1-based layout (proven ≡VM by iverilog).
6319fn emit_seq_block_verilog_sp(name: &str, block: &CfgBlock, with_heap: bool) -> String {
6320    let name_in = |k: usize| format!("in{k}");
6321    let name_val = |k: usize| format!("R[{k}]");
6322    let src = |s: &Src| src_named(s, &name_in, &name_val);
6323    let m = block.body.len();
6324    let n_in = block.inputs.len();
6325    let mut needed: Vec<Src> = block.writes.iter().map(|(_, s)| *s).collect();
6326    match &block.term {
6327        Term::Jump(_) => {}
6328        Term::Branch { cond, .. } => needed.push(*cond),
6329        Term::Return(s) => needed.push(*s),
6330    }
6331    let inports = (0..n_in)
6332        .map(|k| format!("input [63:0] in{k}"))
6333        .collect::<Vec<_>>()
6334        .join(", ");
6335    let inports = if inports.is_empty() {
6336        String::new()
6337    } else {
6338        format!("{inports}, ")
6339    };
6340    let outports = (0..needed.len())
6341        .map(|j| format!("output [63:0] o{j}"))
6342        .collect::<Vec<_>>()
6343        .join(", ");
6344    let outports = if outports.is_empty() {
6345        String::new()
6346    } else {
6347        format!("{outports}, ")
6348    };
6349    let outassign = needed
6350        .iter()
6351        .enumerate()
6352        .map(|(j, s)| format!("  assign o{j} = {};\n", src(s)))
6353        .collect::<String>();
6354    // Optional STATE-PLANE port + side-band registers driven per-step by the heap cells.
6355    let heap_ports = if with_heap {
6356        "output [63:0] heap_raddr, input [63:0] heap_rdata, output heap_we, output [63:0] heap_waddr, \
6357         output [63:0] heap_wdata, output alloc_req, output [63:0] alloc_size, input [63:0] hp_in, "
6358    } else {
6359        ""
6360    };
6361    let (heap_regs, heap_defaults, heap_assigns) = if with_heap {
6362        (
6363            "  reg [63:0] heap_raddr_r, heap_waddr_r, heap_wdata_r, alloc_size_r; reg heap_we_r, alloc_req_r;\n",
6364            "    heap_raddr_r=64'd0; heap_we_r=1'b0; heap_waddr_r=64'd0; heap_wdata_r=64'd0; alloc_req_r=1'b0; alloc_size_r=64'd0;\n",
6365            "  assign heap_raddr=heap_raddr_r; assign heap_we=running && heap_we_r; assign heap_waddr=heap_waddr_r;\n\
6366             \x20 assign heap_wdata=heap_wdata_r; assign alloc_req=running && alloc_req_r; assign alloc_size=alloc_size_r;\n",
6367        )
6368    } else {
6369        ("", "", "")
6370    };
6371    let mut arms = String::new();
6372    for (i, c) in block.body.iter().enumerate() {
6373        // Heap cells settle in one combinational step, driving the side-band port; everything else via seq_cell_arm.
6374        let arm = match c {
6375            LoopCell::HeapRead { addr } => {
6376                format!(
6377                    "is_comb=1'b1; comb_y=heap_rdata; heap_raddr_r={};",
6378                    src(addr)
6379                )
6380            }
6381            LoopCell::HeapWrite { addr, val } => format!(
6382                "is_comb=1'b1; comb_y={v}; heap_we_r=1'b1; heap_waddr_r={a}; heap_wdata_r={v};",
6383                a = src(addr),
6384                v = src(val)
6385            ),
6386            LoopCell::HeapAlloc { size } => {
6387                format!(
6388                    "is_comb=1'b1; comb_y=hp_in; alloc_req_r=1'b1; alloc_size_r={};",
6389                    src(size)
6390                )
6391            }
6392            _ => seq_cell_arm(c, &src),
6393        };
6394        arms.push_str(&format!("      {i}: begin {arm} end\n"));
6395    }
6396    // A block whose body is empty (writes/term read inputs directly) is trivially done (running stays 0); its
6397    // `R[step]` references are dead but must still elaborate, so `R` is always declared with depth ≥ 1.
6398    let mdecl = format!("localparam M = {m};");
6399    let rdecl = format!(
6400        "  (* ram_style=\"registers\" *) reg [63:0] R [0:{}];\n",
6401        m.max(1) - 1
6402    );
6403    format!(
6404        r#"// seqblk_{name} — a per-block SEQUENCED datapath (LATENCY-INSENSITIVE): start pulse -> walk the cells
6405// (combinational in one step, division/float on the shared multi-cycle units, heap on the shared state plane) ->
6406// done + the block's outputs.
6407module seqblk_{name}(input clk, input rst, input start, {inports}{outports}{heap_ports}output done);
6408  {mdecl}
6409{rdecl}  reg [31:0] step;
6410  reg running, issued;
6411  reg is_comb; reg [63:0] comb_y; reg [63:0] ua, ub; reg [3:0] usel;
6412{heap_regs}{converters}  always @(*) begin
6413    is_comb=1'b1; comb_y=64'd0; ua=64'd0; ub=64'd0; usel=4'd0;
6414{heap_defaults}    case (step)
6415{arms}      default: ;
6416    endcase
6417  end
6418{units}  always @(posedge clk) begin
6419    if (rst) begin running<=1'b0; step<=32'd0; issued<=1'b0; end
6420    else if (start) begin step<=32'd0; running<=(M>0); issued<=1'b0; end
6421    else if (running) begin
6422      if (is_comb) begin
6423        R[step] <= comb_y;
6424        if (step==M-1) running<=1'b0; else step<=step+32'd1;
6425      end else if (!issued) begin
6426        issued <= 1'b1;
6427      end else if (mc_done) begin
6428        R[step] <= mc_y; issued<=1'b0;
6429        if (step==M-1) running<=1'b0; else step<=step+32'd1;
6430      end
6431    end
6432  end
6433  assign done = !running;
6434{heap_assigns}{outassign}endmodule
6435"#,
6436        converters = I64_F64_CONVERTERS,
6437        units = seq_units_for(&block.body),
6438    )
6439}
6440
6441/// Emit the DONE-AWARE whole-program sequenced fabric (`seqprog_{name}`): the per-block sequenced datapaths
6442/// (`seqblk_*`, each with multi-cycle division/float TILES) + a block-PC FSM that STARTS the current block, waits
6443/// its `done`, latches the register writes, and takes the terminator (Jump/Branch/Return). This is the
6444/// unification — one whole program (loop + division + float) on one spatial fabric, latency-insensitive.
6445/// Proven == `run_program_cfg_on_grid` == the VM by iverilog.
6446pub fn emit_seq_program_verilog(name: &str, pf: &ProgramCfgFabric) -> String {
6447    let nb = pf.blocks.len();
6448    assert!(nb >= 1, "seq program: need ≥1 block");
6449    // Does the program touch the shared state plane? If so, the top instantiates ONE register-file heap
6450    // (async read, sync write, bump allocator) threaded across the block walk and every block gets the heap port.
6451    let has_heap = pf.blocks.iter().any(|b| b.body.iter().any(cell_is_heap));
6452    // The shared heap is a bounded register file (the fabric is a bounded-heap machine by design). 64 words is
6453    // ample for the collection/string programs the state plane targets and keeps the FF budget FPGA-feasible
6454    // (64×64 = 4096 FFs vs 126k on the Nexys A7); a program that overflows it diverges from the VM (a genuine
6455    // bound, not a silent wrap) and would be caught ≡VM.
6456    const HEAP_WORDS: usize = 64;
6457    let aw = (usize::BITS - (HEAP_WORDS - 1).leading_zeros()).max(1) as usize;
6458    let nreg = {
6459        let mut max_reg = 0u16;
6460        for b in &pf.blocks {
6461            for r in &b.inputs {
6462                max_reg = max_reg.max(*r);
6463            }
6464            for (r, _) in &b.writes {
6465                max_reg = max_reg.max(*r);
6466            }
6467        }
6468        (max_reg as usize + 1).max(pf.n_params).max(1)
6469    };
6470    let pcw = {
6471        let mut b = 1;
6472        while (1usize << b) < nb {
6473            b += 1;
6474        }
6475        b
6476    };
6477
6478    let mut modules = String::new();
6479    let mut needs: Vec<(Vec<Src>, usize)> = Vec::with_capacity(nb);
6480    for (k, b) in pf.blocks.iter().enumerate() {
6481        modules.push_str(&emit_seq_block_verilog_sp(
6482            &format!("{name}_b{k}"),
6483            b,
6484            has_heap,
6485        ));
6486        modules.push('\n');
6487        let mut needed: Vec<Src> = b.writes.iter().map(|(_, s)| *s).collect();
6488        let term_off = needed.len();
6489        match &b.term {
6490            Term::Jump(_) => {}
6491            Term::Branch { cond, .. } => needed.push(*cond),
6492            Term::Return(s) => needed.push(*s),
6493        }
6494        needs.push((needed, term_off));
6495    }
6496
6497    let mut ports: Vec<String> = vec!["input clk".into(), "input rst".into()];
6498    for p in 0..pf.n_params {
6499        ports.push(format!("input [63:0] p{p}"));
6500    }
6501    ports.push("output [63:0] result".into());
6502    ports.push("output done".into());
6503
6504    let mut body = String::new();
6505    body.push_str(&format!("  reg [63:0] file [0:{}];\n", nreg - 1));
6506    body.push_str(&format!("  reg [{}:0] pc;\n", pcw - 1));
6507    body.push_str("  reg done_r; reg [63:0] result_r; reg started;\n");
6508    body.push_str("  assign done = done_r; assign result = result_r;\n");
6509    for r in 0..nreg {
6510        body.push_str(&format!("  wire [63:0] fr_{r} = file[{r}];\n"));
6511    }
6512    // The shared STATE PLANE: ONE register-file heap (async read, sync write) + a bump-pointer `hp`, threaded
6513    // across the whole block walk. `ram_style=registers` forces real FFs (the open FOSS flow drops write-ported
6514    // LUT-RAM). The active (pc-selected) block owns the heap port; inactive blocks gate `heap_we`/`alloc_req` low.
6515    if has_heap {
6516        body.push_str(&format!(
6517            "  (* ram_style=\"registers\" *) reg [63:0] heap [0:{}];\n",
6518            HEAP_WORDS - 1
6519        ));
6520        body.push_str("  reg [63:0] hp;\n");
6521        for k in 0..nb {
6522            body.push_str(&format!(
6523                "  wire [63:0] b{k}_heap_raddr, b{k}_heap_waddr, b{k}_heap_wdata, b{k}_alloc_size; wire b{k}_heap_we, b{k}_alloc_req;\n"
6524            ));
6525        }
6526        body.push_str(
6527            "  reg [63:0] act_raddr, act_waddr, act_wdata, act_size; reg act_we, act_alloc;\n",
6528        );
6529        body.push_str("  always @(*) begin\n    act_raddr=64'd0; act_waddr=64'd0; act_wdata=64'd0; act_size=64'd0; act_we=1'b0; act_alloc=1'b0;\n    case (pc)\n");
6530        for k in 0..nb {
6531            body.push_str(&format!(
6532                "      {pcw}'d{k}: begin act_raddr=b{k}_heap_raddr; act_we=b{k}_heap_we; act_waddr=b{k}_heap_waddr; act_wdata=b{k}_heap_wdata; act_alloc=b{k}_alloc_req; act_size=b{k}_alloc_size; end\n"
6533            ));
6534        }
6535        body.push_str("      default: ;\n    endcase\n  end\n");
6536        body.push_str(&format!(
6537            "  wire [63:0] heap_rdata_bus = heap[act_raddr[{}:0]];\n",
6538            aw - 1
6539        ));
6540        body.push_str("  integer hj;\n  always @(posedge clk) begin\n");
6541        body.push_str(&format!("    if (rst) begin hp<=64'd0; for (hj=0; hj<{HEAP_WORDS}; hj=hj+1) heap[hj]<=64'd0; end\n"));
6542        body.push_str("    else begin\n");
6543        body.push_str(&format!(
6544            "      if (act_we) heap[act_waddr[{}:0]] <= act_wdata;\n",
6545            aw - 1
6546        ));
6547        body.push_str("      if (act_alloc) hp <= hp + act_size;\n");
6548        body.push_str("      else if (act_we && ($signed(act_waddr) + 64'sd1 > $signed(hp))) hp <= act_waddr + 64'sd1;\n");
6549        body.push_str("    end\n  end\n");
6550    }
6551    // Per-block datapath instances (all present; only the pc-selected one is started).
6552    for (k, b) in pf.blocks.iter().enumerate() {
6553        let (needed, _) = &needs[k];
6554        for j in 0..needed.len() {
6555            body.push_str(&format!("  wire [63:0] b{k}_o{j};\n"));
6556        }
6557        body.push_str(&format!("  wire b{k}_done;\n"));
6558        let mut conns: Vec<String> = vec![
6559            ".clk(clk)".into(),
6560            ".rst(rst)".into(),
6561            format!(".start((pc=={pcw}'d{k}) && !started && !done_r)"),
6562        ];
6563        for (j, r) in b.inputs.iter().enumerate() {
6564            conns.push(format!(".in{j}(fr_{r})"));
6565        }
6566        for j in 0..needed.len() {
6567            conns.push(format!(".o{j}(b{k}_o{j})"));
6568        }
6569        if has_heap {
6570            conns.push(format!(".heap_raddr(b{k}_heap_raddr)"));
6571            conns.push(".heap_rdata(heap_rdata_bus)".into());
6572            conns.push(format!(".heap_we(b{k}_heap_we)"));
6573            conns.push(format!(".heap_waddr(b{k}_heap_waddr)"));
6574            conns.push(format!(".heap_wdata(b{k}_heap_wdata)"));
6575            conns.push(format!(".alloc_req(b{k}_alloc_req)"));
6576            conns.push(format!(".alloc_size(b{k}_alloc_size)"));
6577            conns.push(".hp_in(hp)".into());
6578        }
6579        conns.push(format!(".done(b{k}_done)"));
6580        body.push_str(&format!(
6581            "  seqblk_{name}_b{k} u_b{k}({});\n",
6582            conns.join(", ")
6583        ));
6584    }
6585    // The currently-selected block's done.
6586    body.push_str("  reg cur_done;\n  always @(*) begin cur_done = 1'b1; case (pc)\n");
6587    for k in 0..nb {
6588        body.push_str(&format!("    {pcw}'d{k}: cur_done = b{k}_done;\n"));
6589    }
6590    body.push_str("    default: cur_done = 1'b1;\n  endcase end\n");
6591
6592    // The block-walk FSM: start the block (one cycle), wait cur_done, latch writes, take the terminator.
6593    body.push_str("  always @(posedge clk) begin\n    if (rst) begin\n");
6594    body.push_str(&format!(
6595        "      pc<={pcw}'d0; done_r<=1'b0; result_r<=64'd0; started<=1'b0;\n"
6596    ));
6597    for r in 0..nreg {
6598        if r < pf.n_params {
6599            body.push_str(&format!("      file[{r}]<=p{r};\n"));
6600        } else {
6601            body.push_str(&format!("      file[{r}]<=64'd0;\n"));
6602        }
6603    }
6604    body.push_str("    end else if (!done_r) begin\n");
6605    body.push_str("      if (!started) started<=1'b1;\n");
6606    body.push_str("      else if (cur_done) begin\n        started<=1'b0;\n        case (pc)\n");
6607    for (k, b) in pf.blocks.iter().enumerate() {
6608        let (_, term_off) = &needs[k];
6609        body.push_str(&format!("          {pcw}'d{k}: begin\n"));
6610        for (i, (r, _)) in b.writes.iter().enumerate() {
6611            body.push_str(&format!("            file[{r}] <= b{k}_o{i};\n"));
6612        }
6613        match &b.term {
6614            Term::Jump(t) => body.push_str(&format!("            pc <= {pcw}'d{t};\n")),
6615            Term::Branch { then_blk, else_blk, .. } => body.push_str(&format!(
6616                "            pc <= (b{k}_o{term_off} != 64'd0) ? {pcw}'d{then_blk} : {pcw}'d{else_blk};\n"
6617            )),
6618            Term::Return(_) => {
6619                body.push_str(&format!("            result_r <= b{k}_o{term_off}; done_r <= 1'b1;\n"))
6620            }
6621        }
6622        body.push_str("          end\n");
6623    }
6624    body.push_str("          default: done_r<=1'b1;\n        endcase\n      end\n    end\n  end\n");
6625
6626    format!(
6627        "// seqprog_{name} — the whole-program SEQUENCED fabric (LATENCY-INSENSITIVE): per-block datapaths with\n\
6628         // multi-cycle division/float TILES + a block-PC FSM that starts each block, waits its done, latches the\n\
6629         // register writes, and takes the terminator. == run_program_cfg_on_grid == the VM.\n\
6630         {modules}\
6631         module seqprog_{name}(\n    {ports}\n);\n{body}endmodule\n",
6632        ports = ports.join(",\n    "),
6633    )
6634}
6635
6636/// The first cell the STANDALONE sequenced decision datapath cannot lower (it is register-only — no heap), as a
6637/// typed [`LoweringError::StatePlaneUnlowered`]. `Ok(())` ⇒ every cell is a register tile (Arith/Select/Func).
6638/// A heap-bearing DecisionFabric belongs on [`emit_state_plane_verilog`], not the bare datapath.
6639fn seq_datapath_cells_supported(cells: &[LoopCell]) -> Result<(), LoweringError> {
6640    for c in cells {
6641        if cell_is_heap(c) {
6642            return Err(LoweringError::StatePlaneUnlowered(format!("{c:?}")));
6643        }
6644    }
6645    Ok(())
6646}
6647
6648/// The first cell the WHOLE-PROGRAM sequenced fabric cannot lower. `HeapAlloc`/`HeapRead`/`HeapWrite` now lower via
6649/// the shared state plane, so only a `MemRead` (the loop-sequence induction scratchpad — a distinct mechanism)
6650/// remains a typed refusal. `Ok(())` ⇒ the program lowers (register tiles + the state plane).
6651fn seq_program_cells_supported(cells: &[LoopCell]) -> Result<(), LoweringError> {
6652    for c in cells {
6653        if matches!(c, LoopCell::MemRead { .. }) {
6654            return Err(LoweringError::StatePlaneUnlowered(format!("{c:?}")));
6655        }
6656    }
6657    Ok(())
6658}
6659
6660/// Graceful entry to the whole-program sequenced fabric: `Ok(rtl)` when every block lowers (register tiles + the
6661/// shared state plane for Seq/string programs), or `Err(StatePlaneUnlowered)` for the residual `MemRead`
6662/// loop-sequence form. The typed companion to the panicking [`emit_seq_program_verilog`].
6663pub fn try_emit_seq_program_verilog(
6664    name: &str,
6665    pf: &ProgramCfgFabric,
6666) -> Result<String, LoweringError> {
6667    for b in &pf.blocks {
6668        seq_program_cells_supported(&b.body)?;
6669    }
6670    Ok(emit_seq_program_verilog(name, pf))
6671}
6672
6673/// Graceful entry to a single sequenced decision datapath: `Ok(rtl)` when every cell is register-only, or
6674/// `Err(StatePlaneUnlowered)` for a heap-reading fabric (which belongs on the state-plane emitter). The typed
6675/// companion to [`emit_seq_datapath_verilog`].
6676pub fn try_emit_seq_datapath_verilog(
6677    name: &str,
6678    df: &DecisionFabric,
6679) -> Result<String, LoweringError> {
6680    seq_datapath_cells_supported(&df.cells)?;
6681    Ok(emit_seq_datapath_verilog(name, df))
6682}
6683
6684/// Emit a Nexys A7 SoC wrapper for the whole-program sequenced fabric: a baked vector ROM steps `seqprog_{name}`
6685/// through each input (drive params → pulse the fabric reset → wait `done` → capture result), streaming a
6686/// 12-byte record `{0xA5, result[63:0] MSB-first, run_cycles[15:0], 0x5A}` out the UART TX per vector, then
6687/// looping. Slow /128 core clock (781.25 kHz) + power-on reset. `vectors` are the params per run (n_params each).
6688/// This is the multi-cycle fabric (division + fpu.v tiles) on real silicon.
6689pub fn emit_seqprog_soc_verilog(name: &str, pf: &ProgramCfgFabric, vectors: &[Vec<i64>]) -> String {
6690    let np = pf.n_params;
6691    let nv = vectors.len();
6692    assert!(
6693        nv >= 1 && vectors.iter().all(|v| v.len() == np),
6694        "each vector must have n_params={np} entries"
6695    );
6696    let core = emit_seq_program_verilog(name, pf);
6697
6698    // The baked vector ROM: case(vec) drives p0..p{np-1}.
6699    let mut rom = String::new();
6700    for (i, v) in vectors.iter().enumerate() {
6701        let sets = (0..np)
6702            .map(|k| {
6703                let x = v[k];
6704                if x < 0 {
6705                    format!("p{k}=-64'sd{};", -(x as i128))
6706                } else {
6707                    format!("p{k}=64'sd{x};")
6708                }
6709            })
6710            .collect::<Vec<_>>()
6711            .join(" ");
6712        rom.push_str(&format!("      {i}: begin {sets} end\n"));
6713    }
6714    let pdecls = (0..np)
6715        .map(|k| format!("  reg signed [63:0] p{k};\n"))
6716        .collect::<String>();
6717    let pconn = (0..np)
6718        .map(|k| format!(".p{k}(p{k}), "))
6719        .collect::<String>();
6720    let pdefault = (0..np)
6721        .map(|k| format!("p{k}=64'sd0;"))
6722        .collect::<Vec<_>>()
6723        .join(" ");
6724    let vecw = std::cmp::max(1, (usize::BITS - (nv.max(2) - 1).leading_zeros()) as usize);
6725
6726    format!(
6727        r#"{core}
6728// nexys_seqprog_{name} — the whole-program SEQUENCED fabric (multi-cycle division/float tiles) on a Nexys A7.
6729// A baked vector ROM runs seqprog_{name} for each input (drive params -> pulse fab_rst -> wait done -> capture),
6730// streaming {{0xA5, result[63:0], run_cycles[15:0], 0x5A}} out the UART per vector. 781.25 kHz core clock.
6731module nexys_seqprog_{name}(input clk, input rst, output reg uart_tx, output [15:0] led);
6732  localparam CLKS_PER_BIT = 81;   // 781.25e3 / 9600
6733  localparam NVEC = {nv};
6734  // /128 core clock on a global buffer.
6735  reg [6:0] divcnt = 7'd0;
6736  always @(posedge clk) divcnt <= divcnt + 1'b1;
6737  wire coreclk;
6738  BUFG bufg_core(.I(divcnt[6]), .O(coreclk));
6739  reg [3:0] por = 4'd0;
6740  always @(posedge coreclk) if (!por[3]) por <= por + 1'b1;
6741  wire soc_rst = rst | ~por[3];
6742
6743  reg [{vhi}:0] vec;
6744{pdecls}  always @(*) begin
6745    {pdefault}
6746    case (vec)
6747{rom}      default: ;
6748    endcase
6749  end
6750  reg fab_rst;
6751  wire [63:0] fab_result; wire fab_done;
6752  reg [63:0] result_l; reg [15:0] cyc_l; reg [15:0] runcyc;
6753  seqprog_{name} u_fab(.clk(coreclk), .rst(fab_rst), {pconn}.result(fab_result), .done(fab_done));
6754
6755  localparam R_POR=3'd0, R_RUN=3'd1, R_WAIT=3'd2, R_LOAD=3'd3, R_START=3'd4, R_DATA=3'd5, R_STOP=3'd6;
6756  reg [2:0] rstate;
6757  reg [15:0] clkc; reg [2:0] bidx; reg [3:0] pos; reg [3:0] porc; reg [1:0] rstc;
6758  reg [7:0] txb;
6759  assign led = {{rstate, vec[4:0], 8'd0}};
6760  wire [3:0] rsel = (pos >= 4'd1 && pos <= 4'd8) ? (4'd8 - pos) : 4'd0;
6761  wire [7:0] res_byte = result_l[rsel * 8 +: 8];
6762  always @(posedge coreclk) begin
6763    if (soc_rst) begin
6764      rstate<=R_POR; uart_tx<=1'b1; clkc<=16'd0; bidx<=3'd0; vec<={vw}'d0; pos<=4'd0; porc<=4'd0;
6765      fab_rst<=1'b1; rstc<=2'd0; runcyc<=16'd0; result_l<=64'd0; cyc_l<=16'd0;
6766    end else case (rstate)
6767      R_POR: if (porc==4'd8) begin rstate<=R_RUN; fab_rst<=1'b1; rstc<=2'd0; end else porc<=porc+4'd1;
6768      R_RUN: begin
6769        // hold fab_rst 2 coreclks (load params + reset the fabric), then release to run.
6770        if (rstc==2'd2) begin fab_rst<=1'b0; runcyc<=16'd0; rstate<=R_WAIT; end else rstc<=rstc+2'd1;
6771      end
6772      R_WAIT: begin
6773        runcyc <= runcyc + 16'd1;
6774        if (fab_done) begin result_l<=fab_result; cyc_l<=runcyc; pos<=4'd0; rstate<=R_LOAD; end
6775      end
6776      R_LOAD: begin
6777        case (pos)
6778          4'd0:  txb<=8'hA5;
6779          4'd9:  txb<=cyc_l[15:8];
6780          4'd10: txb<=cyc_l[7:0];
6781          4'd11: txb<=8'h5A;
6782          default: txb<=res_byte;   // pos 1..8 = result[63:0] MSB-first
6783        endcase
6784        clkc<=16'd0; bidx<=3'd0; rstate<=R_START;
6785      end
6786      R_START: begin uart_tx<=1'b0; if (clkc==CLKS_PER_BIT-1) begin clkc<=16'd0; rstate<=R_DATA; end else clkc<=clkc+16'd1; end
6787      R_DATA: begin uart_tx<=txb[bidx];
6788        if (clkc==CLKS_PER_BIT-1) begin clkc<=16'd0; if (bidx==3'd7) rstate<=R_STOP; else bidx<=bidx+3'd1; end else clkc<=clkc+16'd1;
6789      end
6790      R_STOP: begin uart_tx<=1'b1;
6791        if (clkc==CLKS_PER_BIT-1) begin clkc<=16'd0;
6792          if (pos==4'd11) begin
6793            pos<=4'd0;
6794            if (vec==NVEC-1) vec<={vw}'d0; else vec<=vec+{vw}'d1;
6795            fab_rst<=1'b1; rstc<=2'd0; rstate<=R_RUN;
6796          end else begin pos<=pos+4'd1; rstate<=R_LOAD; end
6797        end else clkc<=clkc+16'd1;
6798      end
6799      default: rstate<=R_POR;
6800    endcase
6801  end
6802endmodule
6803"#,
6804        vhi = vecw - 1,
6805        vw = vecw,
6806    )
6807}
6808
6809/// A self-checking clocked testbench for `seqprog_{name}`: per vector, load params, pulse rst, spin on `done`
6810/// (multi-cycle tiles + the block walk take many clocks), and check `result` == the VM (`seqprog_{name}_out.hex`).
6811pub fn emit_seq_program_testbench(name: &str, n_params: usize, n: usize) -> String {
6812    let decls = (0..n_params)
6813        .map(|k| format!("  reg [63:0] p{k};\n"))
6814        .collect::<String>();
6815    let ports = (0..n_params)
6816        .map(|k| format!(".p{k}(p{k}), "))
6817        .collect::<String>();
6818    let drive = (0..n_params)
6819        .map(|k| format!("      p{k} = oin[i*{n_params}+{k}];\n"))
6820        .collect::<String>();
6821    format!(
6822        r#"// tb_seqprog_{name} — the whole-program sequenced fabric vs the RUNNING VM, over {n} vectors.
6823`timescale 1ns/1ps
6824module tb_seqprog_{name};
6825  reg clk=0, rst=1;
6826{decls}  reg [63:0] oin  [0:{in_last}];
6827  reg [63:0] oout [0:{last}];
6828  wire [63:0] result; wire done;
6829  integer i, errors=0, timeout;
6830  seqprog_{name} dut(.clk(clk), .rst(rst), {ports}.result(result), .done(done));
6831  always #5 clk = ~clk;
6832  initial begin
6833    $readmemh("seqprog_{name}_in.hex", oin);
6834    $readmemh("seqprog_{name}_out.hex", oout);
6835    for (i=0; i<{n}; i=i+1) begin
6836{drive}      rst=1'b1; @(posedge clk); #1; rst=1'b0;
6837      timeout=0;
6838      while (!done && timeout<2000000) begin @(posedge clk); timeout=timeout+1; end
6839      #1;
6840      if (result !== oout[i]) begin errors=errors+1; $display("FAIL vec %0d: got %0d exp %0d", i, $signed(result), $signed(oout[i])); end
6841    end
6842    if (errors==0) $display("PASS: {n} whole-program sequenced-fabric runs match the VM");
6843    else $display("FAIL: %0d whole-program mismatches", errors);
6844    $finish;
6845  end
6846endmodule
6847"#,
6848        in_last = if n_params == 0 { 0 } else { n_params * n - 1 },
6849        last = n - 1,
6850    )
6851}
6852
6853/// The two IEEE-754 int↔float converter Verilog FUNCTIONS shared by the sequenced datapath (and bit-identical
6854/// to the engine's): `i64_to_f64` (round-to-nearest-ties-even == `FuncUnit::IntToF`) and `f64_floor_i64`
6855/// (floor toward −inf == `FuncUnit::FFloorF`). Combinational, so IntToF/FFloor cells settle in one step.
6856const I64_F64_CONVERTERS: &str = r#"  function [63:0] i64_to_f64(input signed [63:0] n);
6857    reg s; reg [63:0] m; integer lead, i, sh; reg [10:0] be; reg [51:0] frac;
6858    reg [63:0] shifted, dropped; reg [53:0] sig; reg round_up;
6859    begin
6860      if (n == 64'sd0) i64_to_f64 = 64'd0;
6861      else begin
6862        s = n[63];
6863        m = s ? (~n + 64'sd1) : n;
6864        lead = 0;
6865        for (i = 63; i >= 0; i = i - 1) if (m[i] && lead == 0) lead = i;
6866        be = 11'd1023 + lead[10:0];
6867        if (lead <= 52) begin
6868          shifted = m << (52 - lead);
6869          i64_to_f64 = {s, be, shifted[51:0]};
6870        end else begin
6871          sh = lead - 52;
6872          sig = m >> sh;
6873          dropped = m << (64 - sh);
6874          round_up = dropped[63] && (|dropped[62:0] || sig[0]);
6875          if (round_up) sig = sig + 54'd1;
6876          if (sig[53]) begin be = be + 11'd1; frac = 52'd0; end
6877          else            frac = sig[51:0];
6878          i64_to_f64 = {s, be, frac};
6879        end
6880      end
6881    end
6882  endfunction
6883  function signed [63:0] f64_floor_i64(input [63:0] x);
6884    reg s; reg [10:0] e; reg [52:0] sig; integer ue; reg [63:0] mag; reg hasfrac;
6885    begin
6886      s = x[63]; e = x[62:52];
6887      if (e < 11'd1023)
6888        f64_floor_i64 = (s && !(e == 11'd0 && x[51:0] == 52'd0)) ? -64'sd1 : 64'sd0;
6889      else begin
6890        sig = {1'b1, x[51:0]};
6891        ue = e - 11'd1023;
6892        if (ue >= 52) begin mag = {11'd0, sig} << (ue - 52); hasfrac = 1'b0; end
6893        else begin
6894          mag = {11'd0, sig} >> (52 - ue);
6895          hasfrac = (({11'd0, sig} << (ue + 12)) != 64'd0);
6896        end
6897        if (!s) f64_floor_i64 = mag;
6898        else    f64_floor_i64 = hasfrac ? -(mag + 64'd1) : -mag;
6899      end
6900    end
6901  endfunction
6902"#;
6903
6904/// A self-checking CLOCKED testbench for a `seq_{name}` sequenced datapath: for each of `n` input vectors it
6905/// pulses `rst`, waits for `done`, and checks `result` == the VM oracle (`seq_{name}_out.hex`). Multi-cycle tiles
6906/// (the divider / FPU) make a vector take many clocks, so it spins on `done` with a generous timeout.
6907pub fn emit_seq_datapath_testbench(name: &str, n_in: usize, n: usize) -> String {
6908    let decls = (0..n_in)
6909        .map(|k| format!("  reg [63:0] in{k};\n"))
6910        .collect::<String>();
6911    let ports = (0..n_in)
6912        .map(|k| format!(".in{k}(in{k}), "))
6913        .collect::<String>();
6914    let drive = (0..n_in)
6915        .map(|k| format!("      in{k} = oin[i*{n_in}+{k}];\n"))
6916        .collect::<String>();
6917    format!(
6918        r#"// tb_seq_{name} — the sequenced datapath vs the RUNNING VM, over the {n} vectors.
6919`timescale 1ns/1ps
6920module tb_seq_{name};
6921  reg clk=0, rst=1;
6922{decls}  reg [63:0] oin  [0:{in_last}];
6923  reg [63:0] oout [0:{last}];
6924  wire [63:0] result; wire done;
6925  integer i, errors=0, timeout;
6926  seq_{name} dut(.clk(clk), .rst(rst), {ports}.result(result), .done(done));
6927  always #5 clk = ~clk;
6928  initial begin
6929    $readmemh("seq_{name}_in.hex", oin);
6930    $readmemh("seq_{name}_out.hex", oout);
6931    for (i=0; i<{n}; i=i+1) begin
6932{drive}      rst=1'b1; @(posedge clk); #1; rst=1'b0;
6933      timeout=0;
6934      while (!done && timeout<200000) begin @(posedge clk); timeout=timeout+1; end
6935      #1;
6936      if (result !== oout[i]) begin errors=errors+1; $display("FAIL vec %0d: got %0d exp %0d", i, $signed(result), $signed(oout[i])); end
6937    end
6938    if (errors==0) $display("PASS: {n} sequenced-datapath evaluations match the VM (multi-cycle tiles ≡VM)");
6939    else $display("FAIL: %0d sequenced-datapath mismatches", errors);
6940    $finish;
6941  end
6942endmodule
6943"#,
6944        in_last = n_in * n - 1,
6945        last = n - 1,
6946    )
6947}
6948
6949// ── STAGE X3b (XPU): the FLOAT LANE as synthesizable RTL. A `DecisionFabric` whose cells are IEEE-754 float
6950// functional units (`FAddF/FSubF/FMulF/FDivF/FSqrtF`) cannot settle combinationally: each op is the multi-cycle
6951// `fpu.v` unit (a start/done handshake FSM, round-to-nearest-ties-even). So the float lane lowers to a SEQUENCED
6952// controller that binds ONE of each `fpu.v` unit (shared across cells) and, per step, muxes the current cell's
6953// operands to the selected unit, pulses its `start`, waits its `done`, and latches the result register. Float
6954// VALUES are f64 BIT-PATTERNS on the 64-bit datapath (no separate typed channel). Proven ≡ the model
6955// (`eval_grid` over f64 bits) ≡ Rust f64 by iverilog; the `fpu.v` units are already bit-exact to the VM
6956// (`float_heap_matches_vm`). NOT SAT (IEEE-754 is outside QF_BV) — the same layered proof as division/state.
6957
6958/// True iff the fabric is a pure FLOAT-arithmetic datapath (its cells are all `FuncUnit` float ops over
6959/// float/Val operands) — lowered by [`emit_float_datapath_verilog`]. It must end in a float result (no
6960/// `FFloorF` float→int bridge here — the caller floors the streamed f64 downstream) and carry no heap.
6961pub fn decision_fabric_is_float_arith(df: &DecisionFabric) -> bool {
6962    !df.cells.is_empty()
6963        && df.cells.iter().all(|c| {
6964            matches!(
6965                c,
6966                LoopCell::Func {
6967                    unit: FuncUnit::FAddF
6968                        | FuncUnit::FSubF
6969                        | FuncUnit::FMulF
6970                        | FuncUnit::FDivF
6971                        | FuncUnit::FSqrtF,
6972                    ..
6973                }
6974            )
6975        })
6976}
6977
6978/// Lower a pure-float-arithmetic `DecisionFabric` to a SEQUENCED float datapath binding `fpu.v` (see the block
6979/// comment above). Each cell is one multi-cycle FPU step; the streamed `result` is the f64 bit-pattern of the
6980/// final float. Panics on any non-float-arith cell (the honest boundary — `FFloorF`/`Arith` are not bound here).
6981pub fn emit_float_datapath_verilog(name: &str, df: &DecisionFabric) -> String {
6982    let name_in = |k: usize| format!("in{k}");
6983    let name_val = |k: usize| format!("R[{k}]");
6984    let src = |s: &Src| src_named(s, &name_in, &name_val);
6985    let m = df.cells.len();
6986    assert!(m > 0, "a float datapath must have at least one cell");
6987
6988    let inputs = (0..df.n_in)
6989        .map(|k| format!("input [63:0] in{k}"))
6990        .collect::<Vec<_>>()
6991        .join(", ");
6992    let inputs = if inputs.is_empty() {
6993        String::new()
6994    } else {
6995        format!("{inputs}, ")
6996    };
6997
6998    // Per-step operand + unit selection: usel 0=add, 1=sub, 2=mul, 3=div, 4=sqrt.
6999    let mut arms = String::new();
7000    for (i, c) in df.cells.iter().enumerate() {
7001        let (usel, a, b) = match c {
7002            LoopCell::Func { unit, a, b } => match unit {
7003                FuncUnit::FAddF => (0, src(a), src(b)),
7004                FuncUnit::FSubF => (1, src(a), src(b)),
7005                FuncUnit::FMulF => (2, src(a), src(b)),
7006                FuncUnit::FDivF => (3, src(a), src(b)),
7007                FuncUnit::FSqrtF => (4, src(a), "64'd0".to_string()),
7008                other => panic!(
7009                    "float datapath: {other:?} is not a bound fpu.v unit (add/sub/mul/div/sqrt)"
7010                ),
7011            },
7012            other => panic!("float datapath: cell {other:?} is not a float functional unit"),
7013        };
7014        arms.push_str(&format!(
7015            "        {i}: begin opa = {a}; opb = {b}; usel = 3'd{usel}; end\n"
7016        ));
7017    }
7018
7019    format!(
7020        "// float_{name} — a pure-float-arithmetic datapath as a SEQUENCED controller binding fpu.v. One of each\n\
7021         // multi-cycle IEEE-754 unit (fp_addsub/fp_mul/fp_div/fp_sqrt) is SHARED across cells; a micro-sequencer\n\
7022         // muxes each cell's operands to the selected unit, pulses start, waits done, latches the result. The\n\
7023         // streamed `result` is the f64 BIT-PATTERN of the final float. Proven == the model (eval_grid over f64\n\
7024         // bits) == Rust f64 by iverilog; fpu.v is bit-exact to the VM.\n\
7025         module float_{name}(input clk, input rst, {inputs}output [63:0] result, output done);\n\
7026         \x20 localparam M = {m};\n\
7027         \x20 (* ram_style = \"registers\" *) reg [63:0] R [0:M-1];\n\
7028         \x20 reg [31:0] step;\n\
7029         \x20 reg running, issued;\n\
7030         \x20 reg [63:0] opa, opb; reg [2:0] usel;\n\
7031         \x20 always @(*) begin\n\
7032         \x20\x20 opa = 64'd0; opb = 64'd0; usel = 3'd0;\n\
7033         \x20\x20 case (step)\n\
7034         {arms}\
7035         \x20\x20\x20 default: ;\n\
7036         \x20\x20 endcase\n\
7037         \x20 end\n\
7038         \x20 wire is_addsub = (usel == 3'd0) || (usel == 3'd1);\n\
7039         \x20 wire is_sub = (usel == 3'd1);\n\
7040         \x20 wire is_mul = (usel == 3'd2);\n\
7041         \x20 wire is_div = (usel == 3'd3);\n\
7042         \x20 wire is_sqrt = (usel == 3'd4);\n\
7043         \x20 // the start pulse fires for exactly the selected unit in the cycle a step is issued.\n\
7044         \x20 wire pulse = running && !issued;\n\
7045         \x20 wire add_done, mul_done, div_done, sqrt_done;\n\
7046         \x20 wire [63:0] add_y, mul_y, div_y, sqrt_y;\n\
7047         \x20 fp_addsub u_add(.clk(clk), .rst(rst), .start(pulse && is_addsub), .a(opa), .b_in(opb), .sub(is_sub), .y(add_y), .done(add_done));\n\
7048         \x20 fp_mul    u_mul(.clk(clk), .rst(rst), .start(pulse && is_mul), .a(opa), .b(opb), .y(mul_y), .done(mul_done));\n\
7049         \x20 fp_div    u_div(.clk(clk), .rst(rst), .start(pulse && is_div), .a(opa), .b(opb), .y(div_y), .done(div_done));\n\
7050         \x20 fp_sqrt   u_sqrt(.clk(clk), .rst(rst), .start(pulse && is_sqrt), .a(opa), .y(sqrt_y), .done(sqrt_done));\n\
7051         \x20 wire sel_done = is_addsub ? add_done : is_mul ? mul_done : is_div ? div_done : sqrt_done;\n\
7052         \x20 wire [63:0] sel_y = is_addsub ? add_y : is_mul ? mul_y : is_div ? div_y : sqrt_y;\n\
7053         \x20 always @(posedge clk) begin\n\
7054         \x20\x20 if (rst) begin step <= 32'd0; running <= (M > 0); issued <= 1'b0; end\n\
7055         \x20\x20 else if (running) begin\n\
7056         \x20\x20\x20 if (!issued) issued <= 1'b1;            // the start pulse fired this cycle\n\
7057         \x20\x20\x20 else if (sel_done) begin\n\
7058         \x20\x20\x20\x20 R[step] <= sel_y; issued <= 1'b0;\n\
7059         \x20\x20\x20\x20 if (step == M - 1) running <= 1'b0; else step <= step + 32'd1;\n\
7060         \x20\x20\x20 end\n\
7061         \x20\x20 end\n\
7062         \x20 end\n\
7063         \x20 assign done = !running;\n\
7064         \x20 assign result = {res};\n\
7065         endmodule\n",
7066        res = src(&df.result),
7067    )
7068}
7069
7070// ── STAGE X1c (XPU): the POWER LANE as synthesizable RTL. Exponentiation `a ** b` is NOT combinational: at
7071// the RTL level it is a `b`-step multiply FSM (the honest sequential functional unit, like the shared_divider
7072// is for division). So a `DecisionFabric` whose cells are `FuncUnit::Pow` lowers not to a wired DAG but to a
7073// SEQUENCED controller over the SAME cell stream — one shared multiply-accumulate loop per cell: latch the
7074// base/exponent, multiply the accumulator `b` times (exponent clamped to ≥ 0), then latch the result and
7075// advance. 64-bit signed wrapping, so bit-identical to `FuncUnit::eval`'s `a.wrapping_pow(b.max(0) as u32)`.
7076// Proven ≡ the model (eval) ≡ VM by iverilog (`fabric_pow_lock`); NOT SAT (an unbounded multiply loop is
7077// outside QF_BV), the same layered proof as division/float/state.
7078
7079/// True iff the fabric is a pure POWER datapath (every cell is a `FuncUnit::Pow`) — lowered by
7080/// [`emit_pow_datapath_verilog`]. Pow cannot settle combinationally (`FuncUnit::verilog` refuses it), so a
7081/// Pow-bearing fabric takes this sequenced lane exactly as a float fabric takes the fpu.v lane.
7082pub fn decision_fabric_is_pow(df: &DecisionFabric) -> bool {
7083    !df.cells.is_empty()
7084        && df.cells.iter().all(|c| {
7085            matches!(
7086                c,
7087                LoopCell::Func {
7088                    unit: FuncUnit::Pow,
7089                    ..
7090                }
7091            )
7092        })
7093}
7094
7095/// Lower a pure-`Pow` `DecisionFabric` to a SEQUENCED power datapath (see the block comment above). Each cell
7096/// is one `b`-step multiply loop over a shared accumulator; the streamed `result` is the final power. Panics
7097/// on any non-Pow cell (the honest boundary — mixed lanes are a follow-on).
7098pub fn emit_pow_datapath_verilog(name: &str, df: &DecisionFabric) -> String {
7099    let name_in = |k: usize| format!("in{k}");
7100    let name_val = |k: usize| format!("R[{k}]");
7101    let src = |s: &Src| src_named(s, &name_in, &name_val);
7102    let m = df.cells.len();
7103    assert!(m > 0, "a power datapath must have at least one cell");
7104
7105    let inputs = (0..df.n_in)
7106        .map(|k| format!("input signed [63:0] in{k}"))
7107        .collect::<Vec<_>>()
7108        .join(", ");
7109    let inputs = if inputs.is_empty() {
7110        String::new()
7111    } else {
7112        format!("{inputs}, ")
7113    };
7114
7115    // Per-step operand selection: the current cell's base + exponent, muxed into the shared MAC loop.
7116    let mut arms = String::new();
7117    for (i, c) in df.cells.iter().enumerate() {
7118        let (a, b) = match c {
7119            LoopCell::Func {
7120                unit: FuncUnit::Pow,
7121                a,
7122                b,
7123            } => (src(a), src(b)),
7124            other => panic!("power datapath: cell {other:?} is not a `Pow` functional unit"),
7125        };
7126        arms.push_str(&format!("        {i}: begin base = {a}; expo = {b}; end\n"));
7127    }
7128
7129    format!(
7130        "// pow_{name} — a pure-exponentiation datapath as a SEQUENCED controller. One shared multiply-accumulate\n\
7131         // loop is time-multiplexed across the cells: per cell, latch (base, exponent), then multiply the\n\
7132         // accumulator `exponent` times (a negative exponent clamps to 0 → result 1), latch the power, advance.\n\
7133         // 64-bit signed wrapping, so bit-identical to the VM's `a ** b` (`wrapping_pow`). Proven == the model\n\
7134         // (eval) == VM by iverilog (fabric_pow_lock); NOT SAT (an unbounded multiply loop is outside QF_BV).\n\
7135         module pow_{name}(input clk, input rst, {inputs}output signed [63:0] result, output done);\n\
7136         \x20 localparam M = {m};\n\
7137         \x20 (* ram_style = \"registers\" *) reg signed [63:0] R [0:M-1];\n\
7138         \x20 reg [31:0] step;\n\
7139         \x20 reg running, loading;\n\
7140         \x20 reg signed [63:0] base, expo;\n\
7141         \x20 reg signed [63:0] acc;         // running product for the current cell\n\
7142         \x20 reg [63:0] rem;                // remaining multiplies (exponent, clamped ≥ 0)\n\
7143         \x20 always @(*) begin\n\
7144         \x20\x20 base = 64'sd0; expo = 64'sd0;\n\
7145         \x20\x20 case (step)\n\
7146         {arms}\
7147         \x20\x20\x20 default: ;\n\
7148         \x20\x20 endcase\n\
7149         \x20 end\n\
7150         \x20 always @(posedge clk) begin\n\
7151         \x20\x20 if (rst) begin step <= 32'd0; running <= (M > 0); loading <= 1'b1; acc <= 64'sd1; rem <= 64'd0; end\n\
7152         \x20\x20 else if (running) begin\n\
7153         \x20\x20\x20 if (loading) begin\n\
7154         \x20\x20\x20\x20 acc <= 64'sd1; rem <= (expo[63] ? 64'd0 : expo); loading <= 1'b0;\n\
7155         \x20\x20\x20 end else if (rem == 64'd0) begin\n\
7156         \x20\x20\x20\x20 R[step] <= acc; loading <= 1'b1;\n\
7157         \x20\x20\x20\x20 if (step == M - 1) running <= 1'b0; else step <= step + 32'd1;\n\
7158         \x20\x20\x20 end else begin\n\
7159         \x20\x20\x20\x20 acc <= acc * base; rem <= rem - 64'd1;\n\
7160         \x20\x20\x20 end\n\
7161         \x20\x20 end\n\
7162         \x20 end\n\
7163         \x20 assign done = !running;\n\
7164         \x20 assign result = {res};\n\
7165         endmodule\n",
7166        res = src(&df.result),
7167    )
7168}
7169
7170/// A self-checking testbench for a power datapath: for each of `n` input vectors, pulse `rst`, drive `in{k}`,
7171/// run until `done` (bounded by `max_cycles`), and compare `result` against the VM oracle loaded from
7172/// `pow_{name}_{in,out}.hex`. Mirrors [`emit_state_plane_testbench`] (the sequenced clocked harness).
7173pub fn emit_pow_datapath_testbench(name: &str, n_in: usize, n: usize, max_cycles: usize) -> String {
7174    let drive = (0..n_in)
7175        .map(|k| format!("      in{k} = oin[i * {n_in} + {k}];\n"))
7176        .collect::<String>();
7177    let ports = (0..n_in)
7178        .map(|k| format!(".in{k}(in{k}), "))
7179        .collect::<String>();
7180    let decls = (0..n_in)
7181        .map(|k| format!("  reg signed [63:0] in{k};\n"))
7182        .collect::<String>();
7183    format!(
7184        r#"// tb_pow_{name} — the sequenced power datapath vs the RUNNING VM over the exponentiation battery.
7185`timescale 1ns/1ps
7186module tb_pow_{name};
7187  reg clk = 0, rst;
7188{decls}  reg signed [63:0] oin [0:{in_last}];
7189  reg signed [63:0] oout [0:{last}];
7190  wire signed [63:0] result;
7191  wire done;
7192  integer i, c, errors = 0;
7193  pow_{name} dut(.clk(clk), .rst(rst), {ports}.result(result), .done(done));
7194  always #5 clk = ~clk;
7195  initial begin
7196    $readmemh("pow_{name}_in.hex", oin);
7197    $readmemh("pow_{name}_out.hex", oout);
7198    for (i = 0; i < {n}; i = i + 1) begin
7199      @(negedge clk);
7200{drive}      rst = 1; @(negedge clk); rst = 0;
7201      c = 0; while (!done && c < {max_cycles}) begin @(negedge clk); c = c + 1; end
7202      if (result !== oout[i]) begin errors = errors + 1; $display("FAIL vec %0d: got %0d", i, result); end
7203    end
7204    if (errors == 0) $display("PASS: {n} power evaluations match the VM (a`b = the sequenced MAC lane)");
7205    else $display("FAIL: %0d pow mismatches", errors);
7206    $finish;
7207  end
7208endmodule
7209"#,
7210        in_last = n_in * n - 1,
7211        last = n - 1,
7212        max_cycles = max_cycles,
7213    )
7214}
7215
7216// ── STAGE X2b (XPU): the STATE PLANE as synthesizable RTL. A `DecisionFabric` that carries heap cells
7217// (`HeapRead`/`HeapWrite`/`HeapAlloc` — collections/structs/text) cannot settle in one combinational pass:
7218// the heap is MUTABLE and the reads/writes are ORDERED. So it lowers not to a wired DAG but to a SEQUENCED
7219// controller over the SAME cell stream — the honest "dataflow-with-memory" machine. Each cell is one step of
7220// a tiny hardwired micro-sequencer (no fetch, no decode — the sequence IS the program): pure-compute cells
7221// latch a combinational result register; heap cells touch ONE shared heap (a LUTRAM: async read, sync write)
7222// with a bump-pointer allocator. This mirrors `eval_cells`/`eval_grid_with_heap` EXACTLY (row-major = cell
7223// order = step order; a read sees every prior step's committed write; alloc returns the heap top), so the RTL
7224// is proven ≡ VM by iverilog (stateful mutable heap is outside QF_BV — NOT SAT). It is the synthesizable
7225// realization of the Stage-X2a grid (`fabric_grid::eval_grid_with_heap`); both share the golden cell stream.
7226
7227/// The heap-word budget for a state-plane kernel: enough words for the largest collection any vector touches
7228/// (a generous fixed LUTRAM — collections in these kernels are a handful of words; addresses index the low
7229/// `clog2(HEAP)` bits, so an in-bounds pointer always resolves). Kept a power of two for clean address slicing.
7230pub const STATE_PLANE_HEAP_WORDS: usize = 64;
7231
7232/// True iff the fabric carries a state-plane (heap) cell — i.e. it must be lowered by
7233/// [`emit_state_plane_verilog`] (sequenced) rather than [`emit_decision_fabric_verilog`] (combinational).
7234pub fn decision_fabric_has_state(df: &DecisionFabric) -> bool {
7235    df.cells.iter().any(|c| {
7236        matches!(
7237            c,
7238            LoopCell::HeapRead { .. } | LoopCell::HeapWrite { .. } | LoopCell::HeapAlloc { .. }
7239        )
7240    })
7241}
7242
7243/// Lower a heap-bearing `DecisionFabric` to a SEQUENCED state-plane controller (see the block comment above).
7244/// `heap_words` sizes the shared LUTRAM. Pure-compute cells render as combinational `v{i}` wires (over the
7245/// LATCHED cell registers `R[j]`) latched per step; heap cells access the LUTRAM with a bump-pointer `hp`.
7246pub fn emit_state_plane_verilog(name: &str, df: &DecisionFabric, heap_words: usize) -> String {
7247    let name_in = |k: usize| format!("in{k}");
7248    // Operands read the LATCHED result register of a prior cell (step order guarantees j < i is committed).
7249    let name_val = |k: usize| format!("R[{k}]");
7250    let src = |s: &Src| src_named(s, &name_in, &name_val);
7251    let m = df.cells.len();
7252    assert!(m > 0, "a state-plane fabric must have at least one cell");
7253    let aw = (usize::BITS - (heap_words - 1).leading_zeros()).max(1) as usize; // clog2(heap_words)
7254
7255    let inputs = (0..df.n_in)
7256        .map(|k| format!("input [63:0] in{k}"))
7257        .collect::<Vec<_>>()
7258        .join(", ");
7259    let inputs = if inputs.is_empty() {
7260        String::new()
7261    } else {
7262        format!("{inputs}, ")
7263    };
7264
7265    // Per-cell combinational wires: pure-compute cells get their value `v{i}`; heap cells get their address
7266    // `addr{i}` (and a written value `wval{i}` for a write). Everything is 64-bit signed, wrapping like the VM.
7267    let mut wires = String::new();
7268    // The per-step case body: latch `R[i]` (and mutate heap/hp) for the cell at `step == i`.
7269    let mut arms = String::new();
7270    for (i, c) in df.cells.iter().enumerate() {
7271        match c {
7272            LoopCell::Arith { op, a, b } => {
7273                wires.push_str(&format!("  wire [63:0] v{i} = {};\n", op.verilog(&src(a), &src(b))));
7274                arms.push_str(&format!("        {i}: R[{i}] <= v{i};\n"));
7275            }
7276            LoopCell::VectorArith { op, ty, a, b } => {
7277                wires.push_str(&format!("  wire [63:0] v{i} = {};\n", op.verilog(*ty, &src(a), &src(b))));
7278                arms.push_str(&format!("        {i}: R[{i}] <= v{i};\n"));
7279            }
7280            LoopCell::Func { unit, a, b } => {
7281                wires.push_str(&format!("  wire [63:0] v{i} = {};\n", unit.verilog(&src(a), &src(b))));
7282                arms.push_str(&format!("        {i}: R[{i}] <= v{i};\n"));
7283            }
7284            LoopCell::Select { cmp, ca, cb, t, e } => {
7285                wires.push_str(&format!(
7286                    "  wire [63:0] v{i} = ($signed({}) {} $signed({})) ? {} : {};\n",
7287                    src(ca), cmp.verilog(), src(cb), src(t), src(e)
7288                ));
7289                arms.push_str(&format!("        {i}: R[{i}] <= v{i};\n"));
7290            }
7291            LoopCell::HeapRead { addr } => {
7292                wires.push_str(&format!("  wire [63:0] addr{i} = {};\n", src(addr)));
7293                // Async read of the CURRENT heap — sees every prior step's committed (sync) write.
7294                arms.push_str(&format!("        {i}: R[{i}] <= heap[addr{i}[{hi}:0]];\n", hi = aw - 1));
7295            }
7296            LoopCell::HeapWrite { addr, val } => {
7297                wires.push_str(&format!("  wire [63:0] addr{i} = {};\n", src(addr)));
7298                wires.push_str(&format!("  wire [63:0] wval{i} = {};\n", src(val)));
7299                // Sync write; the tile's value is the written word; extend `hp` if the write grew the heap
7300                // (mirrors `eval_cells`' resize-on-out-of-bounds so a later alloc gets a fresh pointer).
7301                arms.push_str(&format!(
7302                    "        {i}: begin heap[addr{i}[{hi}:0]] <= wval{i}; R[{i}] <= wval{i}; \
7303                     if ($signed(addr{i}) + 64'sd1 > $signed(hp)) hp <= addr{i} + 64'sd1; end\n",
7304                    hi = aw - 1
7305                ));
7306            }
7307            LoopCell::HeapAlloc { size } => {
7308                wires.push_str(&format!("  wire [63:0] size{i} = {};\n", src(size)));
7309                // Bump allocator: the fresh pointer is the current heap top; reserve `size` zeroed words.
7310                arms.push_str(&format!(
7311                    "        {i}: begin R[{i}] <= hp; hp <= hp + size{i}; end\n"
7312                ));
7313            }
7314            LoopCell::MemRead { .. } => panic!(
7315                "state plane: a loop-sequence MemRead is the induction-counter scratchpad (P2), distinct \
7316                 from the shared-heap state plane"
7317            ),
7318        }
7319    }
7320
7321    format!(
7322        "// state_{name} — a COLLECTION/STRUCT program as a SEQUENCED state-plane controller: a hardwired\n\
7323         // micro-sequencer (no fetch, no decode — the cell sequence IS the program) over ONE shared heap\n\
7324         // (LUTRAM: async read, sync write) with a bump-pointer allocator. Each cell is one step; pure-compute\n\
7325         // cells latch a combinational result, heap cells read/write the heap in program order. This is the\n\
7326         // Futamura projection for STATEFUL code: the residual dataflow-with-memory IS the circuit; proven ==\n\
7327         // the running VM by iverilog (mutable heap is outside QF_BV — eval+iverilog, not SAT). 64-bit wraps.\n\
7328         module state_{name}(input clk, input rst, {inputs}output [63:0] result, output done);\n\
7329         \x20 localparam M = {m};\n\
7330         \x20 localparam HEAP = {heap_words};\n\
7331         \x20 // ram_style=registers: the open FOSS flow (yosys+nextpnr-xilinx) drops write-ported LUT-RAM, so\n\
7332         \x20 // the shared heap + cell registers are forced to real FFs (a small register file) for silicon.\n\
7333         \x20 (* ram_style = \"registers\" *) reg  [63:0] R [0:M-1];\n\
7334         \x20 (* ram_style = \"registers\" *) reg  [63:0] heap [0:HEAP-1];\n\
7335         \x20 reg  [63:0] hp;\n\
7336         \x20 reg  [31:0] step;\n\
7337         \x20 reg  running;\n\
7338         \x20 integer j;\n\
7339         {wires}\
7340         \x20 always @(posedge clk) begin\n\
7341         \x20   if (rst) begin\n\
7342         \x20     step <= 0; hp <= 0; running <= (M > 0);\n\
7343         \x20     for (j = 0; j < HEAP; j = j + 1) heap[j] <= 64'd0;\n\
7344         \x20   end else if (running) begin\n\
7345         \x20     case (step)\n\
7346         {arms}\
7347         \x20       default: ;\n\
7348         \x20     endcase\n\
7349         \x20     if (step == M - 1) running <= 1'b0;\n\
7350         \x20     else step <= step + 1;\n\
7351         \x20   end\n\
7352         \x20 end\n\
7353         \x20 assign done = !running;\n\
7354         \x20 assign result = {res};\n\
7355         endmodule\n",
7356        res = src(&df.result),
7357    )
7358}
7359
7360/// A self-checking testbench for a state-plane controller: for each of `n` input vectors, pulse `rst`, drive
7361/// `in{k}`, run until `done` (bounded by `max_cycles`), and compare `result` against the VM oracle loaded from
7362/// `state_{name}_{in,out}.hex`. Mirrors [`emit_loop_fabric_testbench`] (the CGRA clocked harness).
7363pub fn emit_state_plane_testbench(name: &str, n_in: usize, n: usize, max_cycles: usize) -> String {
7364    let drive = (0..n_in)
7365        .map(|k| format!("in{k} = oin[i*{n_in}+{k}];"))
7366        .collect::<Vec<_>>()
7367        .join(" ");
7368    let ports = (0..n_in)
7369        .map(|k| format!(".in{k}(in{k})"))
7370        .collect::<Vec<_>>()
7371        .join(", ");
7372    let ports = if ports.is_empty() {
7373        String::new()
7374    } else {
7375        format!("{ports}, ")
7376    };
7377    let decls = (0..n_in)
7378        .map(|k| format!("  reg  [63:0] in{k};"))
7379        .collect::<Vec<_>>()
7380        .join("\n");
7381    format!(
7382        r#"// tb_state_{name} — the sequenced state-plane controller vs the RUNNING VM over the collection program.
7383`timescale 1ns/1ps
7384module tb_state_{name};
7385  reg clk = 0, rst;
7386{decls}
7387  reg  [63:0] oin [0:{ilast}];
7388  reg  [63:0] oout [0:{olast}];
7389  wire [63:0] result;
7390  wire done;
7391  integer i, c, errors = 0;
7392  state_{name} dut(.clk(clk), .rst(rst), {ports}.result(result), .done(done));
7393  always #5 clk = ~clk;
7394  initial begin
7395    $readmemh("state_{name}_in.hex", oin);
7396    $readmemh("state_{name}_out.hex", oout);
7397    for (i = 0; i < {n}; i = i + 1) begin
7398      @(negedge clk); {drive} rst = 1;
7399      @(negedge clk); rst = 0;
7400      c = 0;
7401      while (!done && c < {maxc}) begin @(negedge clk); c = c + 1; end
7402      if (result !== oout[i]) begin errors = errors + 1; $display("FAIL case %0d: got %0d exp %0d", i, result, oout[i]); end
7403    end
7404    if (errors == 0) $display("PASS: {n} state-plane runs match the VM (collections on the heap, sequenced)");
7405    else $display("FAIL: %0d state-plane mismatches", errors);
7406    $finish;
7407  end
7408endmodule
7409"#,
7410        ilast = (n * n_in).saturating_sub(1),
7411        olast = n.saturating_sub(1),
7412        maxc = max_cycles,
7413    )
7414}
7415
7416// ══════════════════════════════════════════════════════════════════════════════════════════════════
7417// WHOLE-FUNCTION CFG FABRIC (Phase 3) — the general multi-block controller.
7418//
7419// A function's control-flow graph is partitioned into basic blocks (the SAME partition the WASM backend
7420// proves out, `vm::wasm::cfg::Blocks`). Each block's straight-line body lowers to a WIRED residual island
7421// (SSA `LoopCell`s over the block's live-in registers — no per-op fetch); a block-sequencing CONTROLLER
7422// walks the graph, evaluating each block's terminator PREDICATE to pick the successor. The predicate feeds
7423// the controller, NOT the tile array — the clean separation the plan calls for. This composes islands into
7424// a whole function and handles branches BETWEEN blocks + early returns — the general control flow the
7425// single-construct decision/loop fabrics could not. (Loops with back-edges keep the counted/sequence loop
7426// fabric; this controller is the acyclic branch composition — the missing capstone piece.)
7427
7428/// A basic block's terminator: where control goes after the block's body runs.
7429#[derive(Clone, Debug)]
7430pub enum Term {
7431    /// Unconditional edge to block `k`.
7432    Jump(usize),
7433    /// Predicated edge: `cond != 0 ? then_blk : else_blk`.
7434    Branch {
7435        cond: Src,
7436        then_blk: usize,
7437        else_blk: usize,
7438    },
7439    /// Return the value named by `Src` (over the block's inputs + body vals).
7440    Return(Src),
7441}
7442
7443/// One spatially-lowered basic block: a wired residual body + its live-in registers + the register writes
7444/// it commits + its terminator.
7445#[derive(Clone, Debug)]
7446pub struct CfgBlock {
7447    /// Live-in registers, in input order: `Src::In(k)` in `body`/`writes`/`term` refers to `inputs[k]`.
7448    pub inputs: Vec<u16>,
7449    /// The block's straight-line datapath (SSA cells).
7450    pub body: Vec<LoopCell>,
7451    /// Each register the block writes, paired with the `Src` giving its final value.
7452    pub writes: Vec<(u16, Src)>,
7453    /// ★ WHAT THIS BLOCK SHOWS, in order — the block's slice of the program's output tape.
7454    ///
7455    /// A function RETURNS one value and the terminator carries it. A `## Main` SHOWS a sequence, and a
7456    /// branchy Main shows a DIFFERENT sequence down each path — which is exactly why the combinational
7457    /// output-tape path refuses it ("a conditional Show cannot ride the combinational tape") and why the
7458    /// tape has to live per block here. Empty for every function lowering, so nothing that existed
7459    /// before this field moves.
7460    pub shows: Vec<Src>,
7461    /// Where control goes next.
7462    pub term: Term,
7463}
7464
7465/// A whole function lowered to spatial block islands + a block-sequencing controller.
7466#[derive(Clone, Debug)]
7467pub struct ProgramCfgFabric {
7468    pub n_params: usize,
7469    pub blocks: Vec<CfgBlock>,
7470}
7471
7472/// Lower a whole (acyclic-control-flow) function to a [`ProgramCfgFabric`]. Panics on a back-edge (a loop
7473/// — use the counted/sequence loop fabric) or an op outside the supported straight-line set (honest bound).
7474/// Per-collection RESERVED CAPACITY for a dynamically-grown Seq/Map/Set (`NewEmptyList` + `Push`). The flat heap
7475/// stores a collection contiguously `[len, e0, e1…]`; an empty collection reserves this many element slots UP
7476/// FRONT so it grows WITHIN its own block instead of at the shared heap frontier — which is what lets MULTIPLE
7477/// collections grow concurrently without colliding (matrix_mult/knapsack/fannkuch/mergesort). This is a BOUND,
7478/// consistent with the fabric's bounded-heap nature (the RTL heap is a fixed BRAM): a collection that exceeds it
7479/// overflows into the next block and the whole-program result diverges from the VM — which the census's ≡VM
7480/// check catches as a REFUSED benchmark (never a silent wrong answer), so the bound is self-guarding. Sized to
7481/// clear the largest collection any benchmark builds (analytics' 1024-bucket table) with margin.
7482pub(crate) const SEQ_RESERVE: i64 = 2048;
7483
7484thread_local! {
7485    /// Per-lowering override of the per-collection reserve. `None` → the safe default [`SEQ_RESERVE`] (headroom
7486    /// for MULTIPLE collections growing concurrently without colliding — matrix_mult / knapsack / analytics).
7487    /// A program whose collections fill SEQUENTIALLY (each `Seq` completed before the next is allocated — the
7488    /// transformer's forward pass) needs almost none: `HeapWrite` auto-grows the frontier, so a small reserve
7489    /// gives the exact right-sized heap. Set via [`with_seq_reserve`]; every lowering is ≡VM-checked at its
7490    /// chosen reserve, so an under-reserve that would collide is caught as a REFUSED (wrong-answer) program.
7491    static SEQ_RESERVE_OVERRIDE: std::cell::RefCell<Option<i64>> = const { std::cell::RefCell::new(None) };
7492}
7493
7494/// The reserve to use right now (the thread-local override, else the default).
7495pub(crate) fn seq_reserve() -> i64 {
7496    SEQ_RESERVE_OVERRIDE.with(|c| c.borrow().unwrap_or(SEQ_RESERVE))
7497}
7498
7499/// Run `f` with the per-collection reserve set to `n` (restored after). Use a small `n` for a program that
7500/// fills its collections sequentially (e.g. the whole-transformer fabric engine) to right-size its heap.
7501pub fn with_seq_reserve<T>(n: i64, f: impl FnOnce() -> T) -> T {
7502    let prev = SEQ_RESERVE_OVERRIDE.with(|c| c.replace(Some(n)));
7503    let out = f();
7504    SEQ_RESERVE_OVERRIDE.with(|c| *c.borrow_mut() = prev);
7505    out
7506}
7507
7508/// The INT→FLOAT promotion of a float op's operand: if `is_float`, the operand already carries an `f64` bit
7509/// pattern and is returned unchanged; otherwise it holds an integer value that the VM would coerce (`*a as f64`
7510/// in `arith::multiply` etc. when an `Int` meets a `Float`), so a [`FuncUnit::IntToF`] cell is appended and its
7511/// result substituted. This realizes the VM's dynamic `Int ∘ Float → Float` coercion statically, so mixed
7512/// int/float arithmetic (`2.0 * k`) routes correctly through the float functional units.
7513fn promote_float_operand(cells: &mut Vec<LoopCell>, s: Src, is_float: bool) -> Src {
7514    if is_float {
7515        return s;
7516    }
7517    let k = cells.len();
7518    cells.push(LoopCell::Func {
7519        unit: FuncUnit::IntToF,
7520        a: s,
7521        b: Src::Const(0),
7522    });
7523    Src::Val(k)
7524}
7525
7526/// Mark a register as holding a plain INTEGER — clearing any stale float / float-collection tracking. VM register
7527/// slots are reused across int and float temps, so an op that writes an integer result MUST retract a prior
7528/// float mark, else a later read (e.g. a loop counter or array index that reused a float temp's slot) would be
7529/// mis-routed through the float lane. Called by every integer-producing arm.
7530fn mark_int_reg(dst: u16) {
7531    FLOAT_REGS.with(|t| {
7532        t.borrow_mut().remove(&dst);
7533    });
7534    FLOAT_COLLS.with(|t| {
7535        t.borrow_mut().remove(&dst);
7536    });
7537}
7538
7539/// Why the whole-program fabric lowering refused a program. A TYPED failure surface: unsupported constructs
7540/// return `Err(LoweringError)` from the `try_*` entry points instead of panicking, so callers (and the
7541/// `lowering_coverage_lock` ratchet) can tell "this construct is not lowered yet" apart from a real bug. The
7542/// `Display` strings are byte-identical to the old `panic!` messages, so the panicking wrappers are unchanged.
7543#[derive(Debug, Clone, PartialEq, Eq)]
7544pub enum LoweringError {
7545    /// A CFG-block op with no fabric tile (a loop/channel/error op in a straight-line body, etc.).
7546    UnsupportedOp(String),
7547    /// A builtin with no fabric unit wired (only floor/sqrt over the float lane today).
7548    UnsupportedBuiltin(String),
7549    /// A loop-fabric body op with no tile (a nested jump the if-converter cannot absorb, etc.).
7550    UnsupportedLoopBody(String),
7551    /// A `Mem`/`Heap` STATE-PLANE cell reached the register-only SEQUENCED datapath. The sequenced fabric is a
7552    /// register-file arithmetic engine (multi-cycle divider + FPU + power tiles); Seq/string programs carry a
7553    /// shared heap and run on the wpe engine (or the state-plane block form — the named follow-on). Detected up
7554    /// front by [`try_emit_seq_program_verilog`] / [`try_emit_seq_datapath_verilog`] so a heap program is a
7555    /// typed refusal, never a raw panic.
7556    StatePlaneUnlowered(String),
7557}
7558
7559impl std::fmt::Display for LoweringError {
7560    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
7561        match self {
7562            LoweringError::UnsupportedOp(op) => write!(
7563                f,
7564                "cfg block: op `{op}` unsupported (straight-line integer + collection body only)"
7565            ),
7566            LoweringError::UnsupportedBuiltin(b) => write!(
7567                f,
7568                "cfg block: builtin `{b}` unsupported on the whole-program fabric \
7569                 (only floor/sqrt over the float lane are wired)"
7570            ),
7571            LoweringError::UnsupportedLoopBody(op) => {
7572                write!(f, "loop fabric: body op `{op}` unsupported")
7573            }
7574            LoweringError::StatePlaneUnlowered(cell) => write!(
7575                f,
7576                "seq datapath: {cell} (Mem/Heap) is a state-plane follow-on"
7577            ),
7578        }
7579    }
7580}
7581impl std::error::Error for LoweringError {}
7582
7583/// Panicking wrapper preserving the original contract for all existing callers: lowers `fn_name`'s CFG to a
7584/// whole-program fabric, or panics with the same message the old code did. New code that wants to handle an
7585/// unsupported construct gracefully should call [`try_program_cfg_fabric`].
7586///
7587/// A `FailWith` op (a bounds / overflow / divide-by-zero / undefined-variable error trap) is REFUSED — a
7588/// program that reaches one errors in the VM too, so there is no ≡VM value for the fabric to produce. (This
7589/// is correct behaviour, not a gap: it must not be "trapped" away, or the fabric would return a value where
7590/// the VM errors. Realistic error-checked programs whose guards PASS carry no reachable `FailWith` and lower.)
7591pub fn program_cfg_fabric(src: &str, fn_name: &str) -> ProgramCfgFabric {
7592    try_program_cfg_fabric(src, fn_name).unwrap_or_else(|e| panic!("{e}"))
7593}
7594
7595pub fn try_program_cfg_fabric(src: &str, fn_name: &str) -> Result<ProgramCfgFabric, LoweringError> {
7596    try_cfg_fabric_at(src, Some(fn_name))
7597}
7598
7599/// Lower the top-level `## Main` through the CFG engine.
7600///
7601/// ★ WHY THIS EXISTS. The fabric lowers branchy control flow — that is what the CFG engine is — but a
7602/// branchy `## Main` could not reach it. The combinational output-tape path refuses a Main with a branch
7603/// ("a conditional Show cannot ride the combinational output tape"), and the CFG path is addressed BY
7604/// FUNCTION NAME, while the top level is not a named function at all. So the capability was present and
7605/// unreachable for exactly the programs most people write: 80 of 113 source-emittable ops reach the
7606/// fabric per-program, and the missing third is not missing tiles, it is a missing route.
7607///
7608/// The top level is a region of the same `code` array that functions index into, starting at pc 0 with
7609/// no parameters, so the region bound below — "up to the first function entry, else the end" — already
7610/// describes it correctly. Nothing about the lowering changes; only how the region is named.
7611///
7612/// ★ WHERE IT STOPS TODAY, MEASURED. A branchy Main now REACHES the lowering and refuses at
7613/// `UnsupportedOp("Show")` rather than at "function not found" — the route is real and the remaining
7614/// gap is one tile. The CFG engine lowers functions that RETURN a value (`Term::Return`), while `Show`
7615/// is an in-block side effect the combinational output-tape path models and this one does not. Closing
7616/// it is a `CfgBlock.shows: Vec<Src>` (serde-skipped when empty, so the chip serializer's bytes do not
7617/// move), a `Op::Show` arm, and a sibling evaluator returning the shown SEQUENCE instead of the single
7618/// `i64` `eval_program_cfg_fabric` returns — with the equivalence obligation being the VM's output
7619/// stream, not just its final value. That is a feature with a proof to discharge, not a patch.
7620pub fn try_main_cfg_fabric(src: &str) -> Result<ProgramCfgFabric, LoweringError> {
7621    try_cfg_fabric_at(src, None)
7622}
7623
7624fn try_cfg_fabric_at(src: &str, fn_name: Option<&str>) -> Result<ProgramCfgFabric, LoweringError> {
7625    use crate::vm::wasm::cfg::Blocks;
7626    use crate::vm::{Compiler, Constant, Op};
7627    use std::collections::HashMap;
7628
7629    crate::ui_bridge::with_parsed_program(src, |parsed, interner| {
7630        // ★ AND A PROGRAM THAT DOES NOT PARSE IS A REFUSAL, NOT A CRASH. This is the same defect as
7631        // the "function not found" panic below, one step earlier: a coverage probe asks `try_` about
7632        // every program in a corpus, and such a corpus deliberately contains programs that do not
7633        // parse or compile. Panicking on them makes the probe itself the thing that fails, which is
7634        // how this arrived — the matrix's tape route survives them only because it is wrapped in
7635        // `catch_unwind`, and a fallible function should not need its caller to defend against it.
7636        let (stmts, types, _p) = match parsed {
7637            Ok(v) => v,
7638            Err(e) => {
7639                return Err(LoweringError::UnsupportedOp(format!(
7640                    "the program does not parse: {e:?}"
7641                )))
7642            }
7643        };
7644        let program = match Compiler::compile_with_types(stmts, interner, Some(types)) {
7645            Ok(p) => p,
7646            Err(e) => {
7647                return Err(LoweringError::UnsupportedOp(format!(
7648                    "the program does not compile: {e:?}"
7649                )))
7650            }
7651        };
7652        // ★ THE FALLIBLE FORM MUST ACTUALLY FAIL. Naming a function this program does not define is the
7653        // most ordinary way for a caller to be wrong, and panicking on it means `try_` cannot be used to
7654        // ASK whether a function is lowerable — which is exactly what a coverage probe wants to do. The
7655        // top-level `## Main` is the case that matters: it is not a named function at all, so every
7656        // caller reaching for it took a panic instead of a refusal it could handle.
7657        // The region to lower: a named function's entry, or the top-level Main at pc 0.
7658        let (entry_pc, n_params) = match fn_name {
7659            None => (0usize, 0u16),
7660            Some(want) => match program
7661                .functions
7662                .iter()
7663                .find(|f| interner.resolve(f.name) == want)
7664            {
7665                Some(f) => (f.entry_pc, f.param_count),
7666                None => {
7667                    let mut known: Vec<&str> = program
7668                        .functions
7669                        .iter()
7670                        .map(|f| interner.resolve(f.name))
7671                        .collect();
7672                    known.sort_unstable();
7673                    return Err(LoweringError::UnsupportedOp(format!(
7674                        "no function named `{want}` in this program (it defines: {}) — the top-level `## Main` is lowered by `try_main_cfg_fabric`, not by name",
7675                        if known.is_empty() { "nothing".to_string() } else { known.join(", ") }
7676                    )));
7677                }
7678            },
7679        };
7680        // The float lane tracks which registers hold f64 bit patterns; start fresh (a prior decision-fabric
7681        // lowering may have left marks) so this function's arithmetic routes to the right units.
7682        FLOAT_REGS.with(|t| t.borrow_mut().clear());
7683        FLOAT_COLLS.with(|t| t.borrow_mut().clear());
7684        TEXT_REGS.with(|t| t.borrow_mut().clear());
7685        NAME_REGS.with(|t| t.borrow_mut().clear());
7686        COLL_LEN.with(|t| t.borrow_mut().clear());
7687        let entry = entry_pc;
7688        let end = program
7689            .functions
7690            .iter()
7691            .map(|g| g.entry_pc)
7692            .filter(|&e| e > entry)
7693            .min()
7694            .unwrap_or(program.code.len());
7695        // Region-local ops (jump targets rebased to 0) so the block partition is self-contained.
7696        let region: Vec<Op> = (entry..end)
7697            .map(|pc| {
7698                let mut op = program.code[pc];
7699                match &mut op {
7700                    Op::Jump { target }
7701                    | Op::JumpIfFalse { target, .. }
7702                    | Op::JumpIfTrue { target, .. } => *target -= entry,
7703                    _ => {}
7704                }
7705                op
7706            })
7707            .collect();
7708        // Back-edges (loops) are allowed: the block-sequencing controller simply RE-ENTERS the loop header
7709        // block each iteration, carrying loop state in the register file, until the exit branch fires — a
7710        // `While` loop is a header block (condition → Branch) with a body block whose terminator jumps BACK.
7711        // The step-budgeted walkers (`eval_program_cfg_fabric` / `run_program_cfg_on_grid` / the whole-program
7712        // engine) terminate on a returning path; a non-terminating program exhausts the budget and panics
7713        // loudly, exactly as the VM would run forever. (Range/`for-each` loops still need `IterNext` lowering.)
7714        let blocks =
7715            Blocks::new(&region).expect("cfg fabric: function region is not self-contained");
7716        let nb = blocks.num_blocks();
7717
7718        // Read a register into the block's SSA space: a register not yet defined in this block is a LIVE-IN,
7719        // assigned a fresh input slot; a defined one returns its current `Src`.
7720        let get = |reg: &mut HashMap<u16, Src>, inputs: &mut Vec<u16>, r: u16| -> Src {
7721            if let Some(s) = reg.get(&r) {
7722                return *s;
7723            }
7724            let k = inputs.len();
7725            inputs.push(r);
7726            let s = Src::In(k);
7727            reg.insert(r, s);
7728            s
7729        };
7730        let cmp_of = |op: &Op| -> Option<CmpKind> {
7731            Some(match op {
7732                Op::Lt { .. } => CmpKind::Lt,
7733                Op::Gt { .. } => CmpKind::Gt,
7734                Op::LtEq { .. } => CmpKind::Le,
7735                Op::GtEq { .. } => CmpKind::Ge,
7736                Op::Eq { .. } => CmpKind::Eq,
7737                Op::NotEq { .. } => CmpKind::Ne,
7738                _ => return None,
7739            })
7740        };
7741
7742        let mut out_blocks: Vec<CfgBlock> = Vec::with_capacity(nb);
7743        for k in 0..nb {
7744            let bstart = blocks.start(k);
7745            let bend = blocks.end(k);
7746            let ops = &region[bstart..bend];
7747            let (body_ops, term_op): (&[Op], Option<Op>) = match ops.last() {
7748                // ★ `Halt` TERMINATES A `## Main` THE WAY `Return` TERMINATES A FUNCTION. The top level
7749                // does not return a value — it ends, and what it produced is the output tape. Without
7750                // this it fell through to the body match and refused as an unsupported op, which is why
7751                // every branchy Main stopped here even once the route existed.
7752                Some(
7753                    t @ (Op::Jump { .. }
7754                    | Op::JumpIfFalse { .. }
7755                    | Op::JumpIfTrue { .. }
7756                    | Op::Return { .. }
7757                    | Op::ReturnNothing
7758                    | Op::Halt),
7759                ) => (&ops[..ops.len() - 1], Some(*t)),
7760                _ => (ops, None), // a fall-through block
7761            };
7762
7763            let mut reg: HashMap<u16, Src> = HashMap::new();
7764            let mut inputs: Vec<u16> = Vec::new();
7765            let mut cells: Vec<LoopCell> = Vec::new();
7766            let mut shows: Vec<Src> = Vec::new();
7767            let mut write_order: Vec<u16> = Vec::new();
7768            let record = |write_order: &mut Vec<u16>, dst: u16| {
7769                if !write_order.contains(&dst) {
7770                    write_order.push(dst);
7771                }
7772            };
7773            for op in body_ops {
7774                forget_overwritten(op, |func| {
7775                    program
7776                        .functions
7777                        .get(func as usize)
7778                        .map_or(0, |f| f.captures.len())
7779                });
7780                if let Some(kind) = cmp_of(op) {
7781                    if let Op::Lt { dst, lhs, rhs }
7782                    | Op::Gt { dst, lhs, rhs }
7783                    | Op::LtEq { dst, lhs, rhs }
7784                    | Op::GtEq { dst, lhs, rhs }
7785                    | Op::Eq { dst, lhs, rhs }
7786                    | Op::NotEq { dst, lhs, rhs } = op
7787                    {
7788                        let ca = get(&mut reg, &mut inputs, *lhs);
7789                        let cb = get(&mut reg, &mut inputs, *rhs);
7790                        // A 1-char STRING LITERAL in a comparison (`item i of s equals " "`) is compared by BYTE:
7791                        // replace its pointer with `heap[ptr + 1]` (its single byte), so it matches an indexed
7792                        // char (also a byte). A char index is `Op::Index` → a plain byte, so only a bare string
7793                        // register needs this.
7794                        let byte_of = |cells: &mut Vec<LoopCell>, r: u16, s: Src| -> Src {
7795                            let one_char = TEXT_REGS.with(|t| t.borrow().contains(&r))
7796                                && COLL_LEN.with(|t| t.borrow().get(&r).copied() == Some(1));
7797                            if one_char {
7798                                let kadd = cells.len();
7799                                cells.push(LoopCell::Arith {
7800                                    op: AluOp::Add,
7801                                    a: s,
7802                                    b: Src::Const(1),
7803                                });
7804                                let kread = cells.len();
7805                                cells.push(LoopCell::HeapRead {
7806                                    addr: Src::Val(kadd),
7807                                });
7808                                Src::Val(kread)
7809                            } else {
7810                                s
7811                            }
7812                        };
7813                        let ca = byte_of(&mut cells, *lhs, ca);
7814                        let cb = byte_of(&mut cells, *rhs, cb);
7815                        let (lhs_f, rhs_f) = FLOAT_REGS
7816                            .with(|t| (t.borrow().contains(lhs), t.borrow().contains(rhs)));
7817                        if lhs_f || rhs_f {
7818                            // A FLOAT comparison — promote an integer operand, then apply the genuine IEEE-754
7819                            // relation on the two `f64` bit patterns (a plain `Select`'s integer bit-pattern
7820                            // ordering is only sound for same-sign floats). Yields the `0/1` predicate.
7821                            let a2 = promote_float_operand(&mut cells, ca, lhs_f);
7822                            let b2 = promote_float_operand(&mut cells, cb, rhs_f);
7823                            let unit = match kind {
7824                                CmpKind::Lt => FuncUnit::FLtF,
7825                                CmpKind::Gt => FuncUnit::FGtF,
7826                                CmpKind::Le => FuncUnit::FLeF,
7827                                CmpKind::Ge => FuncUnit::FGeF,
7828                                CmpKind::Eq => FuncUnit::FEqF,
7829                                CmpKind::Ne => FuncUnit::FNeF,
7830                            };
7831                            let kk = cells.len();
7832                            cells.push(LoopCell::Func { unit, a: a2, b: b2 });
7833                            reg.insert(*dst, Src::Val(kk));
7834                            FLOAT_REGS.with(|t| {
7835                                t.borrow_mut().remove(dst); // the 0/1 predicate is an Int, not a float
7836                            });
7837                        } else {
7838                            let kk = cells.len();
7839                            // A comparison yields the 0/1 predicate value the terminator/successor consumes.
7840                            cells.push(LoopCell::Select {
7841                                cmp: kind,
7842                                ca,
7843                                cb,
7844                                t: Src::Const(1),
7845                                e: Src::Const(0),
7846                            });
7847                            reg.insert(*dst, Src::Val(kk));
7848                        }
7849                        mark_int_reg(*dst); // a 0/1 predicate is an Int
7850                        record(&mut write_order, *dst);
7851                    }
7852                    continue;
7853                }
7854                match op {
7855                    Op::LoadConst { dst, idx } => match &program.constants[*idx as usize] {
7856                        Constant::Int(n) => {
7857                            reg.insert(*dst, Src::Const(*n));
7858                            mark_int_reg(*dst);
7859                            record(&mut write_order, *dst);
7860                        }
7861                        // A Text literal → a char-list `[len, byte0, byte1, ...]` on the heap (UTF-8 bytes, so
7862                        // `length of` = byte count, matching the VM), exactly as `exec_ops` lowers it. The
7863                        // register holds the base pointer; `length of s` reads `heap[ptr]`.
7864                        Constant::Text(s) => {
7865                            // Reserve growth capacity (like `NewEmptyList`) so a mutable string can be APPENDED to
7866                            // in place (`Set text to text + ch`), and record it as Text with its static byte-length
7867                            // (so an `AddAssign` of a known-length string can be lowered to byte writes).
7868                            let ptr = heap_text_literal(&mut cells, s.as_bytes(), seq_reserve());
7869                            reg.insert(*dst, ptr);
7870                            TEXT_REGS.with(|t| {
7871                                t.borrow_mut().insert(*dst);
7872                            });
7873                            COLL_LEN.with(|t| {
7874                                t.borrow_mut().insert(*dst, s.len() as i64);
7875                            });
7876                            record(&mut write_order, *dst);
7877                        }
7878                        // A Float literal → its f64 BIT PATTERN as an integer constant, with the register marked
7879                        // in FLOAT_REGS so downstream arithmetic routes to the float functional units (the float
7880                        // lane), exactly as the decision-fabric lowering does — so float benchmarks run.
7881                        Constant::Float(f) => {
7882                            reg.insert(*dst, Src::Const(f.to_bits() as i64));
7883                            FLOAT_REGS.with(|t| {
7884                                t.borrow_mut().insert(*dst);
7885                            });
7886                            record(&mut write_order, *dst);
7887                        }
7888                        // A Bool rides a scalar slot as 0/1 (exactly as `exec_ops` lowers it) — so `Seq of Bool`
7889                        // (`sieve`'s flag array) and boolean scalars run.
7890                        Constant::Bool(b) => {
7891                            reg.insert(*dst, Src::Const(i64::from(*b)));
7892                            record(&mut write_order, *dst);
7893                        }
7894                        c => panic!("cfg block: unsupported constant {c:?}"),
7895                    },
7896                    Op::Move { dst, src } => {
7897                        let s = get(&mut reg, &mut inputs, *src);
7898                        reg.insert(*dst, s);
7899                        // A copy carries float-ness: `dst` holds `src`'s value, so it is a float iff `src` is
7900                        // (and ceases to be one otherwise — registers are reused across int and float temps).
7901                        let src_f = FLOAT_REGS.with(|t| t.borrow().contains(src));
7902                        FLOAT_REGS.with(|t| {
7903                            let mut s = t.borrow_mut();
7904                            if src_f {
7905                                s.insert(*dst);
7906                            } else {
7907                                s.remove(dst);
7908                            }
7909                        });
7910                        // …and float-collection-ness (a moved Seq pointer keeps its element type).
7911                        let src_fc = FLOAT_COLLS.with(|t| t.borrow().contains(src));
7912                        FLOAT_COLLS.with(|t| {
7913                            let mut s = t.borrow_mut();
7914                            if src_fc {
7915                                s.insert(*dst);
7916                            } else {
7917                                s.remove(dst);
7918                            }
7919                        });
7920                        // …and text-ness + static byte-length (a moved string pointer keeps both).
7921                        let src_txt = TEXT_REGS.with(|t| t.borrow().contains(src));
7922                        TEXT_REGS.with(|t| {
7923                            let mut s = t.borrow_mut();
7924                            if src_txt {
7925                                s.insert(*dst);
7926                            } else {
7927                                s.remove(dst);
7928                            }
7929                        });
7930                        let src_len = COLL_LEN.with(|t| t.borrow().get(src).copied());
7931                        COLL_LEN.with(|t| {
7932                            let mut m = t.borrow_mut();
7933                            match src_len {
7934                                Some(l) => {
7935                                    m.insert(*dst, l);
7936                                }
7937                                None => {
7938                                    m.remove(dst);
7939                                }
7940                            }
7941                        });
7942                        record(&mut write_order, *dst);
7943                    }
7944                    Op::Add { dst, lhs, rhs }
7945                    | Op::Sub { dst, lhs, rhs }
7946                    | Op::Mul { dst, lhs, rhs } => {
7947                        let a = get(&mut reg, &mut inputs, *lhs);
7948                        let b = get(&mut reg, &mut inputs, *rhs);
7949                        let (lhs_f, rhs_f) = FLOAT_REGS
7950                            .with(|t| (t.borrow().contains(lhs), t.borrow().contains(rhs)));
7951                        if lhs_f || rhs_f {
7952                            // The FLOAT LANE: `f64`-bit-pattern add/sub/mul via the float functional units. An
7953                            // integer operand is first promoted (`2.0 * k` → the VM's `k as f64`), so both
7954                            // operands reach the unit as genuine `f64` bit patterns.
7955                            let a2 = promote_float_operand(&mut cells, a, lhs_f);
7956                            let b2 = promote_float_operand(&mut cells, b, rhs_f);
7957                            let unit = match op {
7958                                Op::Add { .. } => FuncUnit::FAddF,
7959                                Op::Sub { .. } => FuncUnit::FSubF,
7960                                _ => FuncUnit::FMulF,
7961                            };
7962                            let kk = cells.len();
7963                            cells.push(LoopCell::Func { unit, a: a2, b: b2 });
7964                            reg.insert(*dst, Src::Val(kk));
7965                            FLOAT_REGS.with(|t| {
7966                                t.borrow_mut().insert(*dst);
7967                            });
7968                        } else {
7969                            let alu = match op {
7970                                Op::Add { .. } => AluOp::Add,
7971                                Op::Sub { .. } => AluOp::Sub,
7972                                _ => AluOp::Mul,
7973                            };
7974                            let kk = cells.len();
7975                            cells.push(LoopCell::Arith { op: alu, a, b });
7976                            reg.insert(*dst, Src::Val(kk));
7977                            mark_int_reg(*dst);
7978                        }
7979                        record(&mut write_order, *dst);
7980                    }
7981                    Op::Div { dst, lhs, rhs }
7982                    | Op::Mod { dst, lhs, rhs }
7983                    | Op::FloorDiv { dst, lhs, rhs } => {
7984                        let a = get(&mut reg, &mut inputs, *lhs);
7985                        let b = get(&mut reg, &mut inputs, *rhs);
7986                        let (lhs_f, rhs_f) = FLOAT_REGS
7987                            .with(|t| (t.borrow().contains(lhs), t.borrow().contains(rhs)));
7988                        if lhs_f || rhs_f {
7989                            // The FLOAT LANE division (`Mod`/`FloorDiv` on floats never arise — the VM keeps `%`
7990                            // integer-only; `/` on a float is true division). Promote an integer operand first.
7991                            let a2 = promote_float_operand(&mut cells, a, lhs_f);
7992                            let b2 = promote_float_operand(&mut cells, b, rhs_f);
7993                            let kk = cells.len();
7994                            cells.push(LoopCell::Func {
7995                                unit: FuncUnit::FDivF,
7996                                a: a2,
7997                                b: b2,
7998                            });
7999                            reg.insert(*dst, Src::Val(kk));
8000                            FLOAT_REGS.with(|t| {
8001                                t.borrow_mut().insert(*dst);
8002                            });
8003                        } else {
8004                            let unit = match op {
8005                                Op::Mod { .. } => FuncUnit::ModS,
8006                                Op::FloorDiv { .. } => FuncUnit::FloorDivS,
8007                                _ => FuncUnit::DivS,
8008                            };
8009                            let kk = cells.len();
8010                            cells.push(LoopCell::Func { unit, a, b });
8011                            reg.insert(*dst, Src::Val(kk));
8012                            mark_int_reg(*dst);
8013                        }
8014                        record(&mut write_order, *dst);
8015                    }
8016                    // bitwise / shift → the same combinational ALU tiles (`op.eval`/`op.verilog` are total over
8017                    // AluOp, so the grid + SAT paths carry these for free — only the lowering was missing).
8018                    Op::BitAnd { dst, lhs, rhs }
8019                    | Op::BitOr { dst, lhs, rhs }
8020                    | Op::BitXor { dst, lhs, rhs }
8021                    | Op::Shl { dst, lhs, rhs }
8022                    | Op::Shr { dst, lhs, rhs } => {
8023                        let a = get(&mut reg, &mut inputs, *lhs);
8024                        let b = get(&mut reg, &mut inputs, *rhs);
8025                        let alu = match op {
8026                            Op::BitAnd { .. } => AluOp::And,
8027                            Op::BitOr { .. } => AluOp::Or,
8028                            Op::BitXor { .. } => AluOp::Xor,
8029                            Op::Shl { .. } => AluOp::Shl,
8030                            _ => AluOp::Shr,
8031                        };
8032                        let kk = cells.len();
8033                        cells.push(LoopCell::Arith { op: alu, a, b });
8034                        reg.insert(*dst, Src::Val(kk));
8035                        mark_int_reg(*dst);
8036                        record(&mut write_order, *dst);
8037                    }
8038                    // exponentiation → the sequential power functional unit (`FuncUnit::Pow`).
8039                    Op::Pow { dst, lhs, rhs } => {
8040                        let a = get(&mut reg, &mut inputs, *lhs);
8041                        let b = get(&mut reg, &mut inputs, *rhs);
8042                        let kk = cells.len();
8043                        cells.push(LoopCell::Func {
8044                            unit: FuncUnit::Pow,
8045                            a,
8046                            b,
8047                        });
8048                        reg.insert(*dst, Src::Val(kk));
8049                        mark_int_reg(*dst);
8050                        record(&mut write_order, *dst);
8051                    }
8052                    Op::AddAssign { dst, src } => {
8053                        let a = get(&mut reg, &mut inputs, *dst);
8054                        let b = get(&mut reg, &mut inputs, *src);
8055                        let (dst_f, src_f) = FLOAT_REGS
8056                            .with(|t| (t.borrow().contains(dst), t.borrow().contains(src)));
8057                        let dst_txt = TEXT_REGS.with(|t| t.borrow().contains(dst));
8058                        if dst_txt {
8059                            // TEXT APPEND — `Set text to text + s` (the compiler folds this to an in-place `+=`).
8060                            let klen = cells.len();
8061                            cells.push(LoopCell::HeapRead { addr: a }); // len = heap[text_ptr]
8062                            let len = Src::Val(klen);
8063                            if let Some(l) = COLL_LEN.with(|t| t.borrow().get(src).copied()) {
8064                                // rhs is a STRING of statically-known byte-length L (a literal / a register carrying
8065                                // one): copy its L bytes just past `text`'s runtime end, then bump the length.
8066                                for j in 0..l {
8067                                    let ksrc = cells.len();
8068                                    cells.push(LoopCell::Arith {
8069                                        op: AluOp::Add,
8070                                        a: b,
8071                                        b: Src::Const(j + 1),
8072                                    });
8073                                    let kbyte = cells.len();
8074                                    cells.push(LoopCell::HeapRead {
8075                                        addr: Src::Val(ksrc),
8076                                    }); // byte=heap[s_ptr+1+j]
8077                                    let kd1 = cells.len();
8078                                    cells.push(LoopCell::Arith {
8079                                        op: AluOp::Add,
8080                                        a,
8081                                        b: Src::Const(j + 1),
8082                                    });
8083                                    let kd2 = cells.len();
8084                                    cells.push(LoopCell::Arith {
8085                                        op: AluOp::Add,
8086                                        a: Src::Val(kd1),
8087                                        b: len,
8088                                    });
8089                                    cells.push(LoopCell::HeapWrite {
8090                                        addr: Src::Val(kd2),
8091                                        val: Src::Val(kbyte),
8092                                    });
8093                                }
8094                                let knl = cells.len();
8095                                cells.push(LoopCell::Arith {
8096                                    op: AluOp::Add,
8097                                    a: len,
8098                                    b: Src::Const(l),
8099                                });
8100                                cells.push(LoopCell::HeapWrite {
8101                                    addr: a,
8102                                    val: Src::Val(knl),
8103                                });
8104                            } else {
8105                                // rhs is an INT — append its decimal digits (ITOA), all in STATIC cells. `i >= 0` is
8106                                // assumed (a negative would need a `-` the VM adds → the ≡VM check catches it). Digit
8107                                // count `D` is a bounded comparison chain; then D positions are written MSB-first.
8108                                // Positions for `k >= D` (invalid) READ-AND-WRITE-BACK the existing byte — a no-op —
8109                                // so nothing is ever corrupted (even a clamped/negative address is harmless).
8110                                const MAXD: i64 = 19; // covers the full i64 range
8111                                let mut d = Src::Const(1);
8112                                let mut p10: i64 = 10;
8113                                for _ in 1..MAXD {
8114                                    let kge = cells.len();
8115                                    cells.push(LoopCell::Select {
8116                                        cmp: CmpKind::Ge,
8117                                        ca: b,
8118                                        cb: Src::Const(p10),
8119                                        t: Src::Const(1),
8120                                        e: Src::Const(0),
8121                                    });
8122                                    let kadd = cells.len();
8123                                    cells.push(LoopCell::Arith {
8124                                        op: AluOp::Add,
8125                                        a: d,
8126                                        b: Src::Val(kge),
8127                                    });
8128                                    d = Src::Val(kadd);
8129                                    p10 = p10.saturating_mul(10);
8130                                }
8131                                let klpd = cells.len();
8132                                cells.push(LoopCell::Arith {
8133                                    op: AluOp::Add,
8134                                    a: len,
8135                                    b: d,
8136                                }); // len + D
8137                                let lpd = Src::Val(klpd);
8138                                let mut p10k: i64 = 1;
8139                                for k in 0..MAXD {
8140                                    let kdiv = cells.len();
8141                                    cells.push(LoopCell::Func {
8142                                        unit: FuncUnit::DivS,
8143                                        a: b,
8144                                        b: Src::Const(p10k),
8145                                    });
8146                                    let kmod = cells.len();
8147                                    cells.push(LoopCell::Func {
8148                                        unit: FuncUnit::ModS,
8149                                        a: Src::Val(kdiv),
8150                                        b: Src::Const(10),
8151                                    });
8152                                    let kchar = cells.len();
8153                                    cells.push(LoopCell::Arith {
8154                                        op: AluOp::Add,
8155                                        a: Src::Val(kmod),
8156                                        b: Src::Const(48),
8157                                    });
8158                                    // heap address of digit k (MSB-first): `ptr + (len + D) - k` — the base pointer
8159                                    // `a` plus the string offset. `k = 0` (LSB) lands at the last slot, `k = D-1`
8160                                    // (MSB) at the first new slot.
8161                                    let ksub = cells.len();
8162                                    cells.push(LoopCell::Arith {
8163                                        op: AluOp::Sub,
8164                                        a: lpd,
8165                                        b: Src::Const(k),
8166                                    });
8167                                    let kpos = cells.len();
8168                                    cells.push(LoopCell::Arith {
8169                                        op: AluOp::Add,
8170                                        a,
8171                                        b: Src::Val(ksub),
8172                                    });
8173                                    let pos = Src::Val(kpos);
8174                                    let kvalid = cells.len();
8175                                    cells.push(LoopCell::Select {
8176                                        cmp: CmpKind::Lt,
8177                                        ca: Src::Const(k),
8178                                        cb: d,
8179                                        t: Src::Const(1),
8180                                        e: Src::Const(0),
8181                                    });
8182                                    let kexist = cells.len();
8183                                    cells.push(LoopCell::HeapRead { addr: pos });
8184                                    let kwval = cells.len();
8185                                    cells.push(LoopCell::Select {
8186                                        cmp: CmpKind::Ne,
8187                                        ca: Src::Val(kvalid),
8188                                        cb: Src::Const(0),
8189                                        t: Src::Val(kchar),
8190                                        e: Src::Val(kexist),
8191                                    });
8192                                    cells.push(LoopCell::HeapWrite {
8193                                        addr: pos,
8194                                        val: Src::Val(kwval),
8195                                    });
8196                                    p10k = p10k.saturating_mul(10);
8197                                }
8198                                cells.push(LoopCell::HeapWrite { addr: a, val: lpd });
8199                                // heap[text_ptr] = len + D
8200                            }
8201                            reg.insert(*dst, a); // in-place grow — the string pointer is unchanged
8202                            COLL_LEN.with(|t| {
8203                                t.borrow_mut().remove(dst); // its length is now runtime-dependent
8204                            });
8205                        } else if dst_f || src_f {
8206                            // Float accumulate (`Set sum to sum + …` on a float `sum`) — the float lane, with an
8207                            // integer operand promoted first, exactly like the binary `Add`.
8208                            let a2 = promote_float_operand(&mut cells, a, dst_f);
8209                            let b2 = promote_float_operand(&mut cells, b, src_f);
8210                            let kk = cells.len();
8211                            cells.push(LoopCell::Func {
8212                                unit: FuncUnit::FAddF,
8213                                a: a2,
8214                                b: b2,
8215                            });
8216                            reg.insert(*dst, Src::Val(kk));
8217                            FLOAT_REGS.with(|t| {
8218                                t.borrow_mut().insert(*dst);
8219                            });
8220                        } else {
8221                            let kk = cells.len();
8222                            cells.push(LoopCell::Arith {
8223                                op: AluOp::Add,
8224                                a,
8225                                b,
8226                            });
8227                            reg.insert(*dst, Src::Val(kk));
8228                            mark_int_reg(*dst);
8229                        }
8230                        record(&mut write_order, *dst);
8231                    }
8232                    // ── state plane: collection reads/writes lower to `Mem` (heap) + `Arith` tiles, mirroring
8233                    // the whole-program model's `exec_ops` exactly (so the heap layout matches the VM). ONE
8234                    // heap is threaded across the block walk (`eval_program_cfg_fabric` / the sequencer's shared
8235                    // BRAM). Layout `[len, e1..eN]`, 1-based: `length of xs` = heap[ptr]; `item i` = heap[ptr+i].
8236                    Op::Length { dst, collection } => {
8237                        let ptr = get(&mut reg, &mut inputs, *collection);
8238                        let k = cells.len();
8239                        cells.push(LoopCell::HeapRead { addr: ptr });
8240                        reg.insert(*dst, Src::Val(k));
8241                        mark_int_reg(*dst); // a length is an Int
8242                        record(&mut write_order, *dst);
8243                    }
8244                    Op::Index {
8245                        dst,
8246                        collection,
8247                        index,
8248                    }
8249                    | Op::IndexUnchecked {
8250                        dst,
8251                        collection,
8252                        index,
8253                    } => {
8254                        let ptr = get(&mut reg, &mut inputs, *collection);
8255                        let idx = get(&mut reg, &mut inputs, *index);
8256                        let ka = cells.len();
8257                        cells.push(LoopCell::Arith {
8258                            op: AluOp::Add,
8259                            a: ptr,
8260                            b: idx,
8261                        });
8262                        let kr = cells.len();
8263                        cells.push(LoopCell::HeapRead { addr: Src::Val(ka) });
8264                        reg.insert(*dst, Src::Val(kr));
8265                        // The float-in-Seq bridge: `item i of v` off a float Seq yields an f64 bit pattern, so the
8266                        // read register is float and flows into the float functional units.
8267                        let coll_f = FLOAT_COLLS.with(|t| t.borrow().contains(collection));
8268                        FLOAT_REGS.with(|t| {
8269                            let mut s = t.borrow_mut();
8270                            if coll_f {
8271                                s.insert(*dst);
8272                            } else {
8273                                s.remove(dst);
8274                            }
8275                        });
8276                        record(&mut write_order, *dst);
8277                    }
8278                    Op::NewList { dst, start, count } | Op::NewTuple { dst, start, count } => {
8279                        let n = *count;
8280                        // A literal list of floats (`[1.0, 2.0, ...]`) is a float-element collection.
8281                        if (0..n).any(|j| FLOAT_REGS.with(|t| t.borrow().contains(&(*start + j)))) {
8282                            FLOAT_COLLS.with(|t| {
8283                                t.borrow_mut().insert(*dst);
8284                            });
8285                        }
8286                        let ka = cells.len();
8287                        cells.push(LoopCell::HeapAlloc {
8288                            size: Src::Const(n as i64 + 1),
8289                        });
8290                        let ptr = Src::Val(ka);
8291                        cells.push(LoopCell::HeapWrite {
8292                            addr: ptr,
8293                            val: Src::Const(n as i64),
8294                        });
8295                        for j in 0..n {
8296                            let elem = get(&mut reg, &mut inputs, *start + j);
8297                            let kadd = cells.len();
8298                            cells.push(LoopCell::Arith {
8299                                op: AluOp::Add,
8300                                a: ptr,
8301                                b: Src::Const(j as i64 + 1),
8302                            });
8303                            cells.push(LoopCell::HeapWrite {
8304                                addr: Src::Val(kadd),
8305                                val: elem,
8306                            });
8307                        }
8308                        reg.insert(*dst, ptr);
8309                        record(&mut write_order, *dst);
8310                    }
8311                    Op::Args { dst } => {
8312                        let ptr = heap_argv(&mut cells, seq_reserve());
8313                        reg.insert(*dst, ptr);
8314                        TEXT_REGS.with(|t| {
8315                            t.borrow_mut().remove(dst);
8316                        });
8317                        record(&mut write_order, *dst);
8318                    }
8319                    Op::NewEmptyListFixed { dst, capacity } => {
8320                        // A proved fixed allocation owns its full buffer even under a
8321                        // smaller general collection reserve.
8322                        let ka = cells.len();
8323                        cells.push(LoopCell::HeapAlloc {
8324                            size: Src::Const(i64::from(*capacity) + 1),
8325                        });
8326                        let ptr = Src::Val(ka);
8327                        cells.push(LoopCell::HeapWrite {
8328                            addr: ptr,
8329                            val: Src::Const(0),
8330                        });
8331                        reg.insert(*dst, ptr);
8332                        record(&mut write_order, *dst);
8333                    }
8334                    Op::NewEmptyList { dst }
8335                    | Op::NewEmptyListWithCapacity { dst, .. }
8336                    | Op::NewEmptyListI32 { dst }
8337                    | Op::NewEmptySet { dst }
8338                    | Op::NewEmptyMap { dst }
8339                    | Op::NewEmptyMapWithCapacity { dst, .. } => {
8340                        // Reserve the collection's own capacity block up front, so it grows within itself and
8341                        // several collections can grow concurrently without colliding (see `SEQ_RESERVE`).
8342                        let ka = cells.len();
8343                        cells.push(LoopCell::HeapAlloc {
8344                            size: Src::Const(1 + seq_reserve()),
8345                        });
8346                        let ptr = Src::Val(ka);
8347                        cells.push(LoopCell::HeapWrite {
8348                            addr: ptr,
8349                            val: Src::Const(0),
8350                        });
8351                        reg.insert(*dst, ptr);
8352                        record(&mut write_order, *dst);
8353                    }
8354                    Op::DestructureTuple { src, start, count } => {
8355                        let ptr = get(&mut reg, &mut inputs, *src);
8356                        for j in 0..*count {
8357                            let kadd = cells.len();
8358                            cells.push(LoopCell::Arith {
8359                                op: AluOp::Add,
8360                                a: ptr,
8361                                b: Src::Const(j as i64 + 1),
8362                            });
8363                            let kr = cells.len();
8364                            cells.push(LoopCell::HeapRead {
8365                                addr: Src::Val(kadd),
8366                            });
8367                            reg.insert(*start + j, Src::Val(kr));
8368                            record(&mut write_order, *start + j);
8369                        }
8370                    }
8371                    Op::SetIndex {
8372                        collection,
8373                        index,
8374                        value,
8375                    }
8376                    | Op::SetIndexUnchecked {
8377                        collection,
8378                        index,
8379                        value,
8380                    } => {
8381                        // Storing a float into a slot marks the collection float-element-typed.
8382                        if FLOAT_REGS.with(|t| t.borrow().contains(value)) {
8383                            FLOAT_COLLS.with(|t| {
8384                                t.borrow_mut().insert(*collection);
8385                            });
8386                        }
8387                        let ptr = get(&mut reg, &mut inputs, *collection);
8388                        let idx = get(&mut reg, &mut inputs, *index);
8389                        let val = get(&mut reg, &mut inputs, *value);
8390                        let kadd = cells.len();
8391                        cells.push(LoopCell::Arith {
8392                            op: AluOp::Add,
8393                            a: ptr,
8394                            b: idx,
8395                        });
8396                        cells.push(LoopCell::HeapWrite {
8397                            addr: Src::Val(kadd),
8398                            val,
8399                        });
8400                    }
8401                    // `m contains k` on a Map/Set — the DIRECT-ADDRESS table `Set at`/`item` already use (heap[ptr
8402                    // + key]); membership is `heap[ptr + key] != 0`, matching the VM whenever stored values are
8403                    // non-zero (two_sum stores `1`) — the same model that makes `collect`'s map get/set run ≡VM.
8404                    // A key that maps to a stored `0` would read as absent → the census's ≡VM check catches it.
8405                    Op::Contains {
8406                        dst,
8407                        collection,
8408                        value,
8409                    } => {
8410                        let ptr = get(&mut reg, &mut inputs, *collection);
8411                        let key = get(&mut reg, &mut inputs, *value);
8412                        let kaddr = cells.len();
8413                        cells.push(LoopCell::Arith {
8414                            op: AluOp::Add,
8415                            a: ptr,
8416                            b: key,
8417                        });
8418                        let kread = cells.len();
8419                        cells.push(LoopCell::HeapRead {
8420                            addr: Src::Val(kaddr),
8421                        });
8422                        let ksel = cells.len();
8423                        cells.push(LoopCell::Select {
8424                            cmp: CmpKind::Ne,
8425                            ca: Src::Val(kread),
8426                            cb: Src::Const(0),
8427                            t: Src::Const(1),
8428                            e: Src::Const(0),
8429                        });
8430                        reg.insert(*dst, Src::Val(ksel));
8431                        mark_int_reg(*dst); // a 0/1 membership flag is an Int
8432                        record(&mut write_order, *dst);
8433                    }
8434                    Op::ListPush { list, value } => {
8435                        // Pushing a float marks the collection as float-element-typed (the float-in-Seq bridge).
8436                        if FLOAT_REGS.with(|t| t.borrow().contains(value)) {
8437                            FLOAT_COLLS.with(|t| {
8438                                t.borrow_mut().insert(*list);
8439                            });
8440                        }
8441                        let ptr = get(&mut reg, &mut inputs, *list);
8442                        let val = get(&mut reg, &mut inputs, *value);
8443                        let klen = cells.len();
8444                        cells.push(LoopCell::HeapRead { addr: ptr });
8445                        let len = Src::Val(klen);
8446                        let kp1 = cells.len();
8447                        cells.push(LoopCell::Arith {
8448                            op: AluOp::Add,
8449                            a: ptr,
8450                            b: Src::Const(1),
8451                        });
8452                        let kaddr = cells.len();
8453                        cells.push(LoopCell::Arith {
8454                            op: AluOp::Add,
8455                            a: Src::Val(kp1),
8456                            b: len,
8457                        });
8458                        cells.push(LoopCell::HeapWrite {
8459                            addr: Src::Val(kaddr),
8460                            val,
8461                        });
8462                        let knl = cells.len();
8463                        cells.push(LoopCell::Arith {
8464                            op: AluOp::Add,
8465                            a: len,
8466                            b: Src::Const(1),
8467                        });
8468                        cells.push(LoopCell::HeapWrite {
8469                            addr: ptr,
8470                            val: Src::Val(knl),
8471                        });
8472                    }
8473                    Op::ListPop { list, dst } => {
8474                        let ptr = get(&mut reg, &mut inputs, *list);
8475                        let klen = cells.len();
8476                        cells.push(LoopCell::HeapRead { addr: ptr });
8477                        let len = Src::Val(klen);
8478                        let kaddr = cells.len();
8479                        cells.push(LoopCell::Arith {
8480                            op: AluOp::Add,
8481                            a: ptr,
8482                            b: len,
8483                        });
8484                        let kr = cells.len();
8485                        cells.push(LoopCell::HeapRead {
8486                            addr: Src::Val(kaddr),
8487                        });
8488                        let kdec = cells.len();
8489                        cells.push(LoopCell::Arith {
8490                            op: AluOp::Sub,
8491                            a: len,
8492                            b: Src::Const(1),
8493                        });
8494                        cells.push(LoopCell::HeapWrite {
8495                            addr: ptr,
8496                            val: Src::Val(kdec),
8497                        });
8498                        reg.insert(*dst, Src::Val(kr));
8499                        // A popped element off a float Seq is itself a float.
8500                        let coll_f = FLOAT_COLLS.with(|t| t.borrow().contains(list));
8501                        FLOAT_REGS.with(|t| {
8502                            let mut s = t.borrow_mut();
8503                            if coll_f {
8504                                s.insert(*dst);
8505                            } else {
8506                                s.remove(dst);
8507                            }
8508                        });
8509                        record(&mut write_order, *dst);
8510                    }
8511                    // ── the FLOAT→INT bridge: `floor`/`sqrt` over a float-typed register run on the float
8512                    // functional units, mirroring the decision-fabric lowering. `floor` of a float yields an
8513                    // Int (dst leaves FLOAT_REGS); `sqrt` yields a Float (dst stays float). `floor` of an Int
8514                    // is the Int itself. This lets float benchmarks that end in `floor(...)` (their Int result)
8515                    // run on the whole-program engine ≡ VM.
8516                    Op::CallBuiltin {
8517                        dst,
8518                        builtin,
8519                        args_start,
8520                        ..
8521                    } => {
8522                        use crate::semantics::builtins::BuiltinId;
8523                        let a0 = get(&mut reg, &mut inputs, *args_start);
8524                        let is_f = FLOAT_REGS.with(|t| t.borrow().contains(args_start));
8525                        match builtin {
8526                            BuiltinId::Sqrt if is_f => {
8527                                let k = cells.len();
8528                                cells.push(LoopCell::Func {
8529                                    unit: FuncUnit::FSqrtF,
8530                                    a: a0,
8531                                    b: Src::Const(0),
8532                                });
8533                                reg.insert(*dst, Src::Val(k));
8534                                FLOAT_REGS.with(|t| {
8535                                    t.borrow_mut().insert(*dst);
8536                                });
8537                            }
8538                            BuiltinId::Floor if is_f => {
8539                                let k = cells.len();
8540                                cells.push(LoopCell::Func {
8541                                    unit: FuncUnit::FFloorF,
8542                                    a: a0,
8543                                    b: Src::Const(0),
8544                                });
8545                                reg.insert(*dst, Src::Val(k));
8546                                mark_int_reg(*dst); // FFloorF is the float→INT bridge — clear the stale float tag
8547                            }
8548                            BuiltinId::Floor => {
8549                                reg.insert(*dst, a0);
8550                                mark_int_reg(*dst); // floor of an Int is the identity Int
8551                            }
8552                            // Integer abs/min/max: pure select-DAGs — reuse a negate + `Select` (mux) tile,
8553                            // no new functional unit. Float forms fall through (typed `UnsupportedBuiltin`).
8554                            BuiltinId::Abs if !is_f => {
8555                                let kneg = cells.len();
8556                                cells.push(LoopCell::Arith {
8557                                    op: AluOp::Sub,
8558                                    a: Src::Const(0),
8559                                    b: a0,
8560                                });
8561                                let ksel = cells.len();
8562                                cells.push(LoopCell::Select {
8563                                    cmp: CmpKind::Lt,
8564                                    ca: a0,
8565                                    cb: Src::Const(0),
8566                                    t: Src::Val(kneg),
8567                                    e: a0,
8568                                });
8569                                reg.insert(*dst, Src::Val(ksel));
8570                                mark_int_reg(*dst);
8571                            }
8572                            BuiltinId::Min if !is_f => {
8573                                let a1 = get(&mut reg, &mut inputs, *args_start + 1);
8574                                let k = cells.len();
8575                                cells.push(LoopCell::Select {
8576                                    cmp: CmpKind::Lt,
8577                                    ca: a0,
8578                                    cb: a1,
8579                                    t: a0,
8580                                    e: a1,
8581                                });
8582                                reg.insert(*dst, Src::Val(k));
8583                                mark_int_reg(*dst);
8584                            }
8585                            BuiltinId::Max if !is_f => {
8586                                let a1 = get(&mut reg, &mut inputs, *args_start + 1);
8587                                let k = cells.len();
8588                                cells.push(LoopCell::Select {
8589                                    cmp: CmpKind::Gt,
8590                                    ca: a0,
8591                                    cb: a1,
8592                                    t: a0,
8593                                    e: a1,
8594                                });
8595                                reg.insert(*dst, Src::Val(k));
8596                                mark_int_reg(*dst);
8597                            }
8598                            // Float abs/min/max: an IEEE compare tile (`FLtF`/`FGtF` → 0/1) drives a `Select`
8599                            // over the float-bit operands; float negate is `FSubF(0.0, v)` (0.0 bits == 0). The
8600                            // result stays in the float lane.
8601                            BuiltinId::Abs => {
8602                                let kneg = cells.len();
8603                                cells.push(LoopCell::Func {
8604                                    unit: FuncUnit::FSubF,
8605                                    a: Src::Const(0),
8606                                    b: a0,
8607                                });
8608                                let kcmp = cells.len();
8609                                cells.push(LoopCell::Func {
8610                                    unit: FuncUnit::FLtF,
8611                                    a: a0,
8612                                    b: Src::Const(0),
8613                                });
8614                                let ksel = cells.len();
8615                                cells.push(LoopCell::Select {
8616                                    cmp: CmpKind::Ne,
8617                                    ca: Src::Val(kcmp),
8618                                    cb: Src::Const(0),
8619                                    t: Src::Val(kneg),
8620                                    e: a0,
8621                                });
8622                                reg.insert(*dst, Src::Val(ksel));
8623                                FLOAT_REGS.with(|t| {
8624                                    t.borrow_mut().insert(*dst);
8625                                });
8626                            }
8627                            BuiltinId::Min => {
8628                                let a1 = get(&mut reg, &mut inputs, *args_start + 1);
8629                                let kcmp = cells.len();
8630                                cells.push(LoopCell::Func {
8631                                    unit: FuncUnit::FLtF,
8632                                    a: a0,
8633                                    b: a1,
8634                                });
8635                                let ksel = cells.len();
8636                                cells.push(LoopCell::Select {
8637                                    cmp: CmpKind::Ne,
8638                                    ca: Src::Val(kcmp),
8639                                    cb: Src::Const(0),
8640                                    t: a0,
8641                                    e: a1,
8642                                });
8643                                reg.insert(*dst, Src::Val(ksel));
8644                                FLOAT_REGS.with(|t| {
8645                                    t.borrow_mut().insert(*dst);
8646                                });
8647                            }
8648                            BuiltinId::Max => {
8649                                let a1 = get(&mut reg, &mut inputs, *args_start + 1);
8650                                let kcmp = cells.len();
8651                                cells.push(LoopCell::Func {
8652                                    unit: FuncUnit::FGtF,
8653                                    a: a0,
8654                                    b: a1,
8655                                });
8656                                let ksel = cells.len();
8657                                cells.push(LoopCell::Select {
8658                                    cmp: CmpKind::Ne,
8659                                    ca: Src::Val(kcmp),
8660                                    cb: Src::Const(0),
8661                                    t: a0,
8662                                    e: a1,
8663                                });
8664                                reg.insert(*dst, Src::Val(ksel));
8665                                FLOAT_REGS.with(|t| {
8666                                    t.borrow_mut().insert(*dst);
8667                                });
8668                            }
8669                            other => {
8670                                return Err(LoweringError::UnsupportedBuiltin(format!("{other:?}")))
8671                            }
8672                        }
8673                        record(&mut write_order, *dst);
8674                    }
8675                    // ★ `Show v` — APPEND TO THIS BLOCK'S OUTPUT TAPE. The op has no destination
8676                    // register and writes nothing; it contributes a value to the program's output
8677                    // sequence at the point control passes through this block. That is the whole
8678                    // difference between a function (one `Return`) and a `## Main` (a sequence of
8679                    // `Show`s whose contents depend on which way the branches went).
8680                    //
8681                    // Float and text renderings are REFUSED by name, matching the output-tape path's
8682                    // own refusals: the fabric's tape carries integers, and turning an IEEE double or a
8683                    // heap string into decimal characters is a separate lane, not something to fake
8684                    // here. A wrong rendering would be a silently different output stream.
8685                    Op::Show { src } => {
8686                        if FLOAT_REGS.with(|t| t.borrow().contains(src)) {
8687                            return Err(LoweringError::UnsupportedOp(
8688                                "Show of a float (rendering IEEE→decimal is a separate lane)"
8689                                    .into(),
8690                            ));
8691                        }
8692                        if TEXT_REGS.with(|t| t.borrow().contains(src)) {
8693                            return Err(LoweringError::UnsupportedOp(
8694                                "Show of text (rendering a heap string is a separate lane)".into(),
8695                            ));
8696                        }
8697                        let v = get(&mut reg, &mut inputs, *src);
8698                        shows.push(v);
8699                    }
8700                    other => return Err(LoweringError::UnsupportedOp(other.name().to_string())),
8701                }
8702            }
8703
8704            let term = match term_op {
8705                Some(Op::Return { src }) => Term::Return(get(&mut reg, &mut inputs, src)),
8706                // A Main that halts yields no value; its result is the tape it wrote on the way here.
8707                Some(Op::ReturnNothing) | Some(Op::Halt) => Term::Return(Src::Const(0)),
8708                Some(Op::Jump { target }) => Term::Jump(blocks.block_of(target)),
8709                Some(Op::JumpIfFalse { cond, target }) => {
8710                    // JumpIfFalse: cond FALSE → jump to `target` (else); cond TRUE → fall through (then).
8711                    let c = get(&mut reg, &mut inputs, cond);
8712                    Term::Branch {
8713                        cond: c,
8714                        then_blk: blocks.block_of(bend),
8715                        else_blk: blocks.block_of(target),
8716                    }
8717                }
8718                Some(Op::JumpIfTrue { cond, target }) => {
8719                    let c = get(&mut reg, &mut inputs, cond);
8720                    Term::Branch {
8721                        cond: c,
8722                        then_blk: blocks.block_of(target),
8723                        else_blk: blocks.block_of(bend),
8724                    }
8725                }
8726                None => Term::Jump(blocks.block_of(bend)), // straight fall-through to the next block
8727                _ => unreachable!(),
8728            };
8729
8730            let writes: Vec<(u16, Src)> = write_order.iter().map(|r| (*r, reg[r])).collect();
8731            out_blocks.push(CfgBlock {
8732                inputs,
8733                body: cells,
8734                writes,
8735                shows,
8736                term,
8737            });
8738        }
8739
8740        Ok(ProgramCfgFabric {
8741            n_params: n_params as usize,
8742            blocks: out_blocks,
8743        })
8744    })
8745}
8746
8747/// Evaluate a whole-function CFG fabric: seed the register file with `params`, then let the block-sequencing
8748/// controller walk the graph — each block runs its wired residual over its live-in registers, commits its
8749/// writes to the file, and its terminator predicate picks the successor — until a block returns. Equals the
8750/// VM interpreting the same function (proven by `fabric_cfg_lock`).
8751pub fn eval_program_cfg_fabric(pf: &ProgramCfgFabric, params: &[i64]) -> i64 {
8752    eval_program_cfg_fabric_traced(pf, params).0
8753}
8754
8755/// The same walk, reporting the returned value AND the sequence of values SHOWN along the path taken.
8756///
8757/// ★ ONE DEFINITION, TWO VIEWS. A `## Main` is judged by its output STREAM, not by a final register, so
8758/// a fabric lowering of Main is only ≡VM if the shown sequence matches — including the fact that a
8759/// branchy Main shows different things down different paths. Splitting this into a second evaluator
8760/// would have put the block-walking rules in two places, and two copies of a rule disagree the first
8761/// time either moves.
8762pub fn eval_program_cfg_fabric_traced(pf: &ProgramCfgFabric, params: &[i64]) -> (i64, Vec<i64>) {
8763    use std::collections::HashMap;
8764    let mut file: HashMap<u16, i64> = (0..pf.n_params).map(|p| (p as u16, params[p])).collect();
8765    // ONE heap threaded across every block (the state plane): `Mem` cells read/write/alloc against it,
8766    // mirroring `eval_grid_with_heap`'s `[len, e1..eN]` layout. A stateless program never touches it.
8767    let mut heap: Vec<i64> = Vec::new();
8768    let mut shown: Vec<i64> = Vec::new();
8769    let mut blk = 0usize;
8770    for _ in 0..10_000_000 {
8771        let b = &pf.blocks[blk];
8772        let inputs: Vec<i64> = b
8773            .inputs
8774            .iter()
8775            .map(|r| *file.get(r).unwrap_or(&0))
8776            .collect();
8777        let mut vals: Vec<i64> = Vec::with_capacity(b.body.len());
8778        for cell in &b.body {
8779            let v = match cell {
8780                LoopCell::Arith { op, a, b } => op.eval(a.eval(&inputs, &vals), b.eval(&inputs, &vals)),
8781                LoopCell::VectorArith { op, ty, a, b } => {
8782                    op.eval(*ty, a.eval(&inputs, &vals) as u64, b.eval(&inputs, &vals) as u64) as i64
8783                }
8784                LoopCell::Func { unit, a, b } => unit.eval(a.eval(&inputs, &vals), b.eval(&inputs, &vals)),
8785                LoopCell::Select { cmp, ca, cb, t, e } => {
8786                    if cmp.eval(ca.eval(&inputs, &vals), cb.eval(&inputs, &vals)) {
8787                        t.eval(&inputs, &vals)
8788                    } else {
8789                        e.eval(&inputs, &vals)
8790                    }
8791                }
8792                // State plane: the shared heap threaded across blocks (same layout as `eval_grid_with_heap`).
8793                LoopCell::HeapRead { addr } => {
8794                    let a = addr.eval(&inputs, &vals);
8795                    heap.get(a.max(0) as usize).copied().unwrap_or(0)
8796                }
8797                LoopCell::HeapWrite { addr, val } => {
8798                    let a = addr.eval(&inputs, &vals);
8799                    let bv = val.eval(&inputs, &vals);
8800                    let ai = a.max(0) as usize;
8801                    if ai >= heap.len() {
8802                        heap.resize(ai + 1, 0);
8803                    }
8804                    heap[ai] = bv;
8805                    bv
8806                }
8807                LoopCell::HeapAlloc { size } => {
8808                    let a = size.eval(&inputs, &vals);
8809                    let ptr = heap.len() as i64;
8810                    heap.resize(heap.len() + a.max(0) as usize, 0);
8811                    ptr
8812                }
8813                LoopCell::MemRead { .. } => panic!(
8814                    "cfg block body cannot contain a loop-sequence MemRead (the induction-counter scratchpad \
8815                     is the loop fabric's mechanism, distinct from the CFG shared-heap state plane)"
8816                ),
8817            };
8818            vals.push(v);
8819        }
8820        // The tape this block contributes, read in the block's own SSA space before the writes commit.
8821        for src in &b.shows {
8822            shown.push(src.eval(&inputs, &vals));
8823        }
8824        for (r, src) in &b.writes {
8825            file.insert(*r, src.eval(&inputs, &vals));
8826        }
8827        match &b.term {
8828            Term::Jump(t) => blk = *t,
8829            Term::Branch {
8830                cond,
8831                then_blk,
8832                else_blk,
8833            } => {
8834                blk = if cond.eval(&inputs, &vals) != 0 {
8835                    *then_blk
8836                } else {
8837                    *else_blk
8838                }
8839            }
8840            Term::Return(src) => return (src.eval(&inputs, &vals), shown),
8841        }
8842    }
8843    panic!("cfg fabric: controller did not return within the step budget (a back-edge slipped through?)")
8844}
8845
8846/// A counted loop's header at `ops[i]`: an `LtEq { cond, counter, bound }` the next op `JumpIfFalse`s on — the
8847/// shape the compiler gives `Repeat for v from a to b` over Int bounds, the counter running from its prologue value
8848/// to `bound` inclusive. Answers `(counter, bound, exit)`, `exit` the pc the loop leaves to.
8849fn counted_header(ops: &[(usize, crate::vm::Op)], i: usize) -> Option<(u16, u16, usize)> {
8850    use crate::vm::Op;
8851    match (ops.get(i).map(|(_, o)| *o), ops.get(i + 1).map(|(_, o)| *o)) {
8852        (Some(Op::LtEq { dst, lhs, rhs }), Some(Op::JumpIfFalse { cond, target }))
8853            if cond == dst =>
8854        {
8855            Some((lhs, rhs, target))
8856        }
8857        _ => None,
8858    }
8859}
8860
8861/// Whether `op` steps a counted loop's `counter` by one, `is_one` naming the registers that hold 1.
8862fn counted_step(op: &crate::vm::Op, counter: u16, is_one: impl Fn(u16) -> bool) -> bool {
8863    use crate::vm::Op;
8864    match *op {
8865        Op::Add { dst, lhs, rhs } => {
8866            dst == counter && ((lhs == counter && is_one(rhs)) || (rhs == counter && is_one(lhs)))
8867        }
8868        Op::AddAssign { dst, src } => dst == counter && is_one(src),
8869        _ => false,
8870    }
8871}
8872
8873/// How many trailing ops of a counted loop's `body` are its latch, the loop controller's work rather than the
8874/// datapath's. The compiler leaves at the last value before stepping — `Lt t = counter < bound` and a
8875/// `JumpIfFalse t` to the loop's `exit`, ahead of the unit step — so no backend forms `bound + 1`; the fabric's
8876/// controllers stop the same way (`if (iv < hi) iv <= iv + 1; else running <= 0`). 3 for that guarded step, 1 for a
8877/// bare unit step, 0 when the body does not end in the counter's step.
8878fn counted_latch(
8879    body: &[(usize, crate::vm::Op)],
8880    counter: u16,
8881    bound: u16,
8882    exit: usize,
8883    is_one: impl Fn(u16) -> bool,
8884) -> usize {
8885    use crate::vm::Op;
8886    let n = body.len();
8887    if n == 0 || !counted_step(&body[n - 1].1, counter, &is_one) {
8888        return 0;
8889    }
8890    if n >= 3 {
8891        if let (Op::Lt { dst, lhs, rhs }, Op::JumpIfFalse { cond, target }) =
8892            (body[n - 3].1, body[n - 2].1)
8893        {
8894            if lhs == counter && rhs == bound && cond == dst && target == exit {
8895                return 3;
8896            }
8897        }
8898    }
8899    1
8900}
8901
8902/// **The genuine Futamura P1 lowering for a counted loop.** Compile `src`, recover the range
8903/// loop in function `fn_name` (a counted header and its unit step, or `NewRange`/`IterPrepare`/`IterNext`, and the
8904/// back-edge `Jump`), and lower
8905/// it to a [`LoopFabric`]: the loop-carried accumulators (state), the induction variable, the
8906/// loop invariants, and the straight-line body (SSA). Panics on any shape outside the counted
8907/// range-loop family (the honest boundary — nested loops / data-dependent exits are future work).
8908pub fn loop_fabric_from_source(src: &str, fn_name: &str) -> LoopFabric {
8909    use crate::vm::{Compiler, Constant, Op};
8910    use std::collections::{BTreeSet, HashMap};
8911
8912    crate::ui_bridge::with_parsed_program(src, |parsed, interner| {
8913        let (stmts, _types, _pol) = parsed.expect("parse failed");
8914        let program = Compiler::compile(stmts, interner).expect("compile failed");
8915        let f = program
8916            .functions
8917            .iter()
8918            .find(|f| interner.resolve(f.name) == fn_name)
8919            .unwrap_or_else(|| panic!("function `{fn_name}` not found"));
8920
8921        // FLOAT LANE: set up the shared thread-locals (clears + seeds `FLOAT_REGS` etc.) exactly like the
8922        // decision fabric, and seed the float PARAMETERS. The body executor (`exec_ops`) then routes float
8923        // arithmetic to `FuncUnit::F*` tiles by consulting `FLOAT_REGS`; the prologue below only has to admit
8924        // float CONSTANTS and carry float-ness through `Move`. Without this a float loop `panic!`ed on `0.0`.
8925        setup_program_thread_locals(&program);
8926        for (p, pt) in f.param_types.iter().enumerate() {
8927            if matches!(pt, Some(crate::vm::BoundaryType::Float)) {
8928                FLOAT_REGS.with(|t| {
8929                    t.borrow_mut().insert(p as u16);
8930                });
8931            }
8932        }
8933
8934        // The function body: entry_pc .. first Return/ReturnNothing.
8935        let mut ops: Vec<(usize, Op)> = Vec::new();
8936        for (i, op) in program.code.iter().enumerate().skip(f.entry_pc) {
8937            ops.push((i, *op));
8938            if matches!(op, Op::Return { .. } | Op::ReturnNothing) {
8939                break;
8940            }
8941        }
8942
8943        // Prologue: reg -> Src (circuit-level, over params). Params occupy R0..param_count.
8944        let mut pro: HashMap<u16, Src> = HashMap::new();
8945        for p in 0..f.param_count {
8946            pro.insert(p, Src::In(p as usize));
8947        }
8948        let getp = |pro: &HashMap<u16, Src>, r: u16| -> Src {
8949            *pro.get(&r)
8950                .unwrap_or_else(|| panic!("loop prologue reads unwritten register R{r}"))
8951        };
8952
8953        // Walk the prologue until IterPrepare, capturing the range bounds; then IterNext gives
8954        // the induction variable and exit; the body runs to the back-edge Jump.
8955        let mut lo = None;
8956        let mut hi = None;
8957        let mut iv_reg = None;
8958        let mut header_pc = None;
8959        // For a SEQUENCE loop (`Repeat for x in xs`) there is NO `NewRange`: `IterPrepare` names the
8960        // iterated sequence register, and `IterNext`'s dst is the ELEMENT `x` (not a numeric index).
8961        let mut seq_reg: Option<u16> = None;
8962        // A LOCAL collection the loop iterates is built here, into a prologue HEAP island (evaluated once
8963        // before the loop); `pro` maps its register to the list pointer (a `Src::Val` into this island).
8964        let mut prologue_heap: Vec<LoopCell> = Vec::new();
8965        let mut ppreds: HashMap<u16, (CmpKind, Src, Src)> = HashMap::new();
8966        // A counted loop's counter: the induction variable, which its unit step at the end of the body advances.
8967        let mut counter: Option<u16> = None;
8968        // A counted loop's bound register and exit pc, which its latch names (`counted_latch`).
8969        let mut latch_bound: Option<(u16, usize)> = None;
8970        // The registers holding a sequence's length, by the sequence: a counted loop bounded by one, whose body
8971        // first reads the counter's item of that sequence, is that sequence's loop (`Repeat for x in xs`).
8972        let mut lengths: HashMap<u16, u16> = HashMap::new();
8973        let mut by_element = false;
8974        let mut idx = 0usize;
8975        while idx < ops.len() {
8976            let (_pc, op) = ops[idx];
8977            if let Some((c, bound, exit)) = counted_header(&ops, idx) {
8978                latch_bound = Some((bound, exit));
8979                let element = match (lengths.get(&bound), ops.get(idx + 2).map(|(_, o)| *o)) {
8980                    (
8981                        Some(&s),
8982                        Some(
8983                            Op::Index {
8984                                dst,
8985                                collection,
8986                                index,
8987                            }
8988                            | Op::IndexUnchecked {
8989                                dst,
8990                                collection,
8991                                index,
8992                            },
8993                        ),
8994                    ) if collection == s
8995                        && index == c
8996                        && matches!(pro.get(&c), Some(Src::Const(1))) =>
8997                    {
8998                        Some((s, dst))
8999                    }
9000                    _ => None,
9001                };
9002                if let Some((s, x)) = element {
9003                    by_element = true;
9004                    seq_reg = Some(s);
9005                    iv_reg = Some(x);
9006                    header_pc = Some(ops[idx].0);
9007                    counter = Some(c);
9008                    idx += 3;
9009                    break;
9010                }
9011                lo = Some(getp(&pro, c));
9012                hi = Some(getp(&pro, bound));
9013                iv_reg = Some(c);
9014                header_pc = Some(ops[idx].0);
9015                counter = Some(c);
9016                idx += 2;
9017                break;
9018            }
9019            match op {
9020                Op::LoadConst { dst, idx: ci } => match program.constants[ci as usize] {
9021                    Constant::Int(n) => {
9022                        pro.insert(dst, Src::Const(n));
9023                    }
9024                    // A float literal rides its `f64` BIT PATTERN in the scalar slot; `dst` is marked float so
9025                    // the body's `exec_ops` routes downstream arithmetic to the float functional units.
9026                    Constant::Float(fv) => {
9027                        pro.insert(dst, Src::Const(fv.to_bits() as i64));
9028                        FLOAT_REGS.with(|t| {
9029                            t.borrow_mut().insert(dst);
9030                        });
9031                    }
9032                    ref c => panic!(
9033                        "loop fabric: non-integer constant {c:?} (only Int / Float supported)"
9034                    ),
9035                },
9036                Op::Move { dst, src } => {
9037                    let s = getp(&pro, src);
9038                    pro.insert(dst, s);
9039                    // Carry float-ness across a prologue copy (so a moved float const stays a float register).
9040                    if FLOAT_REGS.with(|t| t.borrow().contains(&src)) {
9041                        FLOAT_REGS.with(|t| {
9042                            t.borrow_mut().insert(dst);
9043                        });
9044                    }
9045                }
9046                Op::NewRange { start, end, .. } => {
9047                    lo = Some(getp(&pro, start));
9048                    hi = Some(getp(&pro, end));
9049                }
9050                Op::IterPrepare { iterable } => {
9051                    seq_reg = Some(iterable);
9052                }
9053                Op::Length { dst, collection } => {
9054                    lengths.insert(dst, collection);
9055                }
9056                Op::IterNext { dst, .. } => {
9057                    iv_reg = Some(dst);
9058                    header_pc = Some(ops[idx].0); // the loop header the back-edge jumps to
9059                    idx += 1;
9060                    break;
9061                }
9062                // A local collection the loop iterates (`Let ps be [(1,2),..]`) → build it in the prologue
9063                // heap island via `exec_ops` (NewTuple/NewList/Push → HeapAlloc/HeapWrite). `pro` records its
9064                // register as the list pointer (a `Src::Val` into the island). Straight-line only (no branch).
9065                other => {
9066                    exec_ops(
9067                        &[other],
9068                        &mut pro,
9069                        &mut prologue_heap,
9070                        &mut ppreds,
9071                        &program.constants,
9072                    );
9073                }
9074            }
9075            idx += 1;
9076        }
9077        // A SEQUENCE loop has no `NewRange`; its bound is the iterated sequence's length (derived at eval
9078        // time). `seq_input` names a memory-array PARAMETER; `seq_heap_ptr` names a LOCAL heap list built in
9079        // the prologue island (its length is `heap[ptr]`, element `iv` is `heap[ptr + 1 + iv]`).
9080        let (seq_input, seq_heap_ptr): (Option<usize>, Option<Src>) = if lo.is_none() {
9081            let sreg = seq_reg.expect("loop fabric: neither NewRange nor IterPrepare (not a loop)");
9082            match getp(&pro, sreg) {
9083                Src::In(k) => (Some(k), None),
9084                s @ Src::Val(_) => (None, Some(s)), // a local heap list built in the prologue island
9085                other => panic!("loop fabric: the iterated sequence is not a parameter or a local list ({other:?})"),
9086            }
9087        } else {
9088            (None, None)
9089        };
9090        let lo = lo.unwrap_or(Src::Const(0));
9091        let hi = hi.unwrap_or(Src::Const(0));
9092        let iv_reg = iv_reg.expect("loop fabric: no IterNext (not a loop)");
9093        let header_pc = header_pc.expect("loop fabric: no loop header");
9094        // A sequence loop's `IterNext` dst is the ELEMENT, which inherits the sequence's element
9095        // kind: over a `Seq of Float` parameter it carries an f64 bit pattern, so it must be
9096        // float-marked or element×element arithmetic (`x * x`) routes to the integer ALU and
9097        // multiplies bit patterns. (Float CONSTANTS already mark their registers; this is the
9098        // element-register half of the same discipline.)
9099        if let Some(k) = seq_input {
9100            if matches!(
9101                f.param_types.get(k),
9102                Some(Some(crate::vm::BoundaryType::Seq(elem)))
9103                    if matches!(**elem, crate::vm::BoundaryType::Float)
9104            ) {
9105                FLOAT_REGS.with(|t| {
9106                    t.borrow_mut().insert(iv_reg);
9107                });
9108            }
9109        }
9110
9111        // Body = ops from here to the BACK-EDGE Jump (the one targeting the header), exclusive.
9112        // Internal jumps (an if/else's then→merge edge) stay in the body — they are the branch
9113        // structure the if-conversion below consumes. Anything after is IterPop/Return.
9114        let mut body_ops: Vec<(usize, Op)> = Vec::new();
9115        let mut result_reg = None;
9116        while idx < ops.len() {
9117            let (pc, op) = ops[idx];
9118            if matches!(op, Op::Jump { target } if target == header_pc) {
9119                idx += 1;
9120                break;
9121            }
9122            body_ops.push((pc, op));
9123            idx += 1;
9124        }
9125        // A counted loop's latch ends its body: the loop controller advances the counter, not the datapath.
9126        if let Some(c) = counter {
9127            let is_one = |r: u16| matches!(pro.get(&r), Some(Src::Const(1)));
9128            let (bound, exit) = latch_bound.expect("a counted loop has a header");
9129            match counted_latch(&body_ops, c, bound, exit, is_one) {
9130                0 => panic!("loop fabric: a counted loop's body does not end in its unit step"),
9131                n => body_ops.truncate(body_ops.len() - n),
9132            }
9133            // A sequence's loop carries its element, not its index: a body that reads the index is outside it.
9134            if by_element {
9135                for (_, op) in &body_ops {
9136                    op.for_each_read_reg(
9137                        |_| 0,
9138                        |r| {
9139                            assert!(
9140                                r != c,
9141                                "loop fabric: a sequence loop's body reads its index"
9142                            );
9143                        },
9144                    );
9145                }
9146            }
9147        }
9148        // Post-loop ops: `IterPop`/`Return` bracket the loop; any straight-line arithmetic between them
9149        // is the EPILOGUE — a transform island over the loop's final state (the first CFG composition).
9150        let mut epilogue_ops: Vec<Op> = Vec::new();
9151        for (_pc, op) in &ops[idx..] {
9152            match op {
9153                Op::IterPop | Op::ReturnNothing => {}
9154                Op::Return { src } => result_reg = Some(*src),
9155                Op::LoadConst { .. }
9156                | Op::Move { .. }
9157                | Op::Add { .. }
9158                | Op::Sub { .. }
9159                | Op::Mul { .. }
9160                | Op::Div { .. }
9161                | Op::Mod { .. }
9162                | Op::FloorDiv { .. }
9163                | Op::AddAssign { .. } => epilogue_ops.push(*op),
9164                other => panic!(
9165                    "loop fabric: unexpected post-loop op `{}` (only a straight-line epilogue transform \
9166                     over the loop result + Return is supported)",
9167                    other.name()
9168                ),
9169            }
9170        }
9171        let result_reg = result_reg.expect("loop fabric: loop has no Return");
9172
9173        // Classify body registers: reads-before-write in the body that are ALSO written are
9174        // loop-carried; reads-before-write that are NOT written are loop invariants (bound to
9175        // their prologue value); the iv register is the induction variable.
9176        let reg_of = |op: &Op| -> (Option<u16>, Vec<u16>) {
9177            match op {
9178                Op::LoadConst { dst, .. } => (Some(*dst), vec![]),
9179                // `now` / `today` — a clock value-load (reads the injected fixed clock); no register read.
9180                Op::LoadNow { dst } | Op::LoadToday { dst } => (Some(*dst), vec![]),
9181                Op::Move { dst, src } => (Some(*dst), vec![*src]),
9182                // Logical `not` → a `(src == 0) ? 1 : 0` Select mux (one read, like `Move`).
9183                Op::Not { dst, src } => (Some(*dst), vec![*src]),
9184                // A numeric-coprocessor builtin (sqrt/floor/abs/min/max) reads its `arg_count` argument
9185                // registers starting at `args_start`; `exec_ops` lowers it to the matching Func/Select tile.
9186                Op::CallBuiltin { dst, args_start, arg_count, .. } => {
9187                    (Some(*dst), (0..*arg_count as u16).map(|k| *args_start + k).collect())
9188                }
9189                Op::Add { dst, lhs, rhs }
9190                | Op::Sub { dst, lhs, rhs }
9191                | Op::Mul { dst, lhs, rhs }
9192                // Division / mod / floor-div in the loop body: `exec_ops` lowers each to a functional-unit
9193                // tile (integer `DivS`/`ModS`/`FloorDivS`, or `FDivF` when an operand is a float) — a
9194                // combinational Func tile, exactly like the epilogue already admits. So `Set s to s / 2.0`
9195                // is a real FDivF tile, and integer `x % k` / `x // k` counted loops also lower.
9196                | Op::Div { dst, lhs, rhs }
9197                | Op::Mod { dst, lhs, rhs }
9198                | Op::FloorDiv { dst, lhs, rhs }
9199                // Bitwise / shifts → combinational Arith tiles (integer): `a & b`, `a | b`, `a xor b`,
9200                // `a << b`, `a >> b`. `exec_ops` already emits the matching `AluOp`.
9201                | Op::BitAnd { dst, lhs, rhs }
9202                | Op::BitOr { dst, lhs, rhs }
9203                | Op::BitXor { dst, lhs, rhs }
9204                | Op::Shl { dst, lhs, rhs }
9205                | Op::Shr { dst, lhs, rhs }
9206                // Power (`**` / `to the power of`) → the sequential-multiply `Pow` functional-unit tile.
9207                | Op::Pow { dst, lhs, rhs }
9208                | Op::Lt { dst, lhs, rhs }
9209                | Op::Gt { dst, lhs, rhs }
9210                | Op::LtEq { dst, lhs, rhs }
9211                | Op::GtEq { dst, lhs, rhs }
9212                | Op::Eq { dst, lhs, rhs }
9213                | Op::NotEq { dst, lhs, rhs } => (Some(*dst), vec![*lhs, *rhs]),
9214                Op::AddAssign { dst, src } => (Some(*dst), vec![*dst, *src]),
9215                // Jumps carry no STATE register; they are the branch structure the if-conversion
9216                // below consumes. Anything else in a loop body is unsupported (fail loud).
9217                Op::Jump { .. } | Op::JumpIfFalse { .. } => (None, vec![]),
9218                other => panic!("loop fabric: body op `{}` unsupported", other.name()),
9219            }
9220        };
9221        let mut written: BTreeSet<u16> = BTreeSet::new();
9222        let mut upward: BTreeSet<u16> = BTreeSet::new();
9223        for (_pc, op) in &body_ops {
9224            // `DestructureTuple` is MULTI-OUTPUT (`start..start+count`) — it reads the element register and
9225            // writes every binder, so record it directly (a single-`dst` `reg_of` cannot express it).
9226            if let Op::DestructureTuple { src, start, count } = op {
9227                if *src != iv_reg && !written.contains(src) {
9228                    upward.insert(*src);
9229                }
9230                for j in 0..*count {
9231                    written.insert(*start + j);
9232                }
9233                continue;
9234            }
9235            let (dst, reads) = reg_of(op);
9236            for r in reads {
9237                if r != iv_reg && !written.contains(&r) {
9238                    upward.insert(r);
9239                }
9240            }
9241            if let Some(d) = dst {
9242                written.insert(d);
9243            }
9244        }
9245        // result_reg is read after the loop → loop-carried if the body writes it.
9246        let mut carried: Vec<u16> = upward.intersection(&written).copied().collect();
9247        if written.contains(&result_reg) && !carried.contains(&result_reg) {
9248            carried.push(result_reg);
9249        }
9250        carried.sort_unstable();
9251        let invariants_regs: Vec<u16> = upward.difference(&written).copied().collect();
9252
9253        let n_state = carried.len();
9254        let state_slot: HashMap<u16, usize> =
9255            carried.iter().enumerate().map(|(i, r)| (*r, i)).collect();
9256        let inv_slot: HashMap<u16, usize> = invariants_regs
9257            .iter()
9258            .enumerate()
9259            .map(|(i, r)| (*r, i))
9260            .collect();
9261
9262        // Seed the body-SSA register map. Body inputs: 0..n_state = state, n_state = iv,
9263        // n_state+1+j = invariant j.
9264        let mut reg: HashMap<u16, Src> = HashMap::new();
9265        for (r, s) in &state_slot {
9266            reg.insert(*r, Src::In(*s));
9267        }
9268        for (r, s) in &inv_slot {
9269            reg.insert(*r, Src::In(n_state + 1 + *s));
9270        }
9271        // Body input `Src::In(n_state)` is the induction variable. For a NUMERIC loop that IS the value
9272        // `iv_reg` holds. For a SEQUENCE loop it is the ADDRESS (0..len-1) driving a memory-read tile;
9273        // `iv_reg` (the element `x`) is that tile's result, so we seed cell 0 = `MemRead(addr = iv)`.
9274        let init_cells: Vec<LoopCell> = if seq_input.is_some() {
9275            reg.insert(iv_reg, Src::Val(0));
9276            vec![LoopCell::MemRead {
9277                addr: Src::In(n_state),
9278            }]
9279        } else if seq_heap_ptr.is_some() {
9280            // FOREACH over a local heap list: element `iv` = `heap[list_ptr + 1 + iv]`. The list pointer is
9281            // the FINAL invariant slot; `iv_reg` (the element, a tuple ptr) is the seeded HeapRead's result.
9282            let list_ptr_slot = n_state + 1 + invariants_regs.len();
9283            reg.insert(iv_reg, Src::Val(2));
9284            vec![
9285                LoopCell::Arith {
9286                    op: AluOp::Add,
9287                    a: Src::In(list_ptr_slot),
9288                    b: Src::Const(1),
9289                }, // list_ptr + 1
9290                LoopCell::Arith {
9291                    op: AluOp::Add,
9292                    a: Src::Val(0),
9293                    b: Src::In(n_state),
9294                }, // + iv
9295                LoopCell::HeapRead { addr: Src::Val(1) }, // heap[…] = element
9296            ]
9297        } else {
9298            reg.insert(iv_reg, Src::In(n_state));
9299            Vec::new()
9300        };
9301
9302        // If-CONVERT the body (shared with the while-loop extractor): straight-line ops, and any
9303        // data-dependent branch becomes a predicated Select (the mux tile) — control becomes data.
9304        let body = if_convert_body(
9305            &body_ops,
9306            &mut reg,
9307            &carried,
9308            &program.constants,
9309            init_cells,
9310        );
9311
9312        let init: Vec<Src> = carried.iter().map(|r| getp(&pro, *r)).collect();
9313        let next: Vec<Src> = carried.iter().map(|&r| *reg.get(&r).unwrap()).collect();
9314        let mut invariants: Vec<Src> = invariants_regs.iter().map(|r| getp(&pro, *r)).collect();
9315        // A local-heap-list foreach passes the iterated list's POINTER as the final invariant (the body's
9316        // element-address generator reads `heap[list_ptr + 1 + iv]`); `init`/`invariants` evaluate over the
9317        // prologue-heap island in `eval_loop_fabric_heap`.
9318        if let Some(ptr) = seq_heap_ptr {
9319            invariants.push(ptr);
9320        }
9321        // Build the EPILOGUE island: a straight-line transform over the loop's FINAL state (input
9322        // convention `Src::In(i)` = final value of state register `i`). Empty when the loop result is a
9323        // carried register (then `result_state` names it). The epilogue reads state + constants; reading a
9324        // parameter post-loop is not yet supported (it would need a distinct input slot) and fails loud.
9325        let (epilogue, epilogue_result, result_state) = if epilogue_ops.is_empty() {
9326            let rs = *state_slot
9327                .get(&result_reg)
9328                .expect("loop fabric: the returned register is not loop-carried");
9329            (Vec::new(), Src::Const(0), rs)
9330        } else {
9331            let mut ereg: HashMap<u16, Src> = HashMap::new();
9332            for (r, s) in &state_slot {
9333                ereg.insert(*r, Src::In(*s));
9334            }
9335            let mut ecells: Vec<LoopCell> = Vec::new();
9336            let mut epreds: HashMap<u16, (CmpKind, Src, Src)> = HashMap::new();
9337            exec_ops(
9338                &epilogue_ops,
9339                &mut ereg,
9340                &mut ecells,
9341                &mut epreds,
9342                &program.constants,
9343            );
9344            let eres = *ereg
9345                .get(&result_reg)
9346                .expect("loop fabric: the epilogue does not define the returned register");
9347            (ecells, eres, 0usize)
9348        };
9349
9350        LoopFabric {
9351            n_in: f.param_count as usize,
9352            n_state,
9353            init,
9354            lo,
9355            hi,
9356            invariants,
9357            body,
9358            next,
9359            result_state,
9360            seq_input,
9361            epilogue,
9362            epilogue_result,
9363            prologue_heap,
9364            seq_heap_ptr,
9365        }
9366    })
9367}
9368
9369/// Evaluate the CGRA op-by-op the way the sequential controller does: initialise the state, then
9370/// for each induction value in `lo..=hi` apply the body and latch the next state. 64-bit
9371/// wrapping — bit-identical to the VM's `Int` reduced mod 2⁶⁴. This is the reference the emitted
9372/// clocked circuit is checked against.
9373pub fn eval_loop_fabric(lf: &LoopFabric, params: &[i64]) -> i64 {
9374    if lf.seq_heap_ptr.is_some() {
9375        eval_loop_fabric_heap(lf, params)
9376    } else {
9377        eval_loop_fabric_seq(lf, params, &[])
9378    }
9379}
9380
9381/// Evaluate a FOREACH OVER A LOCAL HEAP LIST (`seq_heap_ptr.is_some()`). The prologue-heap island builds the
9382/// list into a shared heap ONCE; then for each `iv` in `0..heap[list_ptr]` the body runs with that heap
9383/// THREADED — the element `heap[list_ptr + 1 + iv]` is destructured via `HeapRead`, and any collection op in
9384/// the body reads/writes the same heap (so mutation across iterations persists). Bit-identical to the VM.
9385pub fn eval_loop_fabric_heap(lf: &LoopFabric, params: &[i64]) -> i64 {
9386    let mut heap: Vec<i64> = Vec::new();
9387    let pro_vals = eval_cells(&lf.prologue_heap, params, &mut heap);
9388    let list_ptr = lf
9389        .seq_heap_ptr
9390        .as_ref()
9391        .expect("eval_loop_fabric_heap: not a heap-list loop")
9392        .eval(params, &pro_vals);
9393    let len = heap.get(list_ptr as usize).copied().unwrap_or(0);
9394    // Initial state + invariants evaluate over the PROLOGUE-HEAP island (a carried init or the list ptr may be
9395    // a `Src::Val` into it); the final invariant is `list_ptr`.
9396    let mut state: Vec<i64> = lf.init.iter().map(|s| s.eval(params, &pro_vals)).collect();
9397    let invs: Vec<i64> = lf
9398        .invariants
9399        .iter()
9400        .map(|s| s.eval(params, &pro_vals))
9401        .collect();
9402    for iv in 0..len {
9403        let mut bin: Vec<i64> = Vec::with_capacity(lf.n_state + 1 + invs.len());
9404        bin.extend_from_slice(&state);
9405        bin.push(iv);
9406        bin.extend_from_slice(&invs);
9407        let vals = eval_cells(&lf.body, &bin, &mut heap); // heap THREADED through the body
9408        state = lf.next.iter().map(|s| s.eval(&bin, &vals)).collect();
9409    }
9410    if lf.epilogue.is_empty() {
9411        state[lf.result_state]
9412    } else {
9413        let evals = eval_cells(&lf.epilogue, &state, &mut heap);
9414        lf.epilogue_result.eval(&state, &evals)
9415    }
9416}
9417
9418/// Evaluate ONE execution of a loop fabric's body over `bin` (the body-input convention: `0..n_state` =
9419/// the current state, `n_state` = the induction variable, then the invariants), returning every SSA value.
9420/// The single oracle every pipelined / metapipelined model of the body is checked against.
9421pub fn eval_loop_body(lf: &LoopFabric, bin: &[i64], seq: &[i64]) -> Vec<i64> {
9422    let mut vals: Vec<i64> = Vec::with_capacity(lf.body.len());
9423    for cell in &lf.body {
9424        let v = match cell {
9425            LoopCell::Arith { op, a, b } => op.eval(a.eval(bin, &vals), b.eval(bin, &vals)),
9426            LoopCell::VectorArith { op, ty, a, b } => {
9427                op.eval(*ty, a.eval(bin, &vals) as u64, b.eval(bin, &vals) as u64) as i64
9428            }
9429            LoopCell::Func { unit, a, b } => unit.eval(a.eval(bin, &vals), b.eval(bin, &vals)),
9430            LoopCell::MemRead { addr } => {
9431                let a = addr.eval(bin, &vals);
9432                seq.get(a as usize).copied().unwrap_or(0)
9433            }
9434            LoopCell::HeapRead { .. } | LoopCell::HeapWrite { .. } | LoopCell::HeapAlloc { .. } => panic!(
9435                "heap tile in a loop body is a state-plane follow-on (collection ops inside a loop); \
9436                 straight-line collection ops evaluate via eval_decision_fabric_heap"
9437            ),
9438            LoopCell::Select { cmp, ca, cb, t, e } => {
9439                if cmp.eval(ca.eval(bin, &vals), cb.eval(bin, &vals)) {
9440                    t.eval(bin, &vals)
9441                } else {
9442                    e.eval(bin, &vals)
9443                }
9444            }
9445        };
9446        vals.push(v);
9447    }
9448    vals
9449}
9450
9451/// Evaluate a loop fabric, supplying the iterated SEQUENCE's element values when it is a sequence loop
9452/// (`seq_input.is_some()`). The induction range is then `0..=seq.len()-1` (an empty seq runs zero
9453/// iterations, exactly like the VM), and each [`LoopCell::MemRead`] reads `seq[addr]`. For a numeric
9454/// range loop, `seq` is unused and the bounds come from `lo`/`hi`.
9455pub fn eval_loop_fabric_seq(lf: &LoopFabric, params: &[i64], seq: &[i64]) -> i64 {
9456    let empty: [i64; 0] = [];
9457    let mut state: Vec<i64> = lf.init.iter().map(|s| s.eval(params, &empty)).collect();
9458    let (lo, hi) = if lf.seq_input.is_some() {
9459        (0i64, seq.len() as i64 - 1) // empty seq → hi == -1 → zero iterations
9460    } else {
9461        (lf.lo.eval(params, &empty), lf.hi.eval(params, &empty))
9462    };
9463    let invs: Vec<i64> = lf
9464        .invariants
9465        .iter()
9466        .map(|s| s.eval(params, &empty))
9467        .collect();
9468    let mut iv = lo;
9469    while iv <= hi {
9470        let mut bin: Vec<i64> = Vec::with_capacity(lf.n_state + 1 + invs.len());
9471        bin.extend_from_slice(&state);
9472        bin.push(iv);
9473        bin.extend_from_slice(&invs);
9474        let vals = eval_loop_body(lf, &bin, seq);
9475        state = lf.next.iter().map(|s| s.eval(&bin, &vals)).collect();
9476        if iv == hi {
9477            break; // avoid i64 overflow at hi == i64::MAX; the VM stops here too
9478        }
9479        iv = iv.wrapping_add(1);
9480    }
9481    // Post-loop EPILOGUE island: a straight-line transform over the loop's FINAL state (`Src::In(i)` =
9482    // final state[i], `Src::Val(j)` = epilogue cell j). Empty ⇒ the result is a carried register.
9483    if lf.epilogue.is_empty() {
9484        return state[lf.result_state];
9485    }
9486    let mut evals: Vec<i64> = Vec::with_capacity(lf.epilogue.len());
9487    for cell in &lf.epilogue {
9488        let v = match cell {
9489            LoopCell::Arith { op, a, b } => op.eval(a.eval(&state, &evals), b.eval(&state, &evals)),
9490            LoopCell::VectorArith { op, ty, a, b } => op.eval(
9491                *ty,
9492                a.eval(&state, &evals) as u64,
9493                b.eval(&state, &evals) as u64,
9494            ) as i64,
9495            LoopCell::Func { unit, a, b } => {
9496                unit.eval(a.eval(&state, &evals), b.eval(&state, &evals))
9497            }
9498            LoopCell::Select { cmp, ca, cb, t, e } => {
9499                if cmp.eval(ca.eval(&state, &evals), cb.eval(&state, &evals)) {
9500                    t.eval(&state, &evals)
9501                } else {
9502                    e.eval(&state, &evals)
9503                }
9504            }
9505            LoopCell::MemRead { .. }
9506            | LoopCell::HeapRead { .. }
9507            | LoopCell::HeapWrite { .. }
9508            | LoopCell::HeapAlloc { .. } => {
9509                panic!("epilogue tile cannot be a memory/heap tile — no sequence iteration after the loop")
9510            }
9511        };
9512        evals.push(v);
9513    }
9514    lf.epilogue_result.eval(&state, &evals)
9515}
9516
9517/// Emit the CGRA as a SEQUENTIAL circuit: a combinational body datapath (one `wire` per SSA
9518/// value + next-state) plus a controller that clocks the loop-carried state through it once per
9519/// induction value in `lo..=hi`. No fetch, no opcode dispatch: the body is WIRED (spatial), the
9520/// loop is a counter (temporal). `done` pulses when the loop is finished; `result` is the
9521/// returned accumulator.
9522pub fn emit_loop_fabric_verilog(name: &str, lf: &LoopFabric) -> String {
9523    emit_loop_fabric_verilog_dp(name, lf, None)
9524}
9525
9526/// Emit the CGRA RTL with an INT8-NATIVE datapath: every multiply tile's operands are `mul_bits`-bit signed,
9527/// so each MAC maps to exactly ONE DSP48E1 (a 25×18 signed multiplier) instead of the ~10 DSPs a 64×64
9528/// multiply decomposes into. The product is sign-extended back to the 64-bit accumulator, so the ADDS and the
9529/// carried accumulator stay exact 64-bit — the result is byte-identical to the 64-bit fabric (and the VM)
9530/// whenever the multiply operands fit in `mul_bits` (which the int8 model guarantees, and the ≡VM locks
9531/// verify). This is the datapath that makes the spatial array DSP-packable and fast on real silicon.
9532pub fn emit_loop_fabric_verilog_int8(name: &str, lf: &LoopFabric, mul_bits: u32) -> String {
9533    assert!(
9534        (2..=32).contains(&mul_bits),
9535        "int8 datapath mul_bits must be 2..=32 (got {mul_bits})"
9536    );
9537    emit_loop_fabric_verilog_dp(name, lf, Some(mul_bits))
9538}
9539
9540/// One body cell of a counted-loop CGRA as Verilog wires, the value named `b{i}`: an INT8-NATIVE multiply when
9541/// `mul_bits` is set, a sequence read, or one arithmetic, select or functional-unit tile.
9542fn loop_body_wire(
9543    i: usize,
9544    cell: &LoopCell,
9545    mul_bits: Option<u32>,
9546    name_in: &dyn Fn(usize) -> String,
9547    name_val: &dyn Fn(usize) -> String,
9548    mem_addrw: usize,
9549) -> String {
9550    let mut out = String::new();
9551    // INT8-NATIVE MULTIPLY: narrow signed operands → one DSP48E1. Truncating each 64-bit value to its low
9552    // `w` bits and reading it signed is exact for operands in [-2^(w-1), 2^(w-1)-1] (the int8 range the
9553    // model lives in); the 2w-bit product then sign-extends into the 64-bit accumulator, so downstream
9554    // adds/accumulation are unchanged. Verified byte-identical to the 64-bit path (and the VM) by the locks.
9555    if let (
9556        Some(w),
9557        LoopCell::Arith {
9558            op: AluOp::Mul,
9559            a,
9560            b,
9561        },
9562    ) = (mul_bits, cell)
9563    {
9564        let a_s = src_named(a, name_in, name_val);
9565        let b_s = src_named(b, name_in, name_val);
9566        out.push_str(&format!(
9567            "  wire signed [{h}:0] ma{i} = {a_s};\n  wire signed [{h}:0] mb{i} = {b_s};\n  \
9568             wire signed [{ph}:0] mp{i} = ma{i} * mb{i};\n  wire [63:0] b{i} = mp{i};\n",
9569            h = w - 1,
9570            ph = 2 * w - 1,
9571        ));
9572        return out;
9573    }
9574    // SEQUENCE-LOOP OPERAND READ: `mem[addr]` from the on-chip operand buffer (addr is the induction
9575    // counter for `Repeat for x in xs`). Part-select needs a named wire, so bind the address first.
9576    if let LoopCell::MemRead { addr } = cell {
9577        let a_s = src_named(addr, name_in, name_val);
9578        out.push_str(&format!(
9579            "  wire [63:0] amr{i} = {a_s};\n  wire [63:0] b{i} = mem[amr{i}[{aw}:0]];\n",
9580            aw = mem_addrw - 1
9581        ));
9582        return out;
9583    }
9584    let rhs = match cell {
9585        LoopCell::Arith { op, a, b } => op.verilog(
9586            &src_named(a, name_in, name_val),
9587            &src_named(b, name_in, name_val),
9588        ),
9589        LoopCell::VectorArith { op, ty, a, b } => op.verilog(
9590            *ty,
9591            &src_named(a, name_in, name_val),
9592            &src_named(b, name_in, name_val),
9593        ),
9594        LoopCell::Func { unit, a, b } => unit.verilog(&src_named(a, name_in, name_val), &src_named(b, name_in, name_val)),
9595        LoopCell::MemRead { .. } => unreachable!("MemRead is emitted before the match"),
9596        LoopCell::HeapRead { .. } | LoopCell::HeapWrite { .. } | LoopCell::HeapAlloc { .. } => panic!(
9597            "Heap-tile RTL emission (a shared-heap read/write inside a loop) is the state-plane \
9598             follow-on; a sequence loop's operand read lowers via LoopCell::MemRead"
9599        ),
9600        LoopCell::Select { cmp, ca, cb, t, e } => format!(
9601            "($signed({}) {} $signed({})) ? {} : {}",
9602            src_named(ca, name_in, name_val),
9603            cmp.verilog(),
9604            src_named(cb, name_in, name_val),
9605            src_named(t, name_in, name_val),
9606            src_named(e, name_in, name_val),
9607        ),
9608    };
9609    out.push_str(&format!("  wire [63:0] b{i} = {rhs};\n"));
9610    out
9611}
9612
9613fn emit_loop_fabric_verilog_dp(name: &str, lf: &LoopFabric, mul_bits: Option<u32>) -> String {
9614    let name_in = |k: usize| -> String {
9615        if k < lf.n_state {
9616            format!("s{k}")
9617        } else if k == lf.n_state {
9618            "iv".to_string()
9619        } else {
9620            format!("inv{}", k - lf.n_state - 1)
9621        }
9622    };
9623    let name_val = |k: usize| format!("b{k}");
9624    // Circuit-input renderer for init/lo/hi/invariants (In(k) = in{k} param).
9625    let circ = |s: &Src| src_named(s, &|k| format!("in{k}"), &|k| format!("v{k}"));
9626
9627    // A SEQUENCE loop streams an operand vector from an on-chip buffer: the iterated `Seq` input becomes a
9628    // memory (a write port for loading it over UART/from a ROM) rather than a scalar wire, the induction range
9629    // is 0..=seq_len-1, and each `MemRead{addr}` reads `mem[addr]`. This is the operand-fed matmul datapath.
9630    let seq_k = lf.seq_input;
9631    const MEM_DEPTH: usize = 512;
9632    const MEM_ADDRW: usize = 9; // clog2(MEM_DEPTH)
9633    let inputs = (0..lf.n_in)
9634        .filter(|k| Some(*k) != seq_k)
9635        .map(|k| format!("input [63:0] in{k}"))
9636        .collect::<Vec<_>>()
9637        .join(", ");
9638    let inputs = if inputs.is_empty() {
9639        String::new()
9640    } else {
9641        format!("{inputs}, ")
9642    };
9643    let (mem_ports, mem_decl, lo_str, hi_str) = if seq_k.is_some() {
9644        (
9645            format!(
9646                "input [63:0] seq_len, input mem_we, input [{a}:0] mem_waddr, input signed [63:0] mem_wdata, ",
9647                a = MEM_ADDRW - 1
9648            ),
9649            format!(
9650                "  reg signed [63:0] mem [0:{d}];\n  \
9651                 always @(posedge clk) if (mem_we) mem[mem_waddr] <= mem_wdata;\n",
9652                d = MEM_DEPTH - 1
9653            ),
9654            "64'sd0".to_string(),
9655            "$signed(seq_len) - 64'sd1".to_string(),
9656        )
9657    } else {
9658        (String::new(), String::new(), circ(&lf.lo), circ(&lf.hi))
9659    };
9660
9661    let mut invs = String::new();
9662    for (j, s) in lf.invariants.iter().enumerate() {
9663        invs.push_str(&format!("  wire [63:0] inv{j} = {};\n", circ(s)));
9664    }
9665    let mut bodyw = String::new();
9666    for (i, cell) in lf.body.iter().enumerate() {
9667        bodyw.push_str(&loop_body_wire(i, cell, mul_bits, &name_in, &name_val, MEM_ADDRW));
9668    }
9669    let mut nexts = String::new();
9670    for (j, s) in lf.next.iter().enumerate() {
9671        nexts.push_str(&format!(
9672            "  wire [63:0] ns{j} = {};\n",
9673            src_named(s, &name_in, &name_val)
9674        ));
9675    }
9676    let mut reset = String::new();
9677    let mut step = String::new();
9678    for (j, s) in lf.init.iter().enumerate() {
9679        reset.push_str(&format!("      s{j} <= {};\n", circ(s)));
9680        step.push_str(&format!("      s{j} <= ns{j};\n"));
9681    }
9682    let state_decl = (0..lf.n_state)
9683        .map(|j| format!("s{j}"))
9684        .collect::<Vec<_>>()
9685        .join(", ");
9686
9687    format!(
9688        "// cgra_{name} — a compiled COUNTED LOOP as a CGRA: a fixed spatial body datapath\n\
9689         // (wired SSA, no fetch/dispatch) streamed by a sequential controller once per induction\n\
9690         // value in lo..=hi. Futamura P1 for control flow: the body IS the circuit; the loop is a\n\
9691         // counter. 64-bit, so it wraps identically to the VM's Int; the body's one-step next-state\n\
9692         // is certified == the VM's per-iteration transition by our own SAT (the induction step).\n\
9693         module cgra_{name}(input clk, input rst, {inputs}{mem_ports}output [63:0] result, output done);\n\
9694         \x20 reg [63:0] {state_decl};\n\
9695         \x20 reg [63:0] iv;\n\
9696         \x20 reg running;\n\
9697         {mem_decl}\
9698         \x20 wire [63:0] lo = {lo};\n\
9699         \x20 wire [63:0] hi = {hi};\n\
9700         {invs}{bodyw}{nexts}\
9701         \x20 always @(posedge clk) begin\n\
9702         \x20   if (rst) begin\n\
9703         {reset}\
9704         \x20     iv <= lo;\n\
9705         \x20     running <= ($signed(lo) <= $signed(hi));\n\
9706         \x20   end else if (running) begin\n\
9707         {step}\
9708         \x20     if ($signed(iv) < $signed(hi)) iv <= iv + 64'sd1;\n\
9709         \x20     else running <= 1'b0;\n\
9710         \x20   end\n\
9711         \x20 end\n\
9712         \x20 assign done = !running;\n\
9713         \x20 assign result = s{result_state};\n\
9714         endmodule\n",
9715        lo = lo_str,
9716        hi = hi_str,
9717        result_state = lf.result_state,
9718    )
9719}
9720
9721/// The terms one accumulator adds each iteration: `next[j] = s_j + Σ ±terms`, through Add/Sub tiles each read
9722/// once. `None` when the state's next value is not such a sum, or reads the state more than once.
9723fn accumulation_terms(lf: &LoopFabric, j: usize, uses: &[usize]) -> Option<(Vec<(Src, bool)>, Vec<usize>)> {
9724    fn walk(
9725        lf: &LoopFabric,
9726        s: Src,
9727        negated: bool,
9728        uses: &[usize],
9729        terms: &mut Vec<(Src, bool)>,
9730        absorbed: &mut Vec<usize>,
9731    ) {
9732        if let Src::Val(k) = s {
9733            if let LoopCell::Arith { op: op @ (AluOp::Add | AluOp::Sub), a, b } = lf.body[k] {
9734                if uses[k] == 1 {
9735                    absorbed.push(k);
9736                    walk(lf, a, negated, uses, terms, absorbed);
9737                    walk(lf, b, negated != (op == AluOp::Sub), uses, terms, absorbed);
9738                    return;
9739                }
9740            }
9741        }
9742        terms.push((s, negated));
9743    }
9744    let mut terms = Vec::new();
9745    let mut absorbed = Vec::new();
9746    walk(lf, lf.next[j], false, uses, &mut terms, &mut absorbed);
9747    let own: Vec<usize> = (0..terms.len()).filter(|&t| terms[t].0 == Src::In(j)).collect();
9748    if own.len() != 1 || terms[own[0]].1 {
9749        return None;
9750    }
9751    terms.remove(own[0]);
9752    Some((terms, absorbed))
9753}
9754
9755/// A counted loop emitted in the pipelined accumulator form.
9756#[derive(Clone, Debug)]
9757pub struct PipelinedLoop {
9758    pub verilog: String,
9759    /// The register stages the terms take from the counter to the accumulators: a loop whose body runs finishes this
9760    /// many clocks after the counter does.
9761    pub term_stages: usize,
9762}
9763
9764/// The logic depth of one body tile in the pipelined loop's stage budget, in units of a 64-bit add.
9765pub fn loop_cell_delay(cell: &LoopCell, mul_bits: Option<u32>) -> f64 {
9766    match cell {
9767        LoopCell::Arith { op: AluOp::Mul, .. } => if mul_bits.is_some() { 2.0 } else { 4.0 },
9768        LoopCell::Arith { op: AluOp::Add | AluOp::Sub, .. } => 1.0,
9769        LoopCell::Arith { op: AluOp::Shl | AluOp::Shr | AluOp::UShr, b: Src::Const(_), .. } => 0.0,
9770        LoopCell::Arith { op: AluOp::Shl | AluOp::Shr | AluOp::UShr, .. } => 1.0,
9771        LoopCell::Arith { op: AluOp::And | AluOp::Or | AluOp::Xor, .. } => 0.3,
9772        _ => 1.0,
9773    }
9774}
9775
9776/// Emit the counted-loop CGRA with every accumulator in carry-save form and its terms pipelined ahead of it.
9777///
9778/// A loop whose state registers each take `s_j ← s_j + Σ ±terms`, with no term reading any state, carries only
9779/// two recurrences: the induction counter and the accumulators. The terms (operand arithmetic and the multiplies)
9780/// compute from the counter and the invariants through as many register stages as `term_budget` (in the units of
9781/// [`loop_cell_delay`]) asks, each tile in the earliest stage its operands and the budget allow; the counter is
9782/// delayed to every stage that reads it, and a valid flag travels with each iteration. On the edge after the last
9783/// term stage each accumulator's sum and carry words absorb the terms through a tree of 3:2 compressors, so the
9784/// recurrence holds no carry chain. The counter increments through a parallel-prefix AND. The result is the sum of
9785/// the result register's two words, a loop whose body runs finishes [`PipelinedLoop::term_stages`] clocks after the
9786/// counter does, and every other behaviour is [`emit_loop_fabric_verilog_int8`]'s (with `mul_bits`) or
9787/// [`emit_loop_fabric_verilog`]'s.
9788///
9789/// Refused, with the reason, for a loop with a sequence input, a heap island, an epilogue, a functional-unit tile,
9790/// or a state that is not such an accumulator.
9791pub fn emit_loop_fabric_verilog_pipelined(
9792    name: &str,
9793    lf: &LoopFabric,
9794    mul_bits: Option<u32>,
9795    term_budget: f64,
9796) -> Result<PipelinedLoop, String> {
9797    assert!(term_budget > 0.0, "a term stage budget is positive");
9798    if lf.seq_input.is_some() || lf.seq_heap_ptr.is_some() {
9799        return Err("a sequence loop streams its operands from memory; the pipelined form takes counted loops".into());
9800    }
9801    if !lf.prologue_heap.is_empty() || !lf.epilogue.is_empty() {
9802        return Err("a loop with a heap prologue or an epilogue island is not a bare accumulation".into());
9803    }
9804    if let Some(w) = mul_bits {
9805        assert!((2..=32).contains(&w), "int8 datapath mul_bits must be 2..=32 (got {w})");
9806    }
9807    for (i, c) in lf.body.iter().enumerate() {
9808        if !matches!(c, LoopCell::Arith { .. } | LoopCell::Select { .. } | LoopCell::VectorArith { .. }) {
9809            return Err(format!("body tile b{i} ({c:?}) is not a combinational tile"));
9810        }
9811    }
9812    let ns = lf.n_state;
9813    let mut uses = vec![0usize; lf.body.len()];
9814    let srcs = |c: &LoopCell| -> Vec<Src> {
9815        match *c {
9816            LoopCell::Arith { a, b, .. } | LoopCell::VectorArith { a, b, .. } => vec![a, b],
9817            LoopCell::Select { ca, cb, t, e, .. } => vec![ca, cb, t, e],
9818            _ => Vec::new(),
9819        }
9820    };
9821    for c in &lf.body {
9822        for s in srcs(c) {
9823            if let Src::Val(k) = s {
9824                uses[k] += 1;
9825            }
9826        }
9827    }
9828    for s in &lf.next {
9829        if let Src::Val(k) = s {
9830            uses[*k] += 1;
9831        }
9832    }
9833    let mut accumulations = Vec::with_capacity(ns);
9834    let mut absorbed = vec![false; lf.body.len()];
9835    for j in 0..ns {
9836        let (terms, cells) = accumulation_terms(lf, j, &uses)
9837            .ok_or_else(|| format!("state s{j} is not an accumulator s{j} + Σ terms"))?;
9838        for k in cells {
9839            absorbed[k] = true;
9840        }
9841        accumulations.push(terms);
9842    }
9843    // No term may depend on any state: the terms compute a stage ahead of the accumulators that read them.
9844    let mut reads_state = vec![false; lf.body.len()];
9845    for (i, c) in lf.body.iter().enumerate() {
9846        if absorbed[i] {
9847            continue;
9848        }
9849        reads_state[i] = srcs(c).iter().any(|s| match s {
9850            Src::In(k) => *k < ns,
9851            Src::Val(k) => reads_state[*k],
9852            Src::Const(_) => false,
9853        });
9854    }
9855    for (j, terms) in accumulations.iter().enumerate() {
9856        for (t, _) in terms {
9857            let dep = match t {
9858                Src::In(k) => *k < ns,
9859                Src::Val(k) => absorbed[*k] || reads_state[*k],
9860                Src::Const(_) => false,
9861            };
9862            if dep {
9863                return Err(format!("a term of s{j} reads a state register; it cannot run a stage ahead"));
9864            }
9865        }
9866    }
9867
9868    // Each term tile takes the earliest stage its operands and the budget allow: the counter arrives at stage 0, an
9869    // invariant is stable in every stage, a value read in a later stage than its own arrives from a register.
9870    let mut stage = vec![0usize; lf.body.len()];
9871    let mut arrival = vec![0.0f64; lf.body.len()];
9872    for (i, c) in lf.body.iter().enumerate() {
9873        if absorbed[i] {
9874            continue;
9875        }
9876        let ops = srcs(c);
9877        let latest = ops
9878            .iter()
9879            .filter_map(|s| match s {
9880                Src::Val(k) if !absorbed[*k] => Some(stage[*k]),
9881                _ => None,
9882            })
9883            .max()
9884            .unwrap_or(0);
9885        let start = ops
9886            .iter()
9887            .filter_map(|s| match s {
9888                Src::Val(k) if !absorbed[*k] && stage[*k] == latest => Some(arrival[*k]),
9889                _ => None,
9890            })
9891            .fold(0.0, f64::max);
9892        let d = loop_cell_delay(c, mul_bits);
9893        if start > 0.0 && start + d > term_budget {
9894            stage[i] = latest + 1;
9895            arrival[i] = d;
9896        } else {
9897            stage[i] = latest;
9898            arrival[i] = start + d;
9899        }
9900    }
9901    let last = accumulations
9902        .iter()
9903        .flatten()
9904        .filter_map(|(t, _)| match t {
9905            Src::Val(k) => Some(stage[*k]),
9906            _ => None,
9907        })
9908        .max()
9909        .unwrap_or(0);
9910    let term_stages = last + 1;
9911
9912    // How far past its own stage each value and the counter are read: the delay registers they need.
9913    let mut reach = vec![0usize; lf.body.len()];
9914    let mut iv_reach = 0usize;
9915    let note = |s: Src, at: usize, reach: &mut Vec<usize>, iv_reach: &mut usize| match s {
9916        Src::Val(k) => reach[k] = reach[k].max(at - stage[k]),
9917        Src::In(k) if k == ns => *iv_reach = (*iv_reach).max(at),
9918        _ => {}
9919    };
9920    for (i, c) in lf.body.iter().enumerate() {
9921        if !absorbed[i] {
9922            for s in srcs(c) {
9923                note(s, stage[i], &mut reach, &mut iv_reach);
9924            }
9925        }
9926    }
9927    for (t, _) in accumulations.iter().flatten() {
9928        note(*t, last, &mut reach, &mut iv_reach);
9929    }
9930    let delayed = |base: String, d: usize| if d == 0 { base } else { format!("{base}_d{d}") };
9931
9932    let circ = |s: &Src| src_named(s, &|k| format!("in{k}"), &|k| format!("v{k}"));
9933    let inputs = (0..lf.n_in)
9934        .map(|k| format!("input [63:0] in{k}, "))
9935        .collect::<String>();
9936    let mut invs = String::new();
9937    for (j, s) in lf.invariants.iter().enumerate() {
9938        invs.push_str(&format!("  wire [63:0] inv{j} = {};\n", circ(s)));
9939    }
9940    let mut bodyw = String::new();
9941    for (i, cell) in lf.body.iter().enumerate() {
9942        if absorbed[i] {
9943            continue;
9944        }
9945        let at = stage[i];
9946        let name_in = |k: usize| -> String {
9947            if k < ns {
9948                format!("s{k}")
9949            } else if k == ns {
9950                delayed("iv".into(), at)
9951            } else {
9952                format!("inv{}", k - ns - 1)
9953            }
9954        };
9955        let name_val = |k: usize| delayed(format!("b{k}"), at - stage[k]);
9956        bodyw.push_str(&loop_body_wire(i, cell, mul_bits, &name_in, &name_val, 9));
9957    }
9958    let mut pipe_regs = String::new();
9959    let mut pipe_shift = String::new();
9960    for d in 1..=iv_reach {
9961        pipe_regs.push_str(&format!("  reg [63:0] iv_d{d};\n"));
9962        pipe_shift.push_str(&format!("      iv_d{d} <= {};\n", delayed("iv".into(), d - 1)));
9963    }
9964    for (k, &r) in reach.iter().enumerate() {
9965        for d in 1..=r {
9966            pipe_regs.push_str(&format!("  reg [63:0] b{k}_d{d};\n"));
9967            pipe_shift.push_str(&format!("      b{k}_d{d} <= {};\n", delayed(format!("b{k}"), d - 1)));
9968        }
9969    }
9970    let name_in = |k: usize| -> String {
9971        if k < ns {
9972            format!("s{k}")
9973        } else if k == ns {
9974            delayed("iv".into(), last)
9975        } else {
9976            format!("inv{}", k - ns - 1)
9977        }
9978    };
9979    let name_val = |k: usize| delayed(format!("b{k}"), last - stage[k]);
9980
9981    // Each term registers into t{j}_{n}; a subtracted one enters its tree inverted, its +1 folded into the
9982    // constant word.
9983    let mut regs = String::new();
9984    let mut latch = String::new();
9985    let mut trees = String::new();
9986    let mut accumulate = String::new();
9987    let mut reset = String::new();
9988    for (j, terms) in accumulations.iter().enumerate() {
9989        regs.push_str(&format!("  reg [63:0] s{j}_sum, s{j}_carry;\n"));
9990        reset.push_str(&format!("      s{j}_sum <= {}; s{j}_carry <= 64'd0;\n", circ(&lf.init[j])));
9991        let mut words = vec![format!("s{j}_sum"), format!("s{j}_carry")];
9992        let mut constant: i64 = 0;
9993        for (n, (t, negated)) in terms.iter().enumerate() {
9994            match t {
9995                Src::Const(c) => {
9996                    constant = if *negated { constant.wrapping_sub(*c) } else { constant.wrapping_add(*c) };
9997                }
9998                _ => {
9999                    regs.push_str(&format!("  reg [63:0] t{j}_{n};\n"));
10000                    latch.push_str(&format!("      t{j}_{n} <= {};\n", src_named(t, &name_in, &name_val)));
10001                    if *negated {
10002                        words.push(format!("~t{j}_{n}"));
10003                        constant = constant.wrapping_add(1);
10004                    } else {
10005                        words.push(format!("t{j}_{n}"));
10006                    }
10007                }
10008            }
10009        }
10010        if constant != 0 {
10011            words.push(format!("64'h{:016x}", constant as u64));
10012        }
10013        let mut level = 0usize;
10014        while words.len() > 2 {
10015            let mut next = Vec::with_capacity(words.len());
10016            for (g, t) in words.chunks(3).enumerate() {
10017                if t.len() < 3 {
10018                    next.extend(t.iter().cloned());
10019                    continue;
10020                }
10021                let tag = format!("s{j}_l{level}_{g}");
10022                trees.push_str(&format!(
10023                    "  wire [63:0] {tag}_x = {a} ^ {b};\n  wire [63:0] {tag}_s = {tag}_x ^ {c};\n  \
10024                     wire [63:0] {tag}_c = (({a} & {b}) | ({c} & {tag}_x)) << 1;\n",
10025                    a = t[0],
10026                    b = t[1],
10027                    c = t[2],
10028                ));
10029                next.push(format!("{tag}_s"));
10030                next.push(format!("{tag}_c"));
10031            }
10032            words = next;
10033            level += 1;
10034        }
10035        trees.push_str(&format!(
10036            "  wire [63:0] s{j}_sum_next = {};\n  wire [63:0] s{j}_carry_next = {};\n",
10037            words[0],
10038            words.get(1).cloned().unwrap_or_else(|| "64'd0".into())
10039        ));
10040        accumulate.push_str(&format!("        s{j}_sum <= s{j}_sum_next; s{j}_carry <= s{j}_carry_next;\n"));
10041    }
10042    let mut prefix = String::from("  wire [63:0] ivp0 = iv;\n");
10043    let mut level = 0;
10044    let mut span = 1;
10045    while span < 64 {
10046        prefix.push_str(&format!(
10047            "  wire [63:0] ivp{n} = ivp{level} & {{ivp{level}[{hi}:0], {span}'b{ones}}};\n",
10048            n = level + 1,
10049            hi = 63 - span,
10050            ones = "1".repeat(span),
10051        ));
10052        level += 1;
10053        span *= 2;
10054    }
10055    prefix.push_str(&format!("  wire [63:0] iv_next = iv ^ {{ivp{level}[62:0], 1'b1}};\n"));
10056    let state_reads = (0..ns)
10057        .map(|j| format!("  wire [63:0] s{j} = s{j}_sum + s{j}_carry;\n"))
10058        .collect::<String>();
10059    Ok(format!(
10060        "// cgra_{name} — a compiled COUNTED LOOP as a CGRA, pipelined: the body's terms compute from the induction\n\
10061         // counter and the invariants through {term_stages} register stage(s); each accumulator holds sum and carry words\n\
10062         // that absorb them through 3:2 compressors, so neither recurrence carries a carry chain. 64-bit, so it wraps\n\
10063         // identically to the VM's Int; a loop whose body runs finishes {term_stages} clock(s) after the counter does.\n\
10064         module cgra_{name}(input clk, input rst, {inputs}output [63:0] result, output done);\n\
10065         {regs}\
10066         \x20 reg [63:0] iv;\n\
10067         \x20 reg running;\n\
10068         \x20 reg [{live_top}:0] live;\n\
10069         {pipe_regs}\
10070         \x20 wire [63:0] lo = {lo};\n\
10071         \x20 wire [63:0] hi = {hi};\n\
10072         {invs}{bodyw}{trees}{prefix}{state_reads}\
10073         \x20 always @(posedge clk) begin\n\
10074         \x20   if (rst) begin\n\
10075         {reset}\
10076         \x20     iv <= lo;\n\
10077         \x20     running <= ($signed(lo) <= $signed(hi));\n\
10078         \x20     live <= {{{live_width}{{1'b0}}}};\n\
10079         \x20   end else begin\n\
10080         \x20     live <= {live_next};\n\
10081         {pipe_shift}{latch}\
10082         \x20     if (live[{live_top}]) begin\n\
10083         {accumulate}\
10084         \x20     end\n\
10085         \x20     if (running) begin\n\
10086         \x20       if ($signed(iv) < $signed(hi)) iv <= iv_next;\n\
10087         \x20       else running <= 1'b0;\n\
10088         \x20     end\n\
10089         \x20   end\n\
10090         \x20 end\n\
10091         \x20 assign done = !running && !(|live);\n\
10092         \x20 assign result = s{result_state};\n\
10093         endmodule\n",
10094        lo = circ(&lf.lo),
10095        hi = circ(&lf.hi),
10096        result_state = lf.result_state,
10097        live_top = term_stages - 1,
10098        live_width = term_stages,
10099        live_next = if term_stages == 1 {
10100            "running".to_string()
10101        } else {
10102            format!("{{live[{}:0], running}}", term_stages - 2)
10103        },
10104    ))
10105    .map(|verilog| PipelinedLoop { verilog, term_stages })
10106}
10107
10108/// Lower ONE loop-carried state register's next-state (`next[which]`) to a `BoundedExpr` over the
10109/// body inputs `s0..`, `iv`, `inv0..` at the given width — so our certified SAT can prove the
10110/// induction STEP (the body transition ≡ the VM's per-iteration semantics) exhaustively.
10111pub fn loop_next_to_bounded(
10112    lf: &LoopFabric,
10113    which: usize,
10114    width: u32,
10115) -> crate::codegen_sva::sva_to_verify::BoundedExpr {
10116    use crate::codegen_sva::sva_to_verify::{BitVecBoundedOp, BoundedExpr};
10117    let name_in = |k: usize| -> String {
10118        if k < lf.n_state {
10119            format!("s{k}")
10120        } else if k == lf.n_state {
10121            "iv".to_string()
10122        } else {
10123            format!("inv{}", k - lf.n_state - 1)
10124        }
10125    };
10126    fn src_expr(
10127        s: &Src,
10128        vals: &[crate::codegen_sva::sva_to_verify::BoundedExpr],
10129        width: u32,
10130        name_in: &dyn Fn(usize) -> String,
10131    ) -> crate::codegen_sva::sva_to_verify::BoundedExpr {
10132        use crate::codegen_sva::sva_to_verify::BoundedExpr;
10133        match s {
10134            Src::In(k) => BoundedExpr::BitVecVar(name_in(*k), width),
10135            Src::Val(k) => vals[*k].clone(),
10136            Src::Const(c) => BoundedExpr::BitVecConst {
10137                width,
10138                value: *c as u64,
10139            },
10140        }
10141    }
10142    let slt = |l: BoundedExpr, r: BoundedExpr| BoundedExpr::BitVecBinary {
10143        op: BitVecBoundedOp::SLt,
10144        left: Box::new(l),
10145        right: Box::new(r),
10146    };
10147    let eq = |l: BoundedExpr, r: BoundedExpr| BoundedExpr::BitVecBinary {
10148        op: BitVecBoundedOp::Eq,
10149        left: Box::new(l),
10150        right: Box::new(r),
10151    };
10152    let mut vals: Vec<BoundedExpr> = Vec::with_capacity(lf.body.len());
10153    for cell in &lf.body {
10154        let expr = match cell {
10155            LoopCell::Arith { op, a, b } => {
10156                let a = src_expr(a, &vals, width, &name_in);
10157                let b = src_expr(b, &vals, width, &name_in);
10158                let bop = match op {
10159                    AluOp::Add => BitVecBoundedOp::Add,
10160                    AluOp::Sub => BitVecBoundedOp::Sub,
10161                    AluOp::Mul => BitVecBoundedOp::Mul,
10162                    AluOp::And => BitVecBoundedOp::And,
10163                    AluOp::Or => BitVecBoundedOp::Or,
10164                    AluOp::Xor => BitVecBoundedOp::Xor,
10165                    AluOp::Shl => BitVecBoundedOp::Shl,
10166                    AluOp::Shr => BitVecBoundedOp::AShr,
10167                    AluOp::UShr => BitVecBoundedOp::Shr,
10168                };
10169                BoundedExpr::BitVecBinary { op: bop, left: Box::new(a), right: Box::new(b) }
10170            }
10171            LoopCell::VectorArith { .. } => panic!(
10172                "a packed-SIMD (Simd) tile in a loop SAT bound is not SAT-lowered here — a Simd-bearing fabric \
10173                 is proven by eval ≡ VM + iverilog (simd_tile_lock + simd_rtl_lock), like a Func tile"
10174            ),
10175            LoopCell::Func { .. } => panic!(
10176                "division in a counted-loop body is not yet SAT-lowered — the shared_divider's \
10177                 own certified proof (rtl.rs::the_signed_divider_is_proven_correct_by_our_sat) covers \
10178                 the arithmetic; loop-carried division SAT is future work"
10179            ),
10180            LoopCell::MemRead { .. } | LoopCell::HeapRead { .. } | LoopCell::HeapWrite { .. } | LoopCell::HeapAlloc { .. } => panic!(
10181                "MemRead in a loop-carried SAT bound is future work (the memory-loop induction proof)"
10182            ),
10183            LoopCell::Select { cmp, ca, cb, t, e } => {
10184                let ca = src_expr(ca, &vals, width, &name_in);
10185                let cb = src_expr(cb, &vals, width, &name_in);
10186                let t = src_expr(t, &vals, width, &name_in);
10187                let e = src_expr(e, &vals, width, &name_in);
10188                // Normalize the SIGNED comparison to `{SLt, Eq}` with a possible branch-swap
10189                // (`ite(¬p, t, e) = ite(p, e, t)`), so the predicate needs no `Not`.
10190                let (cond, tt, ee) = match cmp {
10191                    CmpKind::Lt => (slt(ca, cb), t, e),
10192                    CmpKind::Gt => (slt(cb, ca), t, e),
10193                    CmpKind::Ge => (slt(ca, cb), e, t),
10194                    CmpKind::Le => (slt(cb, ca), e, t),
10195                    CmpKind::Eq => (eq(ca, cb), t, e),
10196                    CmpKind::Ne => (eq(ca, cb), e, t),
10197                };
10198                BoundedExpr::BitVecIte {
10199                    cond: Box::new(cond),
10200                    then: Box::new(tt),
10201                    else_: Box::new(ee),
10202                }
10203            }
10204        };
10205        vals.push(expr);
10206    }
10207    src_expr(&lf.next[which], &vals, width, &name_in)
10208}
10209
10210// ===========================================================================================
10211// WHILE-loop CGRA — the DATA-DEPENDENT controller: the loop's continue condition is computed by
10212// the FABRIC (a predicate over the loop-carried state), not a counter. The trip count is decided
10213// by the datapath itself. Sound by k-induction: body-step ≡ VM iteration AND the fabric predicate
10214// ≡ the VM's loop condition (both certified by our SAT) ⇒ the fabric runs exactly the VM's
10215// iterations. Reuses the shared if-conversion (`if_convert_body`) so a `while` body may branch.
10216// ===========================================================================================
10217
10218/// A `while`-loop residual lowered to a CGRA: a fixed body datapath streamed by a controller whose
10219/// CONTINUE predicate is fabric-computed over the loop-carried state (no counter).
10220#[derive(Clone, Debug)]
10221pub struct WhileFabric {
10222    pub n_in: usize,
10223    pub n_state: usize,
10224    pub init: Vec<Src>,
10225    pub invariants: Vec<Src>,
10226    /// Body cells. `Src::In(k)`: `0..n_state` = state, `n_state+j` = invariant j (NO induction var).
10227    pub body: Vec<LoopCell>,
10228    pub next: Vec<Src>,
10229    /// The continue predicate `(ca cmp cb)` — the loop runs WHILE it holds. Over body inputs.
10230    pub pred: (CmpKind, Src, Src),
10231    pub result_state: usize,
10232}
10233
10234/// Recover the `while` loop in `fn_name` and lower it to a [`WhileFabric`]. The header (the
10235/// back-edge target) computes the continue predicate; the body (to the back-edge) if-converts.
10236pub fn while_fabric_from_source(src: &str, fn_name: &str) -> WhileFabric {
10237    use crate::vm::{Compiler, Constant, Op};
10238    use std::collections::{BTreeSet, HashMap};
10239
10240    crate::ui_bridge::with_parsed_program(src, |parsed, interner| {
10241        let (stmts, _t, _p) = parsed.expect("parse failed");
10242        let program = Compiler::compile(stmts, interner).expect("compile failed");
10243        let f = program
10244            .functions
10245            .iter()
10246            .find(|f| interner.resolve(f.name) == fn_name)
10247            .unwrap_or_else(|| panic!("function `{fn_name}` not found"));
10248
10249        // FLOAT LANE (mirrors the loop fabric): set up the shared thread-locals + seed the float parameters,
10250        // so the body's `exec_ops` routes float arithmetic to `FuncUnit::F*` tiles. The prologue below then
10251        // admits float CONSTANTS and carries float-ness through `Move`.
10252        setup_program_thread_locals(&program);
10253        for (p, pt) in f.param_types.iter().enumerate() {
10254            if matches!(pt, Some(crate::vm::BoundaryType::Float)) {
10255                FLOAT_REGS.with(|t| {
10256                    t.borrow_mut().insert(p as u16);
10257                });
10258            }
10259        }
10260
10261        let mut ops: Vec<(usize, Op)> = Vec::new();
10262        for (i, op) in program.code.iter().enumerate().skip(f.entry_pc) {
10263            ops.push((i, *op));
10264            if matches!(op, Op::Return { .. } | Op::ReturnNothing) {
10265                break;
10266            }
10267        }
10268        // Back-edge = a Jump targeting an EARLIER pc; its target is the loop header.
10269        let (backedge_idx, header_pc) = ops
10270            .iter()
10271            .enumerate()
10272            .find_map(|(i, (pc, op))| match op {
10273                Op::Jump { target } if *target < *pc => Some((i, *target)),
10274                _ => None,
10275            })
10276            .expect("while fabric: no back-edge (not a while loop)");
10277
10278        let mut pro: HashMap<u16, Src> = HashMap::new();
10279        for p in 0..f.param_count {
10280            pro.insert(p, Src::In(p as usize));
10281        }
10282        let getp = |pro: &HashMap<u16, Src>, r: u16| -> Src {
10283            *pro.get(&r)
10284                .unwrap_or_else(|| panic!("while prologue reads unwritten R{r}"))
10285        };
10286        // Prologue: everything before the header initialises the loop-carried registers.
10287        let mut header_start = 0;
10288        for (i, (pc, op)) in ops.iter().enumerate() {
10289            if *pc == header_pc {
10290                header_start = i;
10291                break;
10292            }
10293            match op {
10294                Op::LoadConst { dst, idx: ci } => match program.constants[*ci as usize] {
10295                    Constant::Int(n) => {
10296                        pro.insert(*dst, Src::Const(n));
10297                    }
10298                    Constant::Float(fv) => {
10299                        pro.insert(*dst, Src::Const(fv.to_bits() as i64));
10300                        FLOAT_REGS.with(|t| {
10301                            t.borrow_mut().insert(*dst);
10302                        });
10303                    }
10304                    ref c => panic!(
10305                        "while prologue: non-integer constant {c:?} (only Int / Float supported)"
10306                    ),
10307                },
10308                Op::Move { dst, src } => {
10309                    let s = getp(&pro, *src);
10310                    if FLOAT_REGS.with(|t| t.borrow().contains(src)) {
10311                        FLOAT_REGS.with(|t| {
10312                            t.borrow_mut().insert(*dst);
10313                        });
10314                    }
10315                    pro.insert(*dst, s);
10316                }
10317                other => panic!("while prologue: `{}` unsupported", other.name()),
10318            }
10319        }
10320        // Header [header_start .. jif) computes the predicate; body (jif .. back-edge) is the loop.
10321        let jif_idx = ops[header_start..backedge_idx]
10322            .iter()
10323            .position(|(_, o)| matches!(o, Op::JumpIfFalse { .. }))
10324            .map(|x| x + header_start)
10325            .expect("while fabric: no JumpIfFalse header test");
10326        let (cond_reg, exit_target) = match ops[jif_idx].1 {
10327            Op::JumpIfFalse { cond, target } => (cond, target),
10328            _ => unreachable!(),
10329        };
10330        let header_ops: Vec<Op> = ops[header_start..jif_idx].iter().map(|(_, o)| *o).collect();
10331        let body_ops: Vec<(usize, Op)> = ops[jif_idx + 1..backedge_idx].to_vec();
10332        let result_reg = ops[backedge_idx + 1..]
10333            .iter()
10334            .find_map(|(pc, o)| match o {
10335                Op::Return { src } if *pc == exit_target => Some(*src),
10336                _ => None,
10337            })
10338            .or_else(|| {
10339                ops.iter().find_map(|(_, o)| match o {
10340                    Op::Return { src } => Some(*src),
10341                    _ => None,
10342                })
10343            })
10344            .expect("while fabric: no Return");
10345
10346        let reg_of = |op: &Op| -> (Option<u16>, Vec<u16>) {
10347            match op {
10348                Op::LoadConst { dst, .. } => (Some(*dst), vec![]),
10349                // `now` / `today` — a clock value-load (reads the injected fixed clock); no register read.
10350                Op::LoadNow { dst } | Op::LoadToday { dst } => (Some(*dst), vec![]),
10351                Op::Move { dst, src } => (Some(*dst), vec![*src]),
10352                // Logical `not` → a `(src == 0) ? 1 : 0` Select mux (one read, like `Move`).
10353                Op::Not { dst, src } => (Some(*dst), vec![*src]),
10354                // A numeric-coprocessor builtin (sqrt/floor/abs/min/max) reads its `arg_count` argument
10355                // registers starting at `args_start`; `exec_ops` lowers it to the matching Func/Select tile.
10356                Op::CallBuiltin { dst, args_start, arg_count, .. } => {
10357                    (Some(*dst), (0..*arg_count as u16).map(|k| *args_start + k).collect())
10358                }
10359                Op::Add { dst, lhs, rhs }
10360                | Op::Sub { dst, lhs, rhs }
10361                | Op::Mul { dst, lhs, rhs }
10362                // Division / mod / floor-div → a functional-unit tile (int `DivS`/`ModS`/`FloorDivS`, or
10363                // `FDivF` when an operand is a float), so a while body's `s / 2.0` is a real FDivF tile.
10364                | Op::Div { dst, lhs, rhs }
10365                | Op::Mod { dst, lhs, rhs }
10366                | Op::FloorDiv { dst, lhs, rhs }
10367                // Bitwise / shifts → combinational Arith tiles (integer): `a & b`, `a | b`, `a xor b`,
10368                // `a << b`, `a >> b`. `exec_ops` already emits the matching `AluOp`.
10369                | Op::BitAnd { dst, lhs, rhs }
10370                | Op::BitOr { dst, lhs, rhs }
10371                | Op::BitXor { dst, lhs, rhs }
10372                | Op::Shl { dst, lhs, rhs }
10373                | Op::Shr { dst, lhs, rhs }
10374                // Power (`**` / `to the power of`) → the sequential-multiply `Pow` functional-unit tile.
10375                | Op::Pow { dst, lhs, rhs }
10376                | Op::Lt { dst, lhs, rhs }
10377                | Op::Gt { dst, lhs, rhs }
10378                | Op::LtEq { dst, lhs, rhs }
10379                | Op::GtEq { dst, lhs, rhs }
10380                | Op::Eq { dst, lhs, rhs }
10381                | Op::NotEq { dst, lhs, rhs } => (Some(*dst), vec![*lhs, *rhs]),
10382                Op::AddAssign { dst, src } => (Some(*dst), vec![*dst, *src]),
10383                Op::Jump { .. } | Op::JumpIfFalse { .. } => (None, vec![]),
10384                other => panic!("while loop: body op `{}` unsupported", other.name()),
10385            }
10386        };
10387        let mut written: BTreeSet<u16> = BTreeSet::new();
10388        let mut upward: BTreeSet<u16> = BTreeSet::new();
10389        for op in header_ops.iter().chain(body_ops.iter().map(|(_, o)| o)) {
10390            let (dst, reads) = reg_of(op);
10391            for r in reads {
10392                if !written.contains(&r) {
10393                    upward.insert(r);
10394                }
10395            }
10396            if let Some(d) = dst {
10397                written.insert(d);
10398            }
10399        }
10400        let mut carried: Vec<u16> = upward.intersection(&written).copied().collect();
10401        if written.contains(&result_reg) && !carried.contains(&result_reg) {
10402            carried.push(result_reg);
10403        }
10404        carried.sort_unstable();
10405        let invariants_regs: Vec<u16> = upward.difference(&written).copied().collect();
10406
10407        let n_state = carried.len();
10408        let state_slot: HashMap<u16, usize> =
10409            carried.iter().enumerate().map(|(i, r)| (*r, i)).collect();
10410        let inv_slot: HashMap<u16, usize> = invariants_regs
10411            .iter()
10412            .enumerate()
10413            .map(|(i, r)| (*r, i))
10414            .collect();
10415
10416        // Body inputs: 0..n_state = state, n_state+j = invariant j (NO induction variable).
10417        let mut reg: HashMap<u16, Src> = HashMap::new();
10418        for (r, s) in &state_slot {
10419            reg.insert(*r, Src::In(*s));
10420        }
10421        for (r, s) in &inv_slot {
10422            reg.insert(*r, Src::In(n_state + *s));
10423        }
10424
10425        // The continue predicate: run the header straight-line (it must produce no datapath cells)
10426        // and read off the comparison feeding the JumpIfFalse.
10427        let mut hdr_cells: Vec<LoopCell> = Vec::new();
10428        let mut preds: HashMap<u16, (CmpKind, Src, Src)> = HashMap::new();
10429        let mut hdr_reg = reg.clone();
10430        exec_ops(
10431            &header_ops,
10432            &mut hdr_reg,
10433            &mut hdr_cells,
10434            &mut preds,
10435            &program.constants,
10436        );
10437        assert!(
10438            hdr_cells.is_empty(),
10439            "while fabric: arithmetic in the loop predicate is unsupported (G3 scope)"
10440        );
10441        let pred = *preds
10442            .get(&cond_reg)
10443            .expect("while fabric: the header test is not a comparison");
10444
10445        let body = if_convert_body(
10446            &body_ops,
10447            &mut reg,
10448            &carried,
10449            &program.constants,
10450            Vec::new(),
10451        );
10452        let init: Vec<Src> = carried.iter().map(|r| getp(&pro, *r)).collect();
10453        let next: Vec<Src> = carried.iter().map(|&r| *reg.get(&r).unwrap()).collect();
10454        let invariants: Vec<Src> = invariants_regs.iter().map(|r| getp(&pro, *r)).collect();
10455        let result_state = *state_slot
10456            .get(&result_reg)
10457            .expect("while fabric: the returned register is not loop-carried");
10458
10459        WhileFabric {
10460            n_in: f.param_count as usize,
10461            n_state,
10462            init,
10463            invariants,
10464            body,
10465            next,
10466            pred,
10467            result_state,
10468        }
10469    })
10470}
10471
10472/// Evaluate the while-CGRA: initialise the state, then WHILE the fabric predicate holds apply the
10473/// body and latch. 64-bit wrapping — the VM's `Int` mod 2⁶⁴. A guard bounds non-termination.
10474pub fn eval_while_fabric(wf: &WhileFabric, params: &[i64]) -> i64 {
10475    let empty: [i64; 0] = [];
10476    let mut state: Vec<i64> = wf.init.iter().map(|s| s.eval(params, &empty)).collect();
10477    let invs: Vec<i64> = wf
10478        .invariants
10479        .iter()
10480        .map(|s| s.eval(params, &empty))
10481        .collect();
10482    let holds = |state: &[i64]| -> bool {
10483        let mut bin: Vec<i64> = state.to_vec();
10484        bin.extend_from_slice(&invs);
10485        let (cmp, ca, cb) = &wf.pred;
10486        cmp.eval(ca.eval(&bin, &empty), cb.eval(&bin, &empty))
10487    };
10488    let mut guard: u64 = 0;
10489    while holds(&state) {
10490        let mut bin: Vec<i64> = state.clone();
10491        bin.extend_from_slice(&invs);
10492        let mut vals: Vec<i64> = Vec::with_capacity(wf.body.len());
10493        for cell in &wf.body {
10494            let v = match cell {
10495                LoopCell::Arith { op, a, b } => op.eval(a.eval(&bin, &vals), b.eval(&bin, &vals)),
10496                LoopCell::VectorArith { op, ty, a, b } => {
10497                    op.eval(*ty, a.eval(&bin, &vals) as u64, b.eval(&bin, &vals) as u64) as i64
10498                }
10499                LoopCell::Func { unit, a, b } => {
10500                    unit.eval(a.eval(&bin, &vals), b.eval(&bin, &vals))
10501                }
10502                LoopCell::MemRead { .. }
10503                | LoopCell::HeapRead { .. }
10504                | LoopCell::HeapWrite { .. }
10505                | LoopCell::HeapAlloc { .. } => panic!(
10506                    "MemRead reached an evaluator that does not thread the iterated sequence — \
10507                     a sequence loop must be evaluated via eval_loop_fabric_seq"
10508                ),
10509                LoopCell::Select { cmp, ca, cb, t, e } => {
10510                    if cmp.eval(ca.eval(&bin, &vals), cb.eval(&bin, &vals)) {
10511                        t.eval(&bin, &vals)
10512                    } else {
10513                        e.eval(&bin, &vals)
10514                    }
10515                }
10516            };
10517            vals.push(v);
10518        }
10519        state = wf.next.iter().map(|s| s.eval(&bin, &vals)).collect();
10520        guard += 1;
10521        assert!(
10522            guard < 100_000_000,
10523            "while fabric did not terminate within the guard"
10524        );
10525    }
10526    state[wf.result_state]
10527}
10528
10529/// Emit the while-CGRA as a SEQUENTIAL circuit: a combinational body datapath + a controller whose
10530/// `running` flag is the FABRIC PREDICATE — evaluated on the init state (at reset) and on the next
10531/// state (each step) to decide whether to keep iterating. No counter: the trip count is data.
10532pub fn emit_while_fabric_verilog(name: &str, wf: &WhileFabric) -> String {
10533    let ns = wf.n_state;
10534    let name_in = |k: usize| -> String {
10535        if k < ns {
10536            format!("s{k}")
10537        } else {
10538            format!("inv{}", k - ns)
10539        }
10540    };
10541    let name_val = |k: usize| format!("b{k}");
10542    let circ = |s: &Src| src_named(s, &|k| format!("in{k}"), &|k| format!("v{k}"));
10543
10544    // Render the predicate with a caller-chosen rendering of the STATE operands.
10545    let pred_with = |state_name: &dyn Fn(usize) -> String| -> String {
10546        let (cmp, ca, cb) = &wf.pred;
10547        let r = |s: &Src| -> String {
10548            match s {
10549                Src::In(k) if *k < ns => state_name(*k),
10550                Src::In(k) => format!("inv{}", k - ns),
10551                Src::Const(c) => {
10552                    if *c < 0 {
10553                        format!("-64'sd{}", (*c as i128).unsigned_abs())
10554                    } else {
10555                        format!("64'sd{c}")
10556                    }
10557                }
10558                Src::Val(_) => unreachable!("while predicate has no datapath vals"),
10559            }
10560        };
10561        format!("($signed({}) {} $signed({}))", r(ca), cmp.verilog(), r(cb))
10562    };
10563
10564    let inputs = (0..wf.n_in)
10565        .map(|k| format!("input [63:0] in{k}"))
10566        .collect::<Vec<_>>()
10567        .join(", ");
10568    let inputs = if inputs.is_empty() {
10569        String::new()
10570    } else {
10571        format!("{inputs}, ")
10572    };
10573    let mut invs = String::new();
10574    for (j, s) in wf.invariants.iter().enumerate() {
10575        invs.push_str(&format!("  wire [63:0] inv{j} = {};\n", circ(s)));
10576    }
10577    let mut bodyw = String::new();
10578    for (i, cell) in wf.body.iter().enumerate() {
10579        let rhs = match cell {
10580            LoopCell::Arith { op, a, b } => {
10581                op.verilog(&src_named(a, &name_in, &name_val), &src_named(b, &name_in, &name_val))
10582            }
10583            LoopCell::VectorArith { op, ty, a, b } => {
10584                op.verilog(*ty, &src_named(a, &name_in, &name_val), &src_named(b, &name_in, &name_val))
10585            }
10586            LoopCell::Func { unit, a, b } => unit.verilog(&src_named(a, &name_in, &name_val), &src_named(b, &name_in, &name_val)),
10587            LoopCell::MemRead { .. } | LoopCell::HeapRead { .. } | LoopCell::HeapWrite { .. } | LoopCell::HeapAlloc { .. } => panic!(
10588                "MemRead RTL emission (a scratchpad-read tile driven by the induction counter) is the \
10589                 P2 scratchpad follow-on; the sequence-loop LOWERING is proven via eval_loop_fabric_seq"
10590            ),
10591            LoopCell::Select { cmp, ca, cb, t, e } => format!(
10592                "($signed({}) {} $signed({})) ? {} : {}",
10593                src_named(ca, &name_in, &name_val),
10594                cmp.verilog(),
10595                src_named(cb, &name_in, &name_val),
10596                src_named(t, &name_in, &name_val),
10597                src_named(e, &name_in, &name_val),
10598            ),
10599        };
10600        bodyw.push_str(&format!("  wire [63:0] b{i} = {rhs};\n"));
10601    }
10602    let mut nexts = String::new();
10603    for (j, s) in wf.next.iter().enumerate() {
10604        nexts.push_str(&format!(
10605            "  wire [63:0] ns{j} = {};\n",
10606            src_named(s, &name_in, &name_val)
10607        ));
10608    }
10609    let mut reset = String::new();
10610    let mut step = String::new();
10611    for (j, s) in wf.init.iter().enumerate() {
10612        reset.push_str(&format!("      s{j} <= {};\n", circ(s)));
10613        step.push_str(&format!("      s{j} <= ns{j};\n"));
10614    }
10615    let state_decl = (0..ns)
10616        .map(|j| format!("s{j}"))
10617        .collect::<Vec<_>>()
10618        .join(", ");
10619    // The continue predicate on the INIT state (reset) and on the NEXT state (each step).
10620    let pred_init = pred_with(&|k| circ(&wf.init[k]));
10621    let pred_nxt = pred_with(&|k| format!("ns{k}"));
10622
10623    format!(
10624        "// while_{name} — a compiled WHILE loop as a CGRA: a fixed body datapath streamed by a\n\
10625         // controller whose CONTINUE condition is FABRIC-COMPUTED (a predicate over the state), not\n\
10626         // a counter — the data-dependent trip count is decided by the datapath. Futamura P1 for a\n\
10627         // data-dependent loop; 64-bit so it wraps like the VM's Int; body-step and predicate are\n\
10628         // each certified == the VM by our own SAT.\n\
10629         module while_{name}(input clk, input rst, {inputs}output [63:0] result, output done);\n\
10630         \x20 reg [63:0] {state_decl};\n\
10631         \x20 reg running;\n\
10632         {invs}{bodyw}{nexts}\
10633         \x20 always @(posedge clk) begin\n\
10634         \x20   if (rst) begin\n\
10635         {reset}\
10636         \x20     running <= {pred_init};\n\
10637         \x20   end else if (running) begin\n\
10638         {step}\
10639         \x20     running <= {pred_nxt};\n\
10640         \x20   end\n\
10641         \x20 end\n\
10642         \x20 assign done = !running;\n\
10643         \x20 assign result = s{result_state};\n\
10644         endmodule\n",
10645        result_state = wf.result_state,
10646    )
10647}
10648
10649/// Lower ONE while-body state register's next-state to a `BoundedExpr` (over `s0..`, `inv0..`) for
10650/// the certified body step. No induction variable in the body-input convention.
10651pub fn while_next_to_bounded(
10652    wf: &WhileFabric,
10653    which: usize,
10654    width: u32,
10655) -> crate::codegen_sva::sva_to_verify::BoundedExpr {
10656    use crate::codegen_sva::sva_to_verify::{BitVecBoundedOp, BoundedExpr};
10657    let ns = wf.n_state;
10658    let name_in = move |k: usize| -> String {
10659        if k < ns {
10660            format!("s{k}")
10661        } else {
10662            format!("inv{}", k - ns)
10663        }
10664    };
10665    fn src_expr(
10666        s: &Src,
10667        vals: &[crate::codegen_sva::sva_to_verify::BoundedExpr],
10668        width: u32,
10669        name_in: &dyn Fn(usize) -> String,
10670    ) -> crate::codegen_sva::sva_to_verify::BoundedExpr {
10671        use crate::codegen_sva::sva_to_verify::BoundedExpr;
10672        match s {
10673            Src::In(k) => BoundedExpr::BitVecVar(name_in(*k), width),
10674            Src::Val(k) => vals[*k].clone(),
10675            Src::Const(c) => BoundedExpr::BitVecConst {
10676                width,
10677                value: *c as u64,
10678            },
10679        }
10680    }
10681    let slt = |l: BoundedExpr, r: BoundedExpr| BoundedExpr::BitVecBinary {
10682        op: BitVecBoundedOp::SLt,
10683        left: Box::new(l),
10684        right: Box::new(r),
10685    };
10686    let eq = |l: BoundedExpr, r: BoundedExpr| BoundedExpr::BitVecBinary {
10687        op: BitVecBoundedOp::Eq,
10688        left: Box::new(l),
10689        right: Box::new(r),
10690    };
10691    let mut vals: Vec<BoundedExpr> = Vec::with_capacity(wf.body.len());
10692    for cell in &wf.body {
10693        let expr = match cell {
10694            LoopCell::Arith { op, a, b } => {
10695                let a = src_expr(a, &vals, width, &name_in);
10696                let b = src_expr(b, &vals, width, &name_in);
10697                let bop = match op {
10698                    AluOp::Add => BitVecBoundedOp::Add,
10699                    AluOp::Sub => BitVecBoundedOp::Sub,
10700                    AluOp::Mul => BitVecBoundedOp::Mul,
10701                    AluOp::And => BitVecBoundedOp::And,
10702                    AluOp::Or => BitVecBoundedOp::Or,
10703                    AluOp::Xor => BitVecBoundedOp::Xor,
10704                    AluOp::Shl => BitVecBoundedOp::Shl,
10705                    AluOp::Shr => BitVecBoundedOp::AShr,
10706                    AluOp::UShr => BitVecBoundedOp::Shr,
10707                };
10708                BoundedExpr::BitVecBinary { op: bop, left: Box::new(a), right: Box::new(b) }
10709            }
10710            LoopCell::VectorArith { .. } => panic!(
10711                "a packed-SIMD (Simd) tile in a loop SAT bound is not SAT-lowered here — a Simd-bearing fabric \
10712                 is proven by eval ≡ VM + iverilog (simd_tile_lock + simd_rtl_lock), like a Func tile"
10713            ),
10714            LoopCell::Func { .. } => panic!(
10715                "division in a counted-loop body is not yet SAT-lowered — the shared_divider's \
10716                 own certified proof (rtl.rs::the_signed_divider_is_proven_correct_by_our_sat) covers \
10717                 the arithmetic; loop-carried division SAT is future work"
10718            ),
10719            LoopCell::MemRead { .. } | LoopCell::HeapRead { .. } | LoopCell::HeapWrite { .. } | LoopCell::HeapAlloc { .. } => panic!(
10720                "MemRead in a loop-carried SAT bound is future work (the memory-loop induction proof)"
10721            ),
10722            LoopCell::Select { cmp, ca, cb, t, e } => {
10723                let ca = src_expr(ca, &vals, width, &name_in);
10724                let cb = src_expr(cb, &vals, width, &name_in);
10725                let t = src_expr(t, &vals, width, &name_in);
10726                let e = src_expr(e, &vals, width, &name_in);
10727                let (cond, tt, ee) = match cmp {
10728                    CmpKind::Lt => (slt(ca, cb), t, e),
10729                    CmpKind::Gt => (slt(cb, ca), t, e),
10730                    CmpKind::Ge => (slt(ca, cb), e, t),
10731                    CmpKind::Le => (slt(cb, ca), e, t),
10732                    CmpKind::Eq => (eq(ca, cb), t, e),
10733                    CmpKind::Ne => (eq(ca, cb), e, t),
10734                };
10735                BoundedExpr::BitVecIte { cond: Box::new(cond), then: Box::new(tt), else_: Box::new(ee) }
10736            }
10737        };
10738        vals.push(expr);
10739    }
10740    src_expr(&wf.next[which], &vals, width, &name_in)
10741}
10742
10743/// A self-checking testbench for the while-CGRA: for each vector, pulse reset, clock until `done`,
10744/// and compare `result` against the VM oracle from `while_{name}_{in,out}.hex`.
10745pub fn emit_while_fabric_testbench(
10746    name: &str,
10747    wf: &WhileFabric,
10748    n: usize,
10749    max_cycles: usize,
10750) -> String {
10751    let drive = (0..wf.n_in)
10752        .map(|k| format!("in{k} = oin[i*{ni}+{k}];", ni = wf.n_in, k = k))
10753        .collect::<Vec<_>>()
10754        .join(" ");
10755    let ports = (0..wf.n_in)
10756        .map(|k| format!(".in{k}(in{k})"))
10757        .collect::<Vec<_>>()
10758        .join(", ");
10759    let ports = if ports.is_empty() {
10760        String::new()
10761    } else {
10762        format!("{ports}, ")
10763    };
10764    let decls = (0..wf.n_in)
10765        .map(|k| format!("  reg  [63:0] in{k};"))
10766        .collect::<Vec<_>>()
10767        .join("\n");
10768    format!(
10769        r#"// tb_while_{name} — the while-CGRA (fabric-predicate controller) vs the RUNNING VM.
10770`timescale 1ns/1ps
10771module tb_while_{name};
10772  reg clk = 0, rst;
10773{decls}
10774  reg  [63:0] oin [0:{ilast}];
10775  reg  [63:0] oout [0:{olast}];
10776  wire [63:0] result;
10777  wire done;
10778  integer i, c, errors = 0;
10779  while_{name} dut(.clk(clk), .rst(rst), {ports}.result(result), .done(done));
10780  always #5 clk = ~clk;
10781  initial begin
10782    $readmemh("while_{name}_in.hex", oin);
10783    $readmemh("while_{name}_out.hex", oout);
10784    for (i = 0; i < {n}; i = i + 1) begin
10785      @(negedge clk); {drive} rst = 1;
10786      @(negedge clk); rst = 0;
10787      c = 0;
10788      while (!done && c < {maxc}) begin @(negedge clk); c = c + 1; end
10789      if (result !== oout[i]) begin errors = errors + 1; $display("FAIL case %0d: got %0d exp %0d", i, result, oout[i]); end
10790    end
10791    if (errors == 0) $display("PASS: {n} while-CGRA runs match the VM (fabric predicate = trip count)");
10792    else $display("FAIL: %0d while-CGRA mismatches", errors);
10793    $finish;
10794  end
10795endmodule
10796"#,
10797        name = name,
10798        n = n,
10799        maxc = max_cycles,
10800        ilast = (n * wf.n_in).saturating_sub(1),
10801        olast = n - 1,
10802    )
10803}
10804
10805// ===========================================================================================
10806// NESTED-loop CGRA — the HIERARCHICAL controller. A perfectly-nested `Repeat … Repeat …` lowers
10807// to an outer counter that, each iteration, runs the INNER loop (itself a CGRA) to completion —
10808// the inner's range may depend on the outer induction variable (`for j from 1 to i`). Sound by
10809// DOUBLE induction: the inner body-step ≡ the VM's inner iteration (certified by our SAT) ⇒ the
10810// inner CGRA ≡ the VM inner loop; the outer applies that inner transition once per outer step ⇒
10811// the nested CGRA ≡ the VM. Reuses `if_convert_body`, so the inner body may itself branch.
10812// ===========================================================================================
10813
10814/// A range bound in the nested context: a circuit input, a constant, or the OUTER induction
10815/// variable (so `for j from 1 to i` is expressible).
10816#[derive(Clone, Copy, Debug, PartialEq)]
10817pub enum NestSrc {
10818    Param(usize),
10819    Const(i64),
10820    OuterIv,
10821}
10822
10823impl NestSrc {
10824    fn eval(&self, params: &[i64], outer_iv: i64) -> i64 {
10825        match self {
10826            NestSrc::Param(p) => params[*p],
10827            NestSrc::Const(c) => *c,
10828            NestSrc::OuterIv => outer_iv,
10829        }
10830    }
10831    /// Render over the FSM's `oi` register (the outer induction variable).
10832    fn verilog(&self) -> String {
10833        match self {
10834            NestSrc::Param(p) => format!("in{p}"),
10835            NestSrc::Const(c) => {
10836                if *c < 0 {
10837                    format!("-64'sd{}", (*c as i128).unsigned_abs())
10838                } else {
10839                    format!("64'sd{c}")
10840                }
10841            }
10842            NestSrc::OuterIv => "oi".to_string(),
10843        }
10844    }
10845}
10846
10847/// A perfectly-nested counted loop lowered to a HIERARCHICAL CGRA: an outer counter over the shared
10848/// loop-carried state, whose body is the inner loop (its own body datapath + inner counter), whose
10849/// range may reference the outer induction variable.
10850#[derive(Clone, Debug)]
10851pub struct NestedFabric {
10852    pub n_in: usize,
10853    pub n_state: usize,
10854    pub init: Vec<Src>,
10855    pub outer_lo: Src,
10856    pub outer_hi: Src,
10857    pub inner_lo: NestSrc,
10858    pub inner_hi: NestSrc,
10859    /// Inner body cells. `Src::In(k)`: `0..n_state` = state, `n_state` = inner iv (j),
10860    /// `n_state+1` = outer iv (i), `n_state+2+k` = invariant k.
10861    pub inner_body: Vec<LoopCell>,
10862    pub inner_next: Vec<Src>,
10863    pub invariants: Vec<Src>,
10864    pub result_state: usize,
10865}
10866
10867/// Recover the perfectly-nested loop in `fn_name` and lower it to a [`NestedFabric`].
10868pub fn nested_fabric_from_source(src: &str, fn_name: &str) -> NestedFabric {
10869    use crate::vm::{Compiler, Constant, Op};
10870    use std::collections::{BTreeSet, HashMap};
10871
10872    crate::ui_bridge::with_parsed_program(src, |parsed, interner| {
10873        let (stmts, _t, _p) = parsed.expect("parse failed");
10874        let program = Compiler::compile(stmts, interner).expect("compile failed");
10875        let f = program
10876            .functions
10877            .iter()
10878            .find(|f| interner.resolve(f.name) == fn_name)
10879            .unwrap_or_else(|| panic!("function `{fn_name}` not found"));
10880
10881        // FLOAT LANE (mirrors the loop / while fabrics): set up the shared thread-locals + seed float params
10882        // so the inner body's `exec_ops` routes float arithmetic to `FuncUnit::F*` tiles; the outer prologue
10883        // below admits a float accumulator constant and carries float-ness through `Move`. `int_const` stays
10884        // int-only — it is used for LOOP BOUNDS / structural indices, which are always integers.
10885        setup_program_thread_locals(&program);
10886        for (p, pt) in f.param_types.iter().enumerate() {
10887            if matches!(pt, Some(crate::vm::BoundaryType::Float)) {
10888                FLOAT_REGS.with(|t| {
10889                    t.borrow_mut().insert(p as u16);
10890                });
10891            }
10892        }
10893
10894        let mut ops: Vec<(usize, Op)> = Vec::new();
10895        for (i, op) in program.code.iter().enumerate().skip(f.entry_pc) {
10896            ops.push((i, *op));
10897            if matches!(op, Op::Return { .. } | Op::ReturnNothing) {
10898                break;
10899            }
10900        }
10901        let int_const = |ci: u32| -> i64 {
10902            match program.constants[ci as usize] {
10903                Constant::Int(n) => n,
10904                ref c => panic!("nested fabric: non-integer constant {c:?}"),
10905            }
10906        };
10907
10908        // ---- Outer prologue + outer IterNext ----
10909        let mut pro: HashMap<u16, Src> = HashMap::new();
10910        for p in 0..f.param_count {
10911            pro.insert(p, Src::In(p as usize));
10912        }
10913        let getp = |pro: &HashMap<u16, Src>, r: u16| -> Src {
10914            *pro.get(&r)
10915                .unwrap_or_else(|| panic!("nested outer prologue reads R{r}"))
10916        };
10917        let (mut outer_lo, mut outer_hi, mut outer_iv_reg, mut outer_header_pc) =
10918            (None, None, None, None);
10919        let mut outer_counter: Option<u16> = None;
10920        let mut outer_latch: Option<(u16, usize)> = None;
10921        let mut idx = 0;
10922        while idx < ops.len() {
10923            let (pc, op) = ops[idx];
10924            if let Some((c, bound, exit)) = counted_header(&ops, idx) {
10925                outer_latch = Some((bound, exit));
10926                outer_lo = Some(getp(&pro, c));
10927                outer_hi = Some(getp(&pro, bound));
10928                outer_iv_reg = Some(c);
10929                outer_header_pc = Some(pc);
10930                outer_counter = Some(c);
10931                idx += 2;
10932                break;
10933            }
10934            match op {
10935                Op::LoadConst { dst, idx: ci } => match program.constants[ci as usize] {
10936                    Constant::Float(fv) => {
10937                        pro.insert(dst, Src::Const(fv.to_bits() as i64));
10938                        FLOAT_REGS.with(|t| {
10939                            t.borrow_mut().insert(dst);
10940                        });
10941                    }
10942                    _ => {
10943                        pro.insert(dst, Src::Const(int_const(ci)));
10944                    }
10945                },
10946                Op::Move { dst, src } => {
10947                    let s = getp(&pro, src);
10948                    if FLOAT_REGS.with(|t| t.borrow().contains(&src)) {
10949                        FLOAT_REGS.with(|t| {
10950                            t.borrow_mut().insert(dst);
10951                        });
10952                    }
10953                    pro.insert(dst, s);
10954                }
10955                Op::NewRange { start, end, .. } => {
10956                    outer_lo = Some(getp(&pro, start));
10957                    outer_hi = Some(getp(&pro, end));
10958                }
10959                Op::IterPrepare { .. } => {}
10960                Op::IterNext { dst, .. } => {
10961                    outer_iv_reg = Some(dst);
10962                    outer_header_pc = Some(pc);
10963                    idx += 1;
10964                    break;
10965                }
10966                other => panic!("nested outer prologue: `{}` unsupported", other.name()),
10967            }
10968            idx += 1;
10969        }
10970        let outer_lo = outer_lo.expect("nested fabric: no outer NewRange");
10971        let outer_hi = outer_hi.expect("nested fabric: no outer range end");
10972        let outer_iv_reg = outer_iv_reg.expect("nested fabric: no outer IterNext");
10973        let outer_header_pc = outer_header_pc.expect("nested fabric: no outer header");
10974
10975        let outer_be = ops[idx..]
10976            .iter()
10977            .position(|(_pc, o)| matches!(o, Op::Jump { target } if *target == outer_header_pc))
10978            .map(|x| x + idx)
10979            .expect("nested fabric: no outer back-edge");
10980        let result_reg = ops
10981            .iter()
10982            .find_map(|(_, o)| match o {
10983                Op::Return { src } => Some(*src),
10984                _ => None,
10985            })
10986            .expect("nested fabric: no Return");
10987
10988        // ---- Inner prologue (inside the outer body): NestSrc-tracked so `for j from 1 to i` works.
10989        let mut pn: HashMap<u16, NestSrc> = HashMap::new();
10990        for p in 0..f.param_count {
10991            pn.insert(p, NestSrc::Param(p as usize));
10992        }
10993        pn.insert(outer_iv_reg, NestSrc::OuterIv);
10994        let getn = |pn: &HashMap<u16, NestSrc>, r: u16| -> NestSrc {
10995            *pn.get(&r)
10996                .unwrap_or_else(|| panic!("nested inner prologue reads R{r}"))
10997        };
10998        let (mut inner_lo, mut inner_hi, mut inner_iv_reg, mut inner_header_pc) =
10999            (None, None, None, None);
11000        let mut inner_counter: Option<u16> = None;
11001        let mut inner_latch: Option<(u16, usize)> = None;
11002        let mut k = idx;
11003        while k < outer_be {
11004            let (pc, op) = ops[k];
11005            if let Some((c, bound, exit)) = counted_header(&ops, k) {
11006                inner_latch = Some((bound, exit));
11007                inner_lo = Some(getn(&pn, c));
11008                inner_hi = Some(getn(&pn, bound));
11009                inner_iv_reg = Some(c);
11010                inner_header_pc = Some(pc);
11011                inner_counter = Some(c);
11012                k += 2;
11013                break;
11014            }
11015            match op {
11016                Op::LoadConst { dst, idx: ci } => {
11017                    pn.insert(dst, NestSrc::Const(int_const(ci)));
11018                }
11019                Op::Move { dst, src } => {
11020                    let s = getn(&pn, src);
11021                    pn.insert(dst, s);
11022                }
11023                Op::NewRange { start, end, .. } => {
11024                    inner_lo = Some(getn(&pn, start));
11025                    inner_hi = Some(getn(&pn, end));
11026                }
11027                Op::IterPrepare { .. } => {}
11028                Op::IterNext { dst, .. } => {
11029                    inner_iv_reg = Some(dst);
11030                    inner_header_pc = Some(pc);
11031                    k += 1;
11032                    break;
11033                }
11034                other => panic!(
11035                    "nested fabric: the outer body must be a single inner loop; got `{}` \
11036                     (deeper nesting / mixed bodies are future work)",
11037                    other.name()
11038                ),
11039            }
11040            k += 1;
11041        }
11042        let inner_lo = inner_lo.expect("nested fabric: no inner NewRange (not perfectly nested)");
11043        let inner_hi = inner_hi.expect("nested fabric: no inner range end");
11044        let inner_iv_reg = inner_iv_reg.expect("nested fabric: no inner IterNext");
11045        let inner_header_pc = inner_header_pc.expect("nested fabric: no inner header");
11046
11047        let inner_be = ops[k..outer_be]
11048            .iter()
11049            .position(|(_pc, o)| matches!(o, Op::Jump { target } if *target == inner_header_pc))
11050            .map(|x| x + k)
11051            .expect("nested fabric: no inner back-edge");
11052        let mut inner_body_pcops: Vec<(usize, Op)> = ops[k..inner_be].to_vec();
11053        // A counted inner loop's latch ends its body: the inner controller advances the counter.
11054        if let Some(c) = inner_counter {
11055            let is_one = |r: u16| matches!(pn.get(&r), Some(NestSrc::Const(1)));
11056            let (bound, exit) = inner_latch.expect("a counted inner loop has a header");
11057            match counted_latch(&inner_body_pcops, c, bound, exit, is_one) {
11058                0 => panic!(
11059                    "nested fabric: a counted inner loop's body does not end in its unit step"
11060                ),
11061                n => inner_body_pcops.truncate(inner_body_pcops.len() - n),
11062            }
11063        }
11064        // Everything between the inner back-edge and the outer back-edge is the inner IterPop, then a counted outer
11065        // loop's latch.
11066        let mut between: Vec<(usize, Op)> = ops[inner_be + 1..outer_be].to_vec();
11067        if let (Some(c), Some((bound, exit))) = (outer_counter, outer_latch) {
11068            let outer_is_one = |r: u16| matches!(pro.get(&r), Some(Src::Const(1)));
11069            let n = counted_latch(&between, c, bound, exit, outer_is_one);
11070            between.truncate(between.len() - n);
11071        }
11072        for (_, o) in &between {
11073            if !matches!(o, Op::IterPop) {
11074                panic!(
11075                    "nested fabric: the outer body has ops beyond the inner loop: `{}`",
11076                    o.name()
11077                );
11078            }
11079        }
11080
11081        let reg_of = |op: &Op| -> (Option<u16>, Vec<u16>) {
11082            match op {
11083                Op::LoadConst { dst, .. } => (Some(*dst), vec![]),
11084                // `now` / `today` — a clock value-load (reads the injected fixed clock); no register read.
11085                Op::LoadNow { dst } | Op::LoadToday { dst } => (Some(*dst), vec![]),
11086                Op::Move { dst, src } => (Some(*dst), vec![*src]),
11087                // Logical `not` → a `(src == 0) ? 1 : 0` Select mux (one read, like `Move`).
11088                Op::Not { dst, src } => (Some(*dst), vec![*src]),
11089                // A numeric-coprocessor builtin (sqrt/floor/abs/min/max) reads its `arg_count` argument
11090                // registers starting at `args_start`; `exec_ops` lowers it to the matching Func/Select tile.
11091                Op::CallBuiltin { dst, args_start, arg_count, .. } => {
11092                    (Some(*dst), (0..*arg_count as u16).map(|k| *args_start + k).collect())
11093                }
11094                Op::Add { dst, lhs, rhs }
11095                | Op::Sub { dst, lhs, rhs }
11096                | Op::Mul { dst, lhs, rhs }
11097                // Division / mod / floor-div → a functional-unit tile (int `DivS`/`ModS`/`FloorDivS`, or
11098                // `FDivF` when an operand is a float), so a nested inner body's `s / 2.0` is a real FDivF tile.
11099                | Op::Div { dst, lhs, rhs }
11100                | Op::Mod { dst, lhs, rhs }
11101                | Op::FloorDiv { dst, lhs, rhs }
11102                // Bitwise / shifts → combinational Arith tiles (integer): `a & b`, `a | b`, `a xor b`,
11103                // `a << b`, `a >> b`. `exec_ops` already emits the matching `AluOp`.
11104                | Op::BitAnd { dst, lhs, rhs }
11105                | Op::BitOr { dst, lhs, rhs }
11106                | Op::BitXor { dst, lhs, rhs }
11107                | Op::Shl { dst, lhs, rhs }
11108                | Op::Shr { dst, lhs, rhs }
11109                // Power (`**` / `to the power of`) → the sequential-multiply `Pow` functional-unit tile.
11110                | Op::Pow { dst, lhs, rhs }
11111                | Op::Lt { dst, lhs, rhs }
11112                | Op::Gt { dst, lhs, rhs }
11113                | Op::LtEq { dst, lhs, rhs }
11114                | Op::GtEq { dst, lhs, rhs }
11115                | Op::Eq { dst, lhs, rhs }
11116                | Op::NotEq { dst, lhs, rhs } => (Some(*dst), vec![*lhs, *rhs]),
11117                Op::AddAssign { dst, src } => (Some(*dst), vec![*dst, *src]),
11118                Op::Jump { .. } | Op::JumpIfFalse { .. } => (None, vec![]),
11119                other => panic!("nested inner body: `{}` unsupported", other.name()),
11120            }
11121        };
11122        // The outer induction value under every name the inner prologue gives it: the counter, and the copy the
11123        // compiler makes of it as the loop's item (`Move item = counter` after the header).
11124        let outer_iv_regs: BTreeSet<u16> = pn
11125            .iter()
11126            .filter(|(_, s)| matches!(s, NestSrc::OuterIv))
11127            .map(|(r, _)| *r)
11128            .collect();
11129        let mut written: BTreeSet<u16> = BTreeSet::new();
11130        let mut upward: BTreeSet<u16> = BTreeSet::new();
11131        for (_, op) in &inner_body_pcops {
11132            let (dst, reads) = reg_of(op);
11133            for r in reads {
11134                if r != inner_iv_reg && !outer_iv_regs.contains(&r) && !written.contains(&r) {
11135                    upward.insert(r);
11136                }
11137            }
11138            if let Some(d) = dst {
11139                written.insert(d);
11140            }
11141        }
11142        let mut carried: Vec<u16> = upward.intersection(&written).copied().collect();
11143        if written.contains(&result_reg) && !carried.contains(&result_reg) {
11144            carried.push(result_reg);
11145        }
11146        carried.sort_unstable();
11147        let invariants_regs: Vec<u16> = upward.difference(&written).copied().collect();
11148        let n_state = carried.len();
11149        let state_slot: HashMap<u16, usize> =
11150            carried.iter().enumerate().map(|(i, r)| (*r, i)).collect();
11151        let inv_slot: HashMap<u16, usize> = invariants_regs
11152            .iter()
11153            .enumerate()
11154            .map(|(i, r)| (*r, i))
11155            .collect();
11156
11157        // Inner body inputs: state, then inner iv, then outer iv, then invariants.
11158        let mut reg: HashMap<u16, Src> = HashMap::new();
11159        for (r, s) in &state_slot {
11160            reg.insert(*r, Src::In(*s));
11161        }
11162        reg.insert(inner_iv_reg, Src::In(n_state));
11163        for r in &outer_iv_regs {
11164            reg.insert(*r, Src::In(n_state + 1));
11165        }
11166        for (r, s) in &inv_slot {
11167            reg.insert(*r, Src::In(n_state + 2 + *s));
11168        }
11169        let inner_body = if_convert_body(
11170            &inner_body_pcops,
11171            &mut reg,
11172            &carried,
11173            &program.constants,
11174            Vec::new(),
11175        );
11176        let init: Vec<Src> = carried.iter().map(|r| getp(&pro, *r)).collect();
11177        let inner_next: Vec<Src> = carried.iter().map(|&r| *reg.get(&r).unwrap()).collect();
11178        let invariants: Vec<Src> = invariants_regs.iter().map(|r| getp(&pro, *r)).collect();
11179        let result_state = *state_slot
11180            .get(&result_reg)
11181            .expect("nested fabric: the returned register is not loop-carried");
11182
11183        NestedFabric {
11184            n_in: f.param_count as usize,
11185            n_state,
11186            init,
11187            outer_lo,
11188            outer_hi,
11189            inner_lo,
11190            inner_hi,
11191            inner_body,
11192            inner_next,
11193            invariants,
11194            result_state,
11195        }
11196    })
11197}
11198
11199/// Evaluate the nested CGRA: the outer counter drives the inner loop to completion each iteration
11200/// (the inner range may reference the outer iv). 64-bit wrapping — the VM's `Int` mod 2⁶⁴.
11201pub fn eval_nested_fabric(nf: &NestedFabric, params: &[i64]) -> i64 {
11202    let empty: [i64; 0] = [];
11203    let mut state: Vec<i64> = nf.init.iter().map(|s| s.eval(params, &empty)).collect();
11204    let invs: Vec<i64> = nf
11205        .invariants
11206        .iter()
11207        .map(|s| s.eval(params, &empty))
11208        .collect();
11209    let olo = nf.outer_lo.eval(params, &empty);
11210    let ohi = nf.outer_hi.eval(params, &empty);
11211    let mut i = olo;
11212    while i <= ohi {
11213        let ilo = nf.inner_lo.eval(params, i);
11214        let ihi = nf.inner_hi.eval(params, i);
11215        let mut j = ilo;
11216        while j <= ihi {
11217            let mut bin: Vec<i64> = state.clone();
11218            bin.push(j);
11219            bin.push(i);
11220            bin.extend_from_slice(&invs);
11221            let mut vals: Vec<i64> = Vec::with_capacity(nf.inner_body.len());
11222            for cell in &nf.inner_body {
11223                let v = match cell {
11224                    LoopCell::Arith { op, a, b } => {
11225                        op.eval(a.eval(&bin, &vals), b.eval(&bin, &vals))
11226                    }
11227                    LoopCell::VectorArith { op, ty, a, b } => {
11228                        op.eval(*ty, a.eval(&bin, &vals) as u64, b.eval(&bin, &vals) as u64) as i64
11229                    }
11230                    LoopCell::Func { unit, a, b } => {
11231                        unit.eval(a.eval(&bin, &vals), b.eval(&bin, &vals))
11232                    }
11233                    LoopCell::MemRead { .. }
11234                    | LoopCell::HeapRead { .. }
11235                    | LoopCell::HeapWrite { .. }
11236                    | LoopCell::HeapAlloc { .. } => panic!(
11237                        "MemRead reached an evaluator that does not thread the iterated sequence — \
11238                         a sequence loop must be evaluated via eval_loop_fabric_seq"
11239                    ),
11240                    LoopCell::Select { cmp, ca, cb, t, e } => {
11241                        if cmp.eval(ca.eval(&bin, &vals), cb.eval(&bin, &vals)) {
11242                            t.eval(&bin, &vals)
11243                        } else {
11244                            e.eval(&bin, &vals)
11245                        }
11246                    }
11247                };
11248                vals.push(v);
11249            }
11250            state = nf.inner_next.iter().map(|s| s.eval(&bin, &vals)).collect();
11251            if j == ihi {
11252                break;
11253            }
11254            j = j.wrapping_add(1);
11255        }
11256        if i == ohi {
11257            break;
11258        }
11259        i = i.wrapping_add(1);
11260    }
11261    state[nf.result_state]
11262}
11263
11264/// Emit the nested CGRA as a HIERARCHICAL FSM: an outer counter that, per iteration, sets up the
11265/// inner range (from the outer iv) and runs the inner counter to completion, streaming the shared
11266/// state through the inner body datapath. Three states — SETUP (arm inner for `oi`), RUN (inner
11267/// iterates), and advance-outer — so the inner range recomputes correctly when the outer advances.
11268pub fn emit_nested_fabric_verilog(name: &str, nf: &NestedFabric) -> String {
11269    let ns = nf.n_state;
11270    let name_in = |k: usize| -> String {
11271        if k < ns {
11272            format!("s{k}")
11273        } else if k == ns {
11274            "ij".to_string()
11275        } else if k == ns + 1 {
11276            "oi".to_string()
11277        } else {
11278            format!("inv{}", k - ns - 2)
11279        }
11280    };
11281    let name_val = |k: usize| format!("b{k}");
11282    let circ = |s: &Src| src_named(s, &|k| format!("in{k}"), &|k| format!("v{k}"));
11283
11284    let inputs = (0..nf.n_in)
11285        .map(|k| format!("input [63:0] in{k}"))
11286        .collect::<Vec<_>>()
11287        .join(", ");
11288    let inputs = if inputs.is_empty() {
11289        String::new()
11290    } else {
11291        format!("{inputs}, ")
11292    };
11293    let mut invs = String::new();
11294    for (j, s) in nf.invariants.iter().enumerate() {
11295        invs.push_str(&format!("  wire [63:0] inv{j} = {};\n", circ(s)));
11296    }
11297    let mut bodyw = String::new();
11298    for (i, cell) in nf.inner_body.iter().enumerate() {
11299        let rhs = match cell {
11300            LoopCell::Arith { op, a, b } => {
11301                op.verilog(&src_named(a, &name_in, &name_val), &src_named(b, &name_in, &name_val))
11302            }
11303            LoopCell::VectorArith { op, ty, a, b } => {
11304                op.verilog(*ty, &src_named(a, &name_in, &name_val), &src_named(b, &name_in, &name_val))
11305            }
11306            LoopCell::Func { unit, a, b } => unit.verilog(&src_named(a, &name_in, &name_val), &src_named(b, &name_in, &name_val)),
11307            LoopCell::MemRead { .. } | LoopCell::HeapRead { .. } | LoopCell::HeapWrite { .. } | LoopCell::HeapAlloc { .. } => panic!(
11308                "MemRead RTL emission (a scratchpad-read tile driven by the induction counter) is the \
11309                 P2 scratchpad follow-on; the sequence-loop LOWERING is proven via eval_loop_fabric_seq"
11310            ),
11311            LoopCell::Select { cmp, ca, cb, t, e } => format!(
11312                "($signed({}) {} $signed({})) ? {} : {}",
11313                src_named(ca, &name_in, &name_val),
11314                cmp.verilog(),
11315                src_named(cb, &name_in, &name_val),
11316                src_named(t, &name_in, &name_val),
11317                src_named(e, &name_in, &name_val),
11318            ),
11319        };
11320        bodyw.push_str(&format!("  wire [63:0] b{i} = {rhs};\n"));
11321    }
11322    let mut nexts = String::new();
11323    for (j, s) in nf.inner_next.iter().enumerate() {
11324        nexts.push_str(&format!(
11325            "  wire [63:0] ns{j} = {};\n",
11326            src_named(s, &name_in, &name_val)
11327        ));
11328    }
11329    let mut reset = String::new();
11330    let mut step = String::new();
11331    for (j, s) in nf.init.iter().enumerate() {
11332        reset.push_str(&format!("      s{j} <= {};\n", circ(s)));
11333        step.push_str(&format!("        s{j} <= ns{j};\n"));
11334    }
11335    let state_decl = (0..ns)
11336        .map(|j| format!("s{j}"))
11337        .collect::<Vec<_>>()
11338        .join(", ");
11339
11340    format!(
11341        "// nested_{name} — a compiled NESTED LOOP as a HIERARCHICAL CGRA: an outer counter that\n\
11342         // runs the INNER loop (its own body datapath + counter) to completion each iteration; the\n\
11343         // inner range may depend on the outer induction variable. Futamura P1 for nested control\n\
11344         // flow; 64-bit so it wraps like the VM's Int; the inner body-step is certified == the VM by\n\
11345         // our own SAT, and the nested loop is sound by double induction.\n\
11346         module nested_{name}(input clk, input rst, {inputs}output [63:0] result, output done);\n\
11347         \x20 localparam SETUP = 2'd0, RUN = 2'd1, DONE = 2'd2;\n\
11348         \x20 reg [1:0] st;\n\
11349         \x20 reg [63:0] {state_decl};\n\
11350         \x20 reg [63:0] oi, ij;\n\
11351         \x20 wire [63:0] olo = {olo};\n\
11352         \x20 wire [63:0] ohi = {ohi};\n\
11353         \x20 wire [63:0] ilo = {ilo};\n\
11354         \x20 wire [63:0] ihi = {ihi};\n\
11355         {invs}{bodyw}{nexts}\
11356         \x20 always @(posedge clk) begin\n\
11357         \x20   if (rst) begin\n\
11358         {reset}\
11359         \x20     oi <= olo;\n\
11360         \x20     st <= ($signed(olo) <= $signed(ohi)) ? SETUP : DONE;\n\
11361         \x20   end else if (st == SETUP) begin\n\
11362         \x20     ij <= ilo;\n\
11363         \x20     if ($signed(ilo) <= $signed(ihi)) st <= RUN;\n\
11364         \x20     else if ($signed(oi) < $signed(ohi)) oi <= oi + 64'sd1;\n\
11365         \x20     else st <= DONE;\n\
11366         \x20   end else if (st == RUN) begin\n\
11367         {step}\
11368         \x20     if ($signed(ij) < $signed(ihi)) ij <= ij + 64'sd1;\n\
11369         \x20     else if ($signed(oi) < $signed(ohi)) begin oi <= oi + 64'sd1; st <= SETUP; end\n\
11370         \x20     else st <= DONE;\n\
11371         \x20   end\n\
11372         \x20 end\n\
11373         \x20 assign done = (st == DONE);\n\
11374         \x20 assign result = s{result_state};\n\
11375         endmodule\n",
11376        olo = circ(&nf.outer_lo),
11377        ohi = circ(&nf.outer_hi),
11378        ilo = nf.inner_lo.verilog(),
11379        ihi = nf.inner_hi.verilog(),
11380        result_state = nf.result_state,
11381    )
11382}
11383
11384/// Lower ONE inner-body state register's next-state to a `BoundedExpr` over `s0..`, `ij`, `oi`,
11385/// `inv0..` — for the certified inner body step (the base of the double induction).
11386pub fn nested_inner_next_to_bounded(
11387    nf: &NestedFabric,
11388    which: usize,
11389    width: u32,
11390) -> crate::codegen_sva::sva_to_verify::BoundedExpr {
11391    use crate::codegen_sva::sva_to_verify::{BitVecBoundedOp, BoundedExpr};
11392    let ns = nf.n_state;
11393    let name_in = move |k: usize| -> String {
11394        if k < ns {
11395            format!("s{k}")
11396        } else if k == ns {
11397            "ij".to_string()
11398        } else if k == ns + 1 {
11399            "oi".to_string()
11400        } else {
11401            format!("inv{}", k - ns - 2)
11402        }
11403    };
11404    fn src_expr(
11405        s: &Src,
11406        vals: &[crate::codegen_sva::sva_to_verify::BoundedExpr],
11407        width: u32,
11408        name_in: &dyn Fn(usize) -> String,
11409    ) -> crate::codegen_sva::sva_to_verify::BoundedExpr {
11410        use crate::codegen_sva::sva_to_verify::BoundedExpr;
11411        match s {
11412            Src::In(k) => BoundedExpr::BitVecVar(name_in(*k), width),
11413            Src::Val(k) => vals[*k].clone(),
11414            Src::Const(c) => BoundedExpr::BitVecConst {
11415                width,
11416                value: *c as u64,
11417            },
11418        }
11419    }
11420    let slt = |l: BoundedExpr, r: BoundedExpr| BoundedExpr::BitVecBinary {
11421        op: BitVecBoundedOp::SLt,
11422        left: Box::new(l),
11423        right: Box::new(r),
11424    };
11425    let eq = |l: BoundedExpr, r: BoundedExpr| BoundedExpr::BitVecBinary {
11426        op: BitVecBoundedOp::Eq,
11427        left: Box::new(l),
11428        right: Box::new(r),
11429    };
11430    let mut vals: Vec<BoundedExpr> = Vec::with_capacity(nf.inner_body.len());
11431    for cell in &nf.inner_body {
11432        let expr = match cell {
11433            LoopCell::Arith { op, a, b } => {
11434                let a = src_expr(a, &vals, width, &name_in);
11435                let b = src_expr(b, &vals, width, &name_in);
11436                let bop = match op {
11437                    AluOp::Add => BitVecBoundedOp::Add,
11438                    AluOp::Sub => BitVecBoundedOp::Sub,
11439                    AluOp::Mul => BitVecBoundedOp::Mul,
11440                    AluOp::And => BitVecBoundedOp::And,
11441                    AluOp::Or => BitVecBoundedOp::Or,
11442                    AluOp::Xor => BitVecBoundedOp::Xor,
11443                    AluOp::Shl => BitVecBoundedOp::Shl,
11444                    AluOp::Shr => BitVecBoundedOp::AShr,
11445                    AluOp::UShr => BitVecBoundedOp::Shr,
11446                };
11447                BoundedExpr::BitVecBinary { op: bop, left: Box::new(a), right: Box::new(b) }
11448            }
11449            LoopCell::VectorArith { .. } => panic!(
11450                "a packed-SIMD (Simd) tile in a loop SAT bound is not SAT-lowered here — a Simd-bearing fabric \
11451                 is proven by eval ≡ VM + iverilog (simd_tile_lock + simd_rtl_lock), like a Func tile"
11452            ),
11453            LoopCell::Func { .. } => panic!(
11454                "division in a counted-loop body is not yet SAT-lowered — the shared_divider's \
11455                 own certified proof (rtl.rs::the_signed_divider_is_proven_correct_by_our_sat) covers \
11456                 the arithmetic; loop-carried division SAT is future work"
11457            ),
11458            LoopCell::MemRead { .. } | LoopCell::HeapRead { .. } | LoopCell::HeapWrite { .. } | LoopCell::HeapAlloc { .. } => panic!(
11459                "MemRead in a loop-carried SAT bound is future work (the memory-loop induction proof)"
11460            ),
11461            LoopCell::Select { cmp, ca, cb, t, e } => {
11462                let ca = src_expr(ca, &vals, width, &name_in);
11463                let cb = src_expr(cb, &vals, width, &name_in);
11464                let t = src_expr(t, &vals, width, &name_in);
11465                let e = src_expr(e, &vals, width, &name_in);
11466                let (cond, tt, ee) = match cmp {
11467                    CmpKind::Lt => (slt(ca, cb), t, e),
11468                    CmpKind::Gt => (slt(cb, ca), t, e),
11469                    CmpKind::Ge => (slt(ca, cb), e, t),
11470                    CmpKind::Le => (slt(cb, ca), e, t),
11471                    CmpKind::Eq => (eq(ca, cb), t, e),
11472                    CmpKind::Ne => (eq(ca, cb), e, t),
11473                };
11474                BoundedExpr::BitVecIte { cond: Box::new(cond), then: Box::new(tt), else_: Box::new(ee) }
11475            }
11476        };
11477        vals.push(expr);
11478    }
11479    src_expr(&nf.inner_next[which], &vals, width, &name_in)
11480}
11481
11482/// A self-checking testbench for the nested CGRA vs the VM oracle (`nested_{name}_{in,out}.hex`).
11483pub fn emit_nested_fabric_testbench(
11484    name: &str,
11485    nf: &NestedFabric,
11486    n: usize,
11487    max_cycles: usize,
11488) -> String {
11489    let drive = (0..nf.n_in)
11490        .map(|k| format!("in{k} = oin[i*{ni}+{k}];", ni = nf.n_in, k = k))
11491        .collect::<Vec<_>>()
11492        .join(" ");
11493    let ports = (0..nf.n_in)
11494        .map(|k| format!(".in{k}(in{k})"))
11495        .collect::<Vec<_>>()
11496        .join(", ");
11497    let ports = if ports.is_empty() {
11498        String::new()
11499    } else {
11500        format!("{ports}, ")
11501    };
11502    let decls = (0..nf.n_in)
11503        .map(|k| format!("  reg  [63:0] in{k};"))
11504        .collect::<Vec<_>>()
11505        .join("\n");
11506    format!(
11507        r#"// tb_nested_{name} — the hierarchical (outer+inner counter) CGRA vs the RUNNING VM.
11508`timescale 1ns/1ps
11509module tb_nested_{name};
11510  reg clk = 0, rst;
11511{decls}
11512  reg  [63:0] oin [0:{ilast}];
11513  reg  [63:0] oout [0:{olast}];
11514  wire [63:0] result;
11515  wire done;
11516  integer i, c, errors = 0;
11517  nested_{name} dut(.clk(clk), .rst(rst), {ports}.result(result), .done(done));
11518  always #5 clk = ~clk;
11519  initial begin
11520    $readmemh("nested_{name}_in.hex", oin);
11521    $readmemh("nested_{name}_out.hex", oout);
11522    for (i = 0; i < {n}; i = i + 1) begin
11523      @(negedge clk); {drive} rst = 1;
11524      @(negedge clk); rst = 0;
11525      c = 0;
11526      while (!done && c < {maxc}) begin @(negedge clk); c = c + 1; end
11527      if (result !== oout[i]) begin errors = errors + 1; $display("FAIL case %0d: got %0d exp %0d", i, result, oout[i]); end
11528    end
11529    if (errors == 0) $display("PASS: {n} nested-CGRA runs match the VM (outer counter streams the inner loop)");
11530    else $display("FAIL: %0d nested-CGRA mismatches", errors);
11531    $finish;
11532  end
11533endmodule
11534"#,
11535        name = name,
11536        n = n,
11537        maxc = max_cycles,
11538        ilast = (n * nf.n_in).saturating_sub(1),
11539        olast = n - 1,
11540    )
11541}
11542
11543/// A self-checking testbench for the CGRA: for each recorded input vector, pulse reset, clock
11544/// until `done`, and compare `result` against the VM oracle from `cgra_{in,out}.hex`.
11545pub fn emit_loop_fabric_testbench(
11546    name: &str,
11547    lf: &LoopFabric,
11548    n: usize,
11549    max_cycles: usize,
11550) -> String {
11551    let drive = (0..lf.n_in)
11552        .map(|k| format!("in{k} = oin[i*{ni}+{k}];", ni = lf.n_in, k = k))
11553        .collect::<Vec<_>>()
11554        .join(" ");
11555    let ports = (0..lf.n_in)
11556        .map(|k| format!(".in{k}(in{k})"))
11557        .collect::<Vec<_>>()
11558        .join(", ");
11559    let ports = if ports.is_empty() {
11560        String::new()
11561    } else {
11562        format!("{ports}, ")
11563    };
11564    let decls = (0..lf.n_in)
11565        .map(|k| format!("  reg  [63:0] in{k};"))
11566        .collect::<Vec<_>>()
11567        .join("\n");
11568    format!(
11569        r#"// tb_cgra_{name} — the CGRA (clocked body datapath) vs the RUNNING VM over the loop program.
11570`timescale 1ns/1ps
11571module tb_cgra_{name};
11572  reg clk = 0, rst;
11573{decls}
11574  reg  [63:0] oin [0:{ilast}];
11575  reg  [63:0] oout [0:{olast}];
11576  wire [63:0] result;
11577  wire done;
11578  integer i, c, errors = 0;
11579  cgra_{name} dut(.clk(clk), .rst(rst), {ports}.result(result), .done(done));
11580  always #5 clk = ~clk;
11581  initial begin
11582    $readmemh("cgra_{name}_in.hex", oin);
11583    $readmemh("cgra_{name}_out.hex", oout);
11584    for (i = 0; i < {n}; i = i + 1) begin
11585      @(negedge clk); {drive} rst = 1;
11586      @(negedge clk); rst = 0;
11587      c = 0;
11588      while (!done && c < {maxc}) begin @(negedge clk); c = c + 1; end
11589      if (result !== oout[i]) begin errors = errors + 1; $display("FAIL case %0d: got %0d exp %0d", i, result, oout[i]); end
11590    end
11591    if (errors == 0) $display("PASS: {n} CGRA loop runs match the VM (body = circuit, loop = counter)");
11592    else $display("FAIL: %0d CGRA mismatches", errors);
11593    $finish;
11594  end
11595endmodule
11596"#,
11597        name = name,
11598        n = n,
11599        maxc = max_cycles,
11600        ilast = (n * lf.n_in).saturating_sub(1),
11601        olast = n - 1,
11602    )
11603}
11604
11605#[cfg(test)]
11606#[path = "../../../logicaffeine_tests/unit/logicaffeine_compile/codegen_sva/fabric_rtl/tests.rs"]
11607mod tests;