Skip to main content

logicaffeine_compile/codegen_sva/
fabric_grid.rs

1//! fabric_grid — a TRUE reconfigurable CGRA: place a fabric's dataflow onto a 2-D array of
2//! identical tiles and route it through a LOCAL switchbox interconnect. The same silicon runs any
3//! fabric by reconfiguration — not a bespoke circuit per program.
4//!
5//! Interconnect: each tile drives two INDEPENDENT channels — a vertical `vs` (to its south
6//! neighbor) and a horizontal `he` (to its east neighbor) — so a value flowing down and a value
7//! routed across CROSS without conflict (the standard CGRA switchbox). Dataflow is feed-forward
8//! (south/east only), so the array is a combinational DAG. A tile reads its north neighbor's `vs`
9//! and its west neighbor's `he`; it may pass either through, TURN one into the other, source a
10//! global input / constant, or COMPUTE (an `Alu`/`Select` tapping those inputs) and drive the
11//! result onto a channel.
12//!
13//! The point (proven in `fabric_grid`'s S3 tests): whatever the placer/router did, the placed +
14//! routed array computes EXACTLY the abstract datapath — certified exhaustively by our own SAT.
15//! That is the property a trustworthy CGRA compiler needs: place-and-route is semantics-preserving.
16
17use super::fabric_rtl::{AluOp, CmpKind, FuncUnit};
18use super::simd::{LaneTy, VecOp};
19
20/// A value source visible to a tile: its north/west channel inputs, a global input, a constant,
21/// or zero (an unused channel).
22#[derive(Clone, Copy, Debug, PartialEq)]
23pub enum Src2 {
24    N,
25    W,
26    In(usize),
27    Const(i64),
28    Zero,
29}
30
31/// A tile's optional compute: an arithmetic op or a predicated select over its visible sources.
32#[derive(Clone, Copy, Debug)]
33pub enum CellKind {
34    Arith(AluOp, Src2, Src2),
35    Select(CmpKind, Src2, Src2, Src2, Src2),
36    /// A FUNCTIONAL-UNIT tile — division/mod/floor-div, magic-reciprocal high-multiply, power, or a float op
37    /// (`FuncUnit`). Combinational at the RTL level (a synthesized divider / the FPU's combinational form), so
38    /// it settles in the same 1-clock feed-forward pass as `Arith` — but it is NOT SAT-lowered (bitblast has
39    /// no `bvsdiv`/`bvsrem`), so a Func-bearing grid is proven by `eval_grid ≡ VM` + iverilog, exactly as the
40    /// decision fabric proves division (`emit_decision_fabric_verilog` + `microprice_rtl_matches_vm_under_iverilog`).
41    Func(FuncUnit, Src2, Src2),
42    /// A packed-SIMD tile — a `VecOp` (add / bitwise) over two operands, computed lane-wise for the given
43    /// `LaneTy` (`Add` = the carry-isolated SWAR form). Combinational, so it settles in the same 1-clock pass
44    /// as `Arith`; but it is NOT SAT-lowered here (proven ≡ lane-wise by `simd_tile_lock` + iverilog), so a
45    /// Simd-bearing grid is combinational-for-emission yet refused by the reconfig SAT (like `Func`).
46    Simd(VecOp, LaneTy, Src2, Src2),
47    /// A MEMORY (state-plane) tile — a read/write/alloc against ONE shared heap that all `Mem` tiles access
48    /// (the `dmem`-style block RAM the soft-core already carries). The heap mirrors the VM/model layout
49    /// (`[len, e0..eN]`), so collections/structs/text/CRDT compose from `Mem` + `Arith` tiles exactly as the
50    /// whole-program model (`fabric_rtl.rs` exec_ops) does. Stateful (a synchronous BRAM), so it is NOT
51    /// SAT-lowered and is NOT combinational — proven by `eval_grid_with_heap ≡ the model ≡ VM` + iverilog.
52    /// The tile's two `Src2` operands are (A, B): `HeapRead` reads `heap[A]`, `HeapWrite` does `heap[A] = B`
53    /// and drives `B`, `HeapAlloc` grows the heap by `A` words and drives the new base pointer.
54    Mem(MemOp, Src2, Src2),
55}
56
57/// The three state-plane operations a `Mem` tile performs against the shared heap (see [`CellKind::Mem`]).
58#[derive(Clone, Copy, Debug, PartialEq, Eq)]
59pub enum MemOp {
60    /// `out = heap[A]` (0 if out of bounds — mirrors the model's `heap.get(a).unwrap_or(0)`).
61    HeapRead,
62    /// `heap[A] = B`; the tile also drives `B` onto its output channel (mirrors the model's write returning `v`).
63    HeapWrite,
64    /// Allocate `A` fresh (zero) words at the top of the heap; `out = the new base pointer` (`heap.len()`).
65    HeapAlloc,
66}
67
68/// What drives one of a tile's output channels: a routed source, or the tile's compute result.
69#[derive(Clone, Copy, Debug)]
70pub enum Drive {
71    Pass(Src2),
72    Alu,
73}
74
75/// One configured CGRA tile.
76#[derive(Clone, Copy, Debug)]
77pub struct Tile {
78    pub alu: Option<CellKind>,
79    pub vs: Drive,
80    pub he: Drive,
81}
82
83impl Tile {
84    /// An idle tile: both channels zero, no compute.
85    pub const IDLE: Tile = Tile {
86        alu: None,
87        vs: Drive::Pass(Src2::Zero),
88        he: Drive::Pass(Src2::Zero),
89    };
90}
91
92/// Which output channel of a tile carries the fabric's result.
93#[derive(Clone, Copy, Debug, PartialEq)]
94pub enum Chan {
95    Vs,
96    He,
97}
98
99/// A placed-and-routed CGRA: a `rows × cols` array of tiles plus the tile/channel that carries the
100/// result. Feed-forward (dataflow goes south/east), so it is a combinational DAG.
101#[derive(Clone, Debug)]
102pub struct Grid {
103    pub rows: usize,
104    pub cols: usize,
105    pub n_in: usize,
106    pub tiles: Vec<Vec<Tile>>,
107    /// The primary output (a single-output fabric's result = `out0`).
108    pub out: (usize, usize, Chan),
109    /// Additional outputs (a loop body has one next-state per accumulator). Total outputs =
110    /// `[out] ++ extra_outs`, named `result` when there is one, else `out0..outK`.
111    pub extra_outs: Vec<(usize, usize, Chan)>,
112}
113
114impl Grid {
115    pub fn new(rows: usize, cols: usize, n_in: usize) -> Grid {
116        Grid {
117            rows,
118            cols,
119            n_in,
120            tiles: vec![vec![Tile::IDLE; cols]; rows],
121            out: (0, 0, Chan::Vs),
122            extra_outs: Vec::new(),
123        }
124    }
125    /// All output taps in order: the primary followed by the extras.
126    pub fn all_outs(&self) -> Vec<(usize, usize, Chan)> {
127        std::iter::once(self.out)
128            .chain(self.extra_outs.iter().copied())
129            .collect()
130    }
131    pub fn set(&mut self, r: usize, c: usize, t: Tile) {
132        self.tiles[r][c] = t;
133    }
134    /// Manhattan wire length = sum over tiles of the channel spans actually used (a rough area/
135    /// routing-cost proxy; every used channel is one nearest-neighbor hop).
136    pub fn used_channels(&self) -> usize {
137        let mut n = 0;
138        for row in &self.tiles {
139            for t in row {
140                if !matches!(t.vs, Drive::Pass(Src2::Zero)) {
141                    n += 1;
142                }
143                if !matches!(t.he, Drive::Pass(Src2::Zero)) {
144                    n += 1;
145                }
146            }
147        }
148        n
149    }
150}
151
152/// Read a `Src2` given a tile's channel inputs and the global inputs (64-bit wrapping).
153fn read2(s: Src2, n_in: i64, w_in: i64, inputs: &[i64]) -> i64 {
154    match s {
155        Src2::N => n_in,
156        Src2::W => w_in,
157        Src2::In(k) => inputs[k],
158        Src2::Const(c) => c,
159        Src2::Zero => 0,
160    }
161}
162
163fn eval_cell(k: CellKind, n_in: i64, w_in: i64, inputs: &[i64]) -> i64 {
164    let r = |s| read2(s, n_in, w_in, inputs);
165    match k {
166        CellKind::Arith(op, a, b) => op.eval(r(a), r(b)),
167        CellKind::Func(unit, a, b) => unit.eval(r(a), r(b)),
168        CellKind::Simd(op, ty, a, b) => op.eval(ty, r(a) as u64, r(b) as u64) as i64,
169        CellKind::Mem(..) => panic!(
170            "eval_cell: a Mem (state-plane) tile needs the shared heap — use eval_grid_with_heap, not the \
171             stateless eval_grid"
172        ),
173        CellKind::Select(cmp, ca, cb, t, e) => {
174            if cmp.eval(r(ca), r(cb)) {
175                r(t)
176            } else {
177                r(e)
178            }
179        }
180    }
181}
182
183/// Simulate the grid (row-major, so every tile's north/west inputs are already resolved — the
184/// feed-forward invariant) and return the result value the output tile/channel carries.
185fn grid_channels(g: &Grid, inputs: &[i64]) -> (Vec<Vec<i64>>, Vec<Vec<i64>>) {
186    let mut vs = vec![vec![0i64; g.cols]; g.rows];
187    let mut he = vec![vec![0i64; g.cols]; g.rows];
188    for r in 0..g.rows {
189        for c in 0..g.cols {
190            let n_in = if r == 0 { 0 } else { vs[r - 1][c] };
191            let w_in = if c == 0 { 0 } else { he[r][c - 1] };
192            let t = g.tiles[r][c];
193            let alu = t.alu.map(|k| eval_cell(k, n_in, w_in, inputs)).unwrap_or(0);
194            let drive = |d: Drive| match d {
195                Drive::Pass(s) => read2(s, n_in, w_in, inputs),
196                Drive::Alu => alu,
197            };
198            vs[r][c] = drive(t.vs);
199            he[r][c] = drive(t.he);
200        }
201    }
202    (vs, he)
203}
204
205fn tap(vs: &[Vec<i64>], he: &[Vec<i64>], o: (usize, usize, Chan)) -> i64 {
206    let (r, c, ch) = o;
207    match ch {
208        Chan::Vs => vs[r][c],
209        Chan::He => he[r][c],
210    }
211}
212
213/// Simulate the grid (feed-forward, row-major) and return the PRIMARY output value.
214pub fn eval_grid(g: &Grid, inputs: &[i64]) -> i64 {
215    let (vs, he) = grid_channels(g, inputs);
216    tap(&vs, &he, g.out)
217}
218
219/// Simulate the grid and return ALL output values in `all_outs` order (for multi-output loop bodies).
220pub fn eval_grid_all(g: &Grid, inputs: &[i64]) -> Vec<i64> {
221    let (vs, he) = grid_channels(g, inputs);
222    g.all_outs().into_iter().map(|o| tap(&vs, &he, o)).collect()
223}
224
225/// Simulate a STATE-PLANE grid — one that carries `Mem` (heap read/write/alloc) tiles — threading ONE shared
226/// heap through the tiles in row-major (= placement = program) order, exactly as the model's `eval_cells`
227/// threads its heap. Non-`Mem` tiles evaluate combinationally (`eval_cell`); a `Mem` tile touches the heap and
228/// drives its result onto its output channel. Returns the primary output. This is how Stage X2 proves
229/// collections/structs/text run on the grid: `eval_grid_with_heap ≡ the whole-program model ≡ the VM`.
230pub fn eval_grid_with_heap(g: &Grid, inputs: &[i64]) -> i64 {
231    let mut heap: Vec<i64> = Vec::new();
232    let (vs, he) = grid_channels_with_heap(g, inputs, &mut heap);
233    tap(&vs, &he, g.out)
234}
235
236/// Simulate a (possibly state-plane) grid, threading a SHARED heap through its `Mem` tiles in row-major
237/// (= placement = program) order — the heap layout that mirrors the VM's `[len, e1..eN]`. Factored so both
238/// the single-output `eval_grid_with_heap` and the multi-output `eval_grid_all_with_heap` share ONE
239/// heap-threading pass; a heap-free grid never touches `heap`, so this equals the stateless `grid_channels`.
240fn grid_channels_with_heap(
241    g: &Grid,
242    inputs: &[i64],
243    heap: &mut Vec<i64>,
244) -> (Vec<Vec<i64>>, Vec<Vec<i64>>) {
245    let mut vs = vec![vec![0i64; g.cols]; g.rows];
246    let mut he = vec![vec![0i64; g.cols]; g.rows];
247    for r in 0..g.rows {
248        for c in 0..g.cols {
249            let n = if r == 0 { 0 } else { vs[r - 1][c] };
250            let w = if c == 0 { 0 } else { he[r][c - 1] };
251            let t = g.tiles[r][c];
252            let alu = match t.alu {
253                Some(CellKind::Mem(op, a, b)) => {
254                    let av = read2(a, n, w, inputs);
255                    match op {
256                        MemOp::HeapRead => heap.get(av.max(0) as usize).copied().unwrap_or(0),
257                        MemOp::HeapWrite => {
258                            let bv = read2(b, n, w, inputs);
259                            let ai = av.max(0) as usize;
260                            if ai >= heap.len() {
261                                heap.resize(ai + 1, 0);
262                            }
263                            heap[ai] = bv;
264                            bv
265                        }
266                        MemOp::HeapAlloc => {
267                            let ptr = heap.len() as i64;
268                            heap.resize(heap.len() + av.max(0) as usize, 0);
269                            ptr
270                        }
271                    }
272                }
273                Some(k) => eval_cell(k, n, w, inputs),
274                None => 0,
275            };
276            vs[r][c] = match t.vs {
277                Drive::Pass(s) => read2(s, n, w, inputs),
278                Drive::Alu => alu,
279            };
280            he[r][c] = match t.he {
281                Drive::Pass(s) => read2(s, n, w, inputs),
282                Drive::Alu => alu,
283            };
284        }
285    }
286    (vs, he)
287}
288
289/// Like [`eval_grid_all`] but threads a SHARED heap (for a whole-program block walk with a state plane):
290/// every `Mem` tile touches `heap` (persisting across blocks), and ALL outputs are returned in `all_outs()`
291/// order so the block-sequencing controller can commit the block's register writes + read its terminator
292/// operand. For a heap-free grid this equals `eval_grid_all` (the heap stays empty).
293pub fn eval_grid_all_with_heap(g: &Grid, inputs: &[i64], heap: &mut Vec<i64>) -> Vec<i64> {
294    let (vs, he) = grid_channels_with_heap(g, inputs, heap);
295    g.all_outs().into_iter().map(|o| tap(&vs, &he, o)).collect()
296}
297
298/// Run a whole (acyclic-control-flow) function on the reconfigurable GRID (Stage X4 — the whole-program XPU).
299/// Each basic block's straight-line body is PLACED on a fresh tile array (`route_loop_body`, the block's
300/// live-in registers as inputs) with its downstream `Src`s as the grid's outputs — every register write, then
301/// the terminator's predicate/return operand; `eval_grid_all` reads them off the placed array, the register
302/// file is updated, and a block-sequencing controller walks the terminator graph (Jump/Branch/Return),
303/// reconfiguring the array to the next block. So a BRANCHY whole program runs as spatial dataflow on ONE array
304/// (each block a reconfiguration), proven ≡ `eval_program_cfg_fabric` ≡ the VM. A block may carry `Mem`
305/// (heap read/write/alloc) tiles: ONE heap is threaded across every reconfiguration (`eval_grid_all_with_heap`),
306/// so collections/structs persist across blocks — the state plane. (Loops are the separate loop fabric.)
307pub fn run_program_cfg_on_grid(pf: &super::fabric_rtl::ProgramCfgFabric, params: &[i64]) -> i64 {
308    use super::fabric_rtl::{Src as FSrc, Term};
309    use std::collections::HashMap;
310    let mut file: HashMap<u16, i64> = (0..pf.n_params).map(|p| (p as u16, params[p])).collect();
311    // ONE shared heap threaded across every block's reconfiguration — the state plane. A stateless program
312    // (no `Mem` tiles) never touches it, so this walk stays identical to the stateless `eval_grid_all` path.
313    let mut heap: Vec<i64> = Vec::new();
314    let mut blk = 0usize;
315    for _ in 0..10_000_000 {
316        let b = &pf.blocks[blk];
317        let inputs: Vec<i64> = b
318            .inputs
319            .iter()
320            .map(|r| *file.get(r).unwrap_or(&0))
321            .collect();
322        // The `Src`s this block needs downstream: each register write's value, then (last) the terminator's
323        // predicate/return operand — placed as the block-grid's outputs and read back by `eval_grid_all`.
324        let mut needed: Vec<FSrc> = b.writes.iter().map(|(_, s)| *s).collect();
325        let term_off = needed.len();
326        match &b.term {
327            Term::Jump(_) => {}
328            Term::Branch { cond, .. } => needed.push(*cond),
329            Term::Return(s) => needed.push(*s),
330        }
331        let g = route_loop_body(&b.body, b.inputs.len(), &needed);
332        let outs = eval_grid_all_with_heap(&g, &inputs, &mut heap);
333        for (i, (r, _)) in b.writes.iter().enumerate() {
334            file.insert(*r, outs[i]);
335        }
336        match &b.term {
337            Term::Jump(t) => blk = *t,
338            Term::Branch {
339                then_blk, else_blk, ..
340            } => {
341                blk = if outs[term_off] != 0 {
342                    *then_blk
343                } else {
344                    *else_blk
345                }
346            }
347            Term::Return(_) => return outs[term_off],
348        }
349    }
350    panic!(
351        "cfg grid: controller did not return within the step budget (a back-edge slipped through?)"
352    )
353}
354
355/// Render a `Src2` in Verilog, given the north/west channel wire names for this tile.
356fn src2_v(s: Src2, n: &str, w: &str) -> String {
357    match s {
358        Src2::N => n.to_string(),
359        Src2::W => w.to_string(),
360        Src2::In(k) => format!("in{k}"),
361        Src2::Const(c) => {
362            if c < 0 {
363                format!("-64'sd{}", (c as i128).unsigned_abs())
364            } else {
365                format!("64'sd{c}")
366            }
367        }
368        Src2::Zero => "64'sd0".to_string(),
369    }
370}
371
372fn cell_v(k: CellKind, n: &str, w: &str) -> String {
373    let s = |x| src2_v(x, n, w);
374    match k {
375        CellKind::Arith(op, a, b) => op.verilog(&s(a), &s(b)),
376        CellKind::Func(unit, a, b) => unit.verilog(&s(a), &s(b)),
377        CellKind::Simd(op, ty, a, b) => op.verilog(ty, &s(a), &s(b)),
378        CellKind::Mem(..) => panic!(
379            "cell_v: Mem-tile RTL (a shared BRAM read/write + a sequencing controller) is the X2b follow-on; \
380             X2a proves the state plane at the eval level via eval_grid_with_heap"
381        ),
382        CellKind::Select(cmp, ca, cb, t, e) => {
383            format!("($signed({}) {} $signed({})) ? {} : {}", s(ca), cmp.verilog(), s(cb), s(t), s(e))
384        }
385    }
386}
387
388/// Emit the tile array as combinational RTL: two channel wires (`vs`/`he`) per tile, each tile
389/// wired only to its north/west neighbours — a genuine nearest-neighbour spatial fabric.
390pub fn emit_grid_verilog(name: &str, g: &Grid) -> String {
391    let inputs = (0..g.n_in)
392        .map(|k| format!("input [63:0] in{k}"))
393        .collect::<Vec<_>>()
394        .join(", ");
395    let inputs = if inputs.is_empty() {
396        String::new()
397    } else {
398        format!("{inputs}, ")
399    };
400    let mut body = String::new();
401    for r in 0..g.rows {
402        for c in 0..g.cols {
403            let nn = if r == 0 {
404                "64'sd0".to_string()
405            } else {
406                format!("vs_{}_{c}", r - 1)
407            };
408            let ww = if c == 0 {
409                "64'sd0".to_string()
410            } else {
411                format!("he_{r}_{}", c - 1)
412            };
413            let t = g.tiles[r][c];
414            if let Some(k) = t.alu {
415                body.push_str(&format!(
416                    "  wire [63:0] alu_{r}_{c} = {};\n",
417                    cell_v(k, &nn, &ww)
418                ));
419            }
420            let drv = |d: Drive| match d {
421                Drive::Pass(s) => src2_v(s, &nn, &ww),
422                Drive::Alu => format!("alu_{r}_{c}"),
423            };
424            body.push_str(&format!("  wire [63:0] vs_{r}_{c} = {};\n", drv(t.vs)));
425            body.push_str(&format!("  wire [63:0] he_{r}_{c} = {};\n", drv(t.he)));
426        }
427    }
428    let outs = g.all_outs();
429    let out_wire = |o: (usize, usize, Chan)| match o.2 {
430        Chan::Vs => format!("vs_{}_{}", o.0, o.1),
431        Chan::He => format!("he_{}_{}", o.0, o.1),
432    };
433    // One output → `result`; several (a loop body's next-state) → `out0..outK`.
434    let (ports, assigns) = if outs.len() == 1 {
435        (
436            "output [63:0] result".to_string(),
437            format!("  assign result = {};\n", out_wire(outs[0])),
438        )
439    } else {
440        let ports = (0..outs.len())
441            .map(|j| format!("output [63:0] out{j}"))
442            .collect::<Vec<_>>()
443            .join(", ");
444        let assigns = outs
445            .iter()
446            .enumerate()
447            .map(|(j, o)| format!("  assign out{j} = {};\n", out_wire(*o)))
448            .collect::<String>();
449        (ports, assigns)
450    };
451    format!(
452        "// grid_{name} — a fabric placed on a {rows}×{cols} CGRA tile array with a two-channel\n\
453         // switchbox interconnect (independent vertical `vs` + horizontal `he` wires per tile,\n\
454         // nearest-neighbour only). Feed-forward south/east, so combinational. The placed+routed\n\
455         // array is certified == the abstract datapath by our own SAT.\n\
456         module grid_{name}({inputs}{ports});\n\
457         {body}{assigns}\
458         endmodule\n",
459        rows = g.rows,
460        cols = g.cols,
461    )
462}
463
464/// The PRIMARY output's `BoundedExpr` (output 0) with the default `in{k}` input names.
465pub fn grid_to_bounded(g: &Grid, width: u32) -> crate::codegen_sva::sva_to_verify::BoundedExpr {
466    grid_output_to_bounded(g, 0, width, &|k| format!("in{k}"))
467}
468
469// ── One datapath vocabulary, two lowerings ──────────────────────────────────────────────
470//
471// A placed grid and the cell body it is supposed to equal are lowered by the SAME walk into two
472// different forms: the `BoundedExpr` tree the SMT/printer surfaces speak, and the shared gate DAG
473// the certifier refutes. Writing that walk twice would be a soundness hole wearing the costume of
474// a style choice — a tile the tree path spells one way and the DAG path another is a mapping we
475// would "prove" against the wrong specification. So there is ONE walk over a `Datapath`
476// vocabulary, and the two forms are its two instances.
477
478/// The datapath vocabulary a placed grid — or a straight-line cell body — is lowered through.
479pub trait Datapath {
480    /// The lowered value: a `BoundedExpr` for the tree form, a `Word` for the shared gate DAG.
481    type V: Clone;
482    /// The global circuit input `k`.
483    fn input(&mut self, k: usize) -> Self::V;
484    /// A constant, truncated to the datapath width.
485    fn constant(&mut self, c: i64) -> Self::V;
486    /// An ALU operation over two lowered operands.
487    fn arith(&mut self, op: AluOp, a: Self::V, b: Self::V) -> Self::V;
488    /// The mux tile: `(ca cmp cb) ? t : e`.
489    fn select(&mut self, cmp: CmpKind, ca: Self::V, cb: Self::V, t: Self::V, e: Self::V)
490        -> Self::V;
491}
492
493/// The bitvector op an `AluOp` blasts as. `Shr` is ARITHMETIC (LOGOS's `shifted right by` is
494/// signed) and `UShr` is the logical one — named once here so no lowering can disagree.
495fn alu_bounded_op(op: AluOp) -> crate::codegen_sva::sva_to_verify::BitVecBoundedOp {
496    use crate::codegen_sva::sva_to_verify::BitVecBoundedOp as B;
497    match op {
498        AluOp::Add => B::Add,
499        AluOp::Sub => B::Sub,
500        AluOp::Mul => B::Mul,
501        AluOp::And => B::And,
502        AluOp::Or => B::Or,
503        AluOp::Xor => B::Xor,
504        AluOp::Shl => B::Shl,
505        AluOp::Shr => B::AShr,
506        AluOp::UShr => B::Shr,
507    }
508}
509
510/// How a `CmpKind` mux resolves into a signed/equality predicate and the ARM ORDER that predicate
511/// selects: `Ge`/`Le`/`Ne` are the complements of `Lt`/`Gt`/`Eq`, spelled by swapping the arms
512/// rather than negating the comparator. One table, read by both lowerings.
513fn select_form(
514    cmp: CmpKind,
515) -> (
516    crate::codegen_sva::sva_to_verify::BitVecBoundedOp,
517    bool,
518    bool,
519) {
520    use crate::codegen_sva::sva_to_verify::BitVecBoundedOp as B;
521    // (predicate op, swap the comparison operands, swap the arms)
522    match cmp {
523        CmpKind::Lt => (B::SLt, false, false),
524        CmpKind::Gt => (B::SLt, true, false),
525        CmpKind::Ge => (B::SLt, false, true),
526        CmpKind::Le => (B::SLt, true, true),
527        CmpKind::Eq => (B::Eq, false, false),
528        CmpKind::Ne => (B::Eq, false, true),
529    }
530}
531
532/// The `BoundedExpr` instance of [`Datapath`] — the tree form.
533pub struct BoundedBuild<'a> {
534    width: u32,
535    name_in: &'a dyn Fn(usize) -> String,
536}
537
538impl<'a> BoundedBuild<'a> {
539    /// Lower at `width`, naming global inputs through `name_in`.
540    pub fn new(width: u32, name_in: &'a dyn Fn(usize) -> String) -> Self {
541        BoundedBuild { width, name_in }
542    }
543}
544
545impl Datapath for BoundedBuild<'_> {
546    type V = crate::codegen_sva::sva_to_verify::BoundedExpr;
547    fn input(&mut self, k: usize) -> Self::V {
548        crate::codegen_sva::sva_to_verify::BoundedExpr::BitVecVar((self.name_in)(k), self.width)
549    }
550    fn constant(&mut self, c: i64) -> Self::V {
551        let mask = if self.width >= 64 {
552            u64::MAX
553        } else {
554            (1u64 << self.width) - 1
555        };
556        crate::codegen_sva::sva_to_verify::BoundedExpr::BitVecConst {
557            width: self.width,
558            value: (c as u64) & mask,
559        }
560    }
561    fn arith(&mut self, op: AluOp, a: Self::V, b: Self::V) -> Self::V {
562        crate::codegen_sva::sva_to_verify::BoundedExpr::BitVecBinary {
563            op: alu_bounded_op(op),
564            left: Box::new(a),
565            right: Box::new(b),
566        }
567    }
568    fn select(
569        &mut self,
570        cmp: CmpKind,
571        ca: Self::V,
572        cb: Self::V,
573        t: Self::V,
574        e: Self::V,
575    ) -> Self::V {
576        use crate::codegen_sva::sva_to_verify::BoundedExpr;
577        let (op, swap_cmp, swap_arms) = select_form(cmp);
578        let (l, r) = if swap_cmp { (cb, ca) } else { (ca, cb) };
579        let (then, els) = if swap_arms { (e, t) } else { (t, e) };
580        BoundedExpr::BitVecIte {
581            cond: Box::new(BoundedExpr::BitVecBinary {
582                op,
583                left: Box::new(l),
584                right: Box::new(r),
585            }),
586            then: Box::new(then),
587            else_: Box::new(els),
588        }
589    }
590}
591
592/// The shared-gate-DAG instance of [`Datapath`] — the form the certifier refutes, linear in the
593/// circuit instead of in its unfolding.
594pub struct GateBuild<'a> {
595    width: u32,
596    name_in: &'a dyn Fn(usize) -> String,
597}
598
599impl<'a> GateBuild<'a> {
600    /// Blast at `width`, naming global inputs through `name_in`.
601    pub fn new(width: u32, name_in: &'a dyn Fn(usize) -> String) -> Self {
602        GateBuild { width, name_in }
603    }
604}
605
606impl Datapath for GateBuild<'_> {
607    type V = crate::codegen_sva::bitblast::Word;
608    fn input(&mut self, k: usize) -> Self::V {
609        crate::codegen_sva::bitblast::word_var(&(self.name_in)(k), self.width)
610    }
611    fn constant(&mut self, c: i64) -> Self::V {
612        crate::codegen_sva::bitblast::word_const(self.width, c as u64)
613    }
614    fn arith(&mut self, op: AluOp, a: Self::V, b: Self::V) -> Self::V {
615        crate::codegen_sva::bitblast::word_binary(alu_bounded_op(op), &a, &b)
616            .expect("every operand of a grid tile carries the grid's one width")
617    }
618    fn select(
619        &mut self,
620        cmp: CmpKind,
621        ca: Self::V,
622        cb: Self::V,
623        t: Self::V,
624        e: Self::V,
625    ) -> Self::V {
626        let (op, swap_cmp, swap_arms) = select_form(cmp);
627        let (l, r) = if swap_cmp { (cb, ca) } else { (ca, cb) };
628        let (then, els) = if swap_arms { (e, t) } else { (t, e) };
629        let pred = crate::codegen_sva::bitblast::word_compare(op, &l, &r)
630            .expect("every operand of a grid tile carries the grid's one width");
631        crate::codegen_sva::bitblast::word_select(&pred, &then, &els)
632            .expect("every operand of a grid tile carries the grid's one width")
633    }
634}
635
636/// The message a tile with no propositional lowering refuses with — one string, so the tree and
637/// DAG lowerings decline identically.
638const NO_BITBLAST_TILE: &str =
639    "grid SAT: a Func (division/float), Mem (state-plane), or Simd (packed) tile has no bitblast \
640     lowering here — such a grid is proven by eval ≡ VM + iverilog, not by our SAT";
641
642/// One tile's two channel outputs `(vs, he)` given its north/west channel inputs. `None` when the
643/// tile has no propositional lowering (`Func`/`Mem`/`Simd`).
644fn tile_outputs<B: Datapath>(b: &mut B, t: Tile, n: &B::V, w: &B::V) -> Option<(B::V, B::V)> {
645    let src2 = |b: &mut B, s: Src2| -> B::V {
646        match s {
647            Src2::N => n.clone(),
648            Src2::W => w.clone(),
649            Src2::In(k) => b.input(k),
650            Src2::Const(c) => b.constant(c),
651            Src2::Zero => b.constant(0),
652        }
653    };
654    let alu = match t.alu {
655        None => None,
656        Some(CellKind::Arith(op, x, y)) => {
657            let (x, y) = (src2(b, x), src2(b, y));
658            Some(b.arith(op, x, y))
659        }
660        Some(CellKind::Select(cmp, ca, cb, tt, ee)) => {
661            let (ca, cb, tt, ee) = (src2(b, ca), src2(b, cb), src2(b, tt), src2(b, ee));
662            Some(b.select(cmp, ca, cb, tt, ee))
663        }
664        Some(CellKind::Func(..)) | Some(CellKind::Mem(..)) | Some(CellKind::Simd(..)) => {
665            return None
666        }
667    };
668    let drive = |b: &mut B, d: Drive| -> B::V {
669        match d {
670            Drive::Pass(s) => src2(b, s),
671            Drive::Alu => alu.clone().expect("Drive::Alu on a tile with no compute"),
672        }
673    };
674    let vs = drive(b, t.vs);
675    let he = drive(b, t.he);
676    Some((vs, he))
677}
678
679/// Lower output `which` (in `all_outs` order) through the datapath vocabulary `b`, resolving the
680/// array row-major so every tile's north/west inputs are already lowered (the feed-forward
681/// invariant). `None` when any live tile has no propositional lowering.
682pub fn grid_output_datapath<B: Datapath>(b: &mut B, g: &Grid, which: usize) -> Option<B::V> {
683    let zero = b.constant(0);
684    let mut vs: Vec<Vec<B::V>> = vec![vec![zero.clone(); g.cols]; g.rows];
685    let mut he: Vec<Vec<B::V>> = vec![vec![zero.clone(); g.cols]; g.rows];
686    for r in 0..g.rows {
687        for c in 0..g.cols {
688            let n = if r == 0 {
689                zero.clone()
690            } else {
691                vs[r - 1][c].clone()
692            };
693            let w = if c == 0 {
694                zero.clone()
695            } else {
696                he[r][c - 1].clone()
697            };
698            let (v, h) = tile_outputs(b, g.tiles[r][c], &n, &w)?;
699            vs[r][c] = v;
700            he[r][c] = h;
701        }
702    }
703    let (or, oc, ch) = g.all_outs()[which];
704    Some(match ch {
705        Chan::Vs => vs[or][oc].clone(),
706        Chan::He => he[or][oc].clone(),
707    })
708}
709
710/// Lower output `which` STRAIGHT INTO THE SHARED GATE DAG at `width` — the certifier's path. Same
711/// walk, same tiles, same circuit as [`grid_output_to_bounded`]; it just never materialises the
712/// tree, which for a reconvergent datapath is exponentially larger than the array that computes
713/// it. `None` when a tile has no propositional lowering.
714pub fn grid_output_to_gates(
715    g: &Grid,
716    which: usize,
717    width: u32,
718    name_in: &dyn Fn(usize) -> String,
719) -> Option<crate::codegen_sva::bitblast::Word> {
720    grid_output_datapath(&mut GateBuild::new(width, name_in), g, which)
721}
722
723/// Lower a straight-line `LoopCell` body through the datapath vocabulary `b` — the placer's own
724/// lowering vocabulary, and the SPECIFICATION side of the placement obligation. `None` for a cell
725/// with no propositional lowering (a functional unit, a memory tile, a packed-SIMD tile).
726pub fn cells_datapath<B: Datapath>(
727    b: &mut B,
728    cells: &[super::fabric_rtl::LoopCell],
729    result: super::fabric_rtl::Src,
730) -> Option<B::V> {
731    use super::fabric_rtl::{LoopCell, Src as FSrc};
732    fn src<B: Datapath>(b: &mut B, s: FSrc, vals: &[B::V]) -> Option<B::V> {
733        Some(match s {
734            FSrc::In(k) => b.input(k),
735            FSrc::Val(k) => vals.get(k)?.clone(),
736            FSrc::Const(c) => b.constant(c),
737        })
738    }
739    let mut vals: Vec<B::V> = Vec::with_capacity(cells.len());
740    for c in cells {
741        let v = match c {
742            LoopCell::Arith { op, a, b: rhs } => {
743                let (x, y) = (src(b, *a, &vals)?, src(b, *rhs, &vals)?);
744                b.arith(*op, x, y)
745            }
746            LoopCell::Select { cmp, ca, cb, t, e } => {
747                let (ca, cb, t, e) = (
748                    src(b, *ca, &vals)?,
749                    src(b, *cb, &vals)?,
750                    src(b, *t, &vals)?,
751                    src(b, *e, &vals)?,
752                );
753                b.select(*cmp, ca, cb, t, e)
754            }
755            _ => return None,
756        };
757        vals.push(v);
758    }
759    src(b, result, &vals)
760}
761
762/// An ALU/select cell body lowered STRAIGHT INTO THE SHARED GATE DAG at `width`. Linear in the
763/// CELLS: a cell read by three later cells is built once and shared by pointer, where the tree
764/// form copies it three times (and their readers copy those, and so on — `fib.lg`'s 75-cell body
765/// unfolds to 1 657 763 tree nodes from a few hundred).
766pub fn cells_to_gates(
767    cells: &[super::fabric_rtl::LoopCell],
768    result: super::fabric_rtl::Src,
769    width: u32,
770) -> Option<crate::codegen_sva::bitblast::Word> {
771    cells_datapath(
772        &mut GateBuild::new(width, &|k| format!("in{k}")),
773        cells,
774        result,
775    )
776}
777
778/// How many operand channels the switchbox must route into ONE tile for `cell`.
779///
780/// A global input or a constant operand is TAPPED at the tile for free (inputs fan out at no
781/// cost); every other operand is another cell's output and has to arrive on one of the tile's two
782/// routed channels — its north or its west. Two ports that read the SAME producer share one
783/// channel, which is why `clamp`'s `If r is greater than 1000` (the comparison's left operand and
784/// the else arm are both `r`) needs ONE channel and routes, while `pick`'s `If bit is 1` (a
785/// computed predicate over two computed arms) needs THREE and does not.
786pub fn tile_channel_demand(cell: &super::fabric_rtl::LoopCell) -> usize {
787    routed_producers(cell).len()
788}
789
790/// A mux tile's operand channels: its north and its west. The interconnect gives each tile two.
791pub const TILE_OPERAND_CHANNELS: usize = 2;
792
793/// The distinct producer cells a tile's operands must have routed to it, in PORT ORDER — the
794/// order the router assigns channels in, so `[0]` takes the north and `[1]` the west.
795fn routed_producers(cell: &super::fabric_rtl::LoopCell) -> Vec<usize> {
796    use super::fabric_rtl::{LoopCell, Src as FSrc};
797    let ports: Vec<super::fabric_rtl::Src> = match *cell {
798        LoopCell::Arith { a, b, .. } | LoopCell::Func { a, b, .. } => vec![a, b],
799        LoopCell::VectorArith { a, b, .. } => vec![a, b],
800        LoopCell::Select { ca, cb, t, e, .. } => vec![ca, cb, t, e],
801        LoopCell::HeapRead { addr } => vec![addr],
802        LoopCell::HeapWrite { addr, val } => vec![addr, val],
803        LoopCell::HeapAlloc { size } => vec![size],
804        LoopCell::MemRead { addr } => vec![addr],
805    };
806    let mut out: Vec<usize> = Vec::new();
807    for p in ports {
808        if let FSrc::Val(j) = p {
809            if !out.contains(&j) {
810                out.push(j);
811            }
812        }
813    }
814    out
815}
816
817/// The first cell of `cells` the router cannot place, as `(cell index, channels it demands)`.
818/// `None` when every cell fits the interconnect. The gate a certifier consults BEFORE calling
819/// [`route_loop_body`], so an unplaceable body is declined by name instead of panicking inside
820/// the placer.
821pub fn unroutable_cell(cells: &[super::fabric_rtl::LoopCell]) -> Option<(usize, usize)> {
822    cells.iter().enumerate().find_map(|(i, c)| {
823        let d = tile_channel_demand(c);
824        (d > TILE_OPERAND_CHANNELS).then_some((i, d))
825    })
826}
827
828/// Lower output `which` (in `all_outs` order) to a `BoundedExpr` at `width`, naming global inputs
829/// via `name_in` — so the placed+routed array can be proven EQUAL to a reference expressed in the
830/// SAME variable namespace (e.g. a loop body's `s0/iv/inv0`), exhaustively.
831pub fn grid_output_to_bounded(
832    g: &Grid,
833    which: usize,
834    width: u32,
835    name_in: &dyn Fn(usize) -> String,
836) -> crate::codegen_sva::sva_to_verify::BoundedExpr {
837    grid_output_datapath(&mut BoundedBuild::new(width, name_in), g, which).expect(NO_BITBLAST_TILE)
838}
839
840/// **The placer + router.** Map a straight-line fabric (`FabricOp` SSA, from the real compiled
841/// residual) onto the tile array. Placement: cell `i` computes on the diagonal at `(2(i+1), i)` —
842/// its own column `i` carries its result downward. Routing: an operand that is a global input or
843/// constant is TAPPED directly (inputs fan out for free); an operand that is another cell's value
844/// is routed via a Manhattan path — forked EAST off its column (`he←N`, which coexists with the
845/// column's downward `vs←N`, so fan-out costs nothing) and turned back into the consumer's north
846/// (`vs←W`) or read from its west. Correct by construction; the S3 SAT proof catches any slip.
847pub fn route_straight_line(
848    cells: &[super::fabric_rtl::FabricOp],
849    n_in: usize,
850    result: usize,
851) -> Grid {
852    use super::fabric_rtl::Src as FSrc;
853    let m = cells.len().max(1);
854    let cols = m;
855    let rows = 2 * m + 2;
856    let r_of = |i: usize| 2 * (i + 1);
857    let mut g = Grid::new(rows, cols, n_in);
858
859    // 1) Place the compute tiles + start each result flowing down its own column.
860    let operand = |s: FSrc| -> (Src2, Option<usize>) {
861        match s {
862            FSrc::In(k) => (Src2::In(k), None),
863            FSrc::Const(c) => (Src2::Const(c), None),
864            FSrc::Val(j) => (Src2::Zero, Some(j)), // placeholder src; filled by the route below
865        }
866    };
867    for (i, cell) in cells.iter().enumerate() {
868        let (ri, ci) = (r_of(i), i);
869        let (mut sa, route_a) = operand(cell.a);
870        let (mut sb, route_b) = operand(cell.b);
871        // A routed operand arrives on the north (A) / west (B) channel of the compute tile.
872        if route_a.is_some() {
873            sa = Src2::N;
874        }
875        if route_b.is_some() {
876            sb = Src2::W;
877        }
878        g.tiles[ri][ci].alu = Some(CellKind::Arith(cell.op, sa, sb));
879        g.tiles[ri][ci].vs = Drive::Alu;
880
881        // 2) Route operand A into the tile NORTH of the cell (row ri-1, col ci):
882        //    fork cell `a`'s value east off column `a` at row ri-1, run it to column ci, turn south.
883        if let Some(a) = route_a {
884            let rr = ri - 1;
885            g.tiles[rr][a].he = Drive::Pass(Src2::N); // fork east off column a (coexists with vs↓)
886            for c in a + 1..ci {
887                g.tiles[rr][c].he = Drive::Pass(Src2::W); // run east
888            }
889            g.tiles[rr][ci].vs = Drive::Pass(Src2::W); // turn south into the cell's north input
890        }
891        // 3) Route operand B into the tile WEST of the cell (row ri, col ci-1): fork east + run.
892        if let Some(b) = route_b {
893            let rr = ri;
894            g.tiles[rr][b].he = Drive::Pass(Src2::N);
895            for c in b + 1..ci {
896                g.tiles[rr][c].he = Drive::Pass(Src2::W);
897            }
898        }
899    }
900
901    // 4) Every cell's value flows DOWN its column so later (lower) cells can fork it east.
902    for i in 0..cells.len() {
903        let (ri, ci) = (r_of(i), i);
904        for r in ri + 1..rows {
905            // Preserve any he route already placed here; only (re)assert the vertical channel.
906            if matches!(g.tiles[r][ci].vs, Drive::Pass(Src2::Zero)) {
907                g.tiles[r][ci].vs = Drive::Pass(Src2::N);
908            }
909        }
910    }
911
912    g.out = (r_of(result), result, Chan::Vs);
913    g
914}
915
916/// Route a producer cell's value into the tile NORTH of a consumer at `(ri, ci)`: fork east off
917/// the producer's column at row `ri-1`, run east, and turn south into the consumer's north input.
918fn route_north(g: &mut Grid, from: Option<usize>, ci: usize, ri: usize) {
919    if let Some(a) = from {
920        let rr = ri - 1;
921        g.tiles[rr][a].he = Drive::Pass(Src2::N);
922        for c in a + 1..ci {
923            g.tiles[rr][c].he = Drive::Pass(Src2::W);
924        }
925        g.tiles[rr][ci].vs = Drive::Pass(Src2::W);
926    }
927}
928
929/// Route a producer cell's value onto the WEST input of a consumer at `(ri, ci)`: fork east off the
930/// producer's column at row `ri`, run east to the consumer's west neighbour.
931fn route_west(g: &mut Grid, from: Option<usize>, ci: usize, ri: usize) {
932    if let Some(b) = from {
933        let rr = ri;
934        g.tiles[rr][b].he = Drive::Pass(Src2::N);
935        for c in b + 1..ci {
936            g.tiles[rr][c].he = Drive::Pass(Src2::W);
937        }
938    }
939}
940
941/// **Map a LOOP BODY onto the tile array.** Same diagonal placement + Manhattan routing as the
942/// straight-line case, but each `LoopCell` may be a `Select` (a mux tile — the grid already models
943/// it), and the body produces MULTIPLE outputs (one per loop-carried next-state register, in
944/// `next` order). Global-input / constant operands are tapped directly; cell-value operands route
945/// (each distinct producer takes one of the tile's two channels — a computed PREDICATE routes
946/// exactly like a computed data arm, and two ports reading one producer share a channel). This
947/// grid is the combinational BODY datapath of a loop-on-a-grid — the controller (counter / fabric
948/// predicate) latches its outputs, exactly as with the hand-wired body.
949pub fn route_loop_body(
950    cells: &[super::fabric_rtl::LoopCell],
951    n_in: usize,
952    next: &[super::fabric_rtl::Src],
953) -> Grid {
954    use super::fabric_rtl::{LoopCell, Src as FSrc};
955    let m = cells.len().max(1);
956    let n_src_out = next.iter().filter(|s| !matches!(s, FSrc::Val(_))).count();
957    let cols = m + n_src_out.max(0);
958    let rows = 2 * m + 2;
959    let r_of = |i: usize| 2 * (i + 1);
960    let mut g = Grid::new(rows, cols.max(1), n_in);
961    let operand = |s: FSrc| -> (Src2, Option<usize>) {
962        match s {
963            FSrc::In(k) => (Src2::In(k), None),
964            FSrc::Const(c) => (Src2::Const(c), None),
965            FSrc::Val(j) => (Src2::Zero, Some(j)),
966        }
967    };
968    for (i, cell) in cells.iter().enumerate() {
969        let (ri, ci) = (r_of(i), i);
970        match *cell {
971            LoopCell::Arith { op, a, b } => {
972                let (mut sa, ra) = operand(a);
973                let (mut sb, rb) = operand(b);
974                if ra.is_some() {
975                    sa = Src2::N;
976                }
977                if rb.is_some() {
978                    sb = Src2::W;
979                }
980                g.tiles[ri][ci].alu = Some(CellKind::Arith(op, sa, sb));
981                g.tiles[ri][ci].vs = Drive::Alu;
982                route_north(&mut g, ra, ci, ri);
983                route_west(&mut g, rb, ci, ri);
984            }
985            // A FUNCTIONAL-UNIT tile (division/mod/floor-div/pow/magic-high-mul/float) — placed exactly like an
986            // Arith tile (its two operands route to north/west), but the tile drives a `FuncUnit` compute. The
987            // emitted RTL is the FuncUnit's combinational form (`emit_decision_fabric_verilog`'s divider `a/b`),
988            // so it settles in the same 1-clock pass; proven ≡ VM by eval + iverilog (NOT SAT — no `bvsdiv`).
989            LoopCell::Func { unit, a, b } => {
990                let (mut sa, ra) = operand(a);
991                let (mut sb, rb) = operand(b);
992                if ra.is_some() {
993                    sa = Src2::N;
994                }
995                if rb.is_some() {
996                    sb = Src2::W;
997                }
998                g.tiles[ri][ci].alu = Some(CellKind::Func(unit, sa, sb));
999                g.tiles[ri][ci].vs = Drive::Alu;
1000                route_north(&mut g, ra, ci, ri);
1001                route_west(&mut g, rb, ci, ri);
1002            }
1003            // A packed-SIMD tile — placed exactly like an Arith tile (its two operands route north/west), but
1004            // the tile drives a `Simd` compute (lane-wise add/bitwise). Combinational; proven ≡ lane-wise + VM
1005            // by eval + iverilog (like `Func`, not SAT).
1006            LoopCell::VectorArith { op, ty, a, b } => {
1007                let (mut sa, ra) = operand(a);
1008                let (mut sb, rb) = operand(b);
1009                if ra.is_some() {
1010                    sa = Src2::N;
1011                }
1012                if rb.is_some() {
1013                    sb = Src2::W;
1014                }
1015                g.tiles[ri][ci].alu = Some(CellKind::Simd(op, ty, sa, sb));
1016                g.tiles[ri][ci].vs = Drive::Alu;
1017                route_north(&mut g, ra, ci, ri);
1018                route_west(&mut g, rb, ci, ri);
1019            }
1020            // STATE-PLANE tiles (Stage X2): heap read/write/alloc against ONE shared heap. Placed like an
1021            // Arith tile (operands route to north/west), but the tile drives a `Mem` compute that touches the
1022            // shared heap. Proven by `eval_grid_with_heap ≡ the model ≡ VM` (stateful → NOT SAT, NOT the
1023            // stateless eval_grid); the BRAM+controller RTL is the X2b follow-on (cell_v panics).
1024            LoopCell::HeapRead { addr } => {
1025                let (mut sa, ra) = operand(addr);
1026                if ra.is_some() {
1027                    sa = Src2::N;
1028                }
1029                g.tiles[ri][ci].alu = Some(CellKind::Mem(MemOp::HeapRead, sa, Src2::Zero));
1030                g.tiles[ri][ci].vs = Drive::Alu;
1031                route_north(&mut g, ra, ci, ri);
1032            }
1033            LoopCell::HeapWrite { addr, val } => {
1034                let (mut sa, ra) = operand(addr);
1035                let (mut sb, rb) = operand(val);
1036                if ra.is_some() {
1037                    sa = Src2::N;
1038                }
1039                if rb.is_some() {
1040                    sb = Src2::W;
1041                }
1042                g.tiles[ri][ci].alu = Some(CellKind::Mem(MemOp::HeapWrite, sa, sb));
1043                g.tiles[ri][ci].vs = Drive::Alu;
1044                route_north(&mut g, ra, ci, ri);
1045                route_west(&mut g, rb, ci, ri);
1046            }
1047            LoopCell::HeapAlloc { size } => {
1048                let (mut sa, ra) = operand(size);
1049                if ra.is_some() {
1050                    sa = Src2::N;
1051                }
1052                g.tiles[ri][ci].alu = Some(CellKind::Mem(MemOp::HeapAlloc, sa, Src2::Zero));
1053                g.tiles[ri][ci].vs = Drive::Alu;
1054                route_north(&mut g, ra, ci, ri);
1055            }
1056            // A loop-SEQUENCE read (the P2 scratchpad, induction-counter-addressed) is a distinct mechanism —
1057            // proven via eval_loop_fabric_seq, not placed as a state-plane tile here.
1058            LoopCell::MemRead { .. } => panic!(
1059                "grid: a loop-sequence MemRead is the induction-counter scratchpad (P2, eval_loop_fabric_seq), \
1060                 distinct from the Stage-X2 shared-heap state plane"
1061            ),
1062            // A mux tile reads FOUR operands, but the switchbox gives every tile the same TWO
1063            // routed channels (its north and its west). An input or a constant is tapped at the
1064            // tile for free; a COMPUTED operand — including a computed PREDICATE, which is an
1065            // ordinary dataflow edge and gets the ordinary treatment — must land on a channel,
1066            // and two ports reading the same producer share one. `unroutable_cell` prices this
1067            // from the cells before the placer is called, so a body that demands a third channel
1068            // is declined by name rather than caught here.
1069            LoopCell::Select { cmp, ca, cb, t, e } => {
1070                let routed = routed_producers(cell);
1071                assert!(
1072                    routed.len() <= TILE_OPERAND_CHANNELS,
1073                    "grid: a tile has {TILE_OPERAND_CHANNELS} operand channels (north and west) \
1074                     but this mux needs {} distinct computed values routed to it — check \
1075                     `unroutable_cell` before placing",
1076                    routed.len()
1077                );
1078                let chan = |s: FSrc| -> Src2 {
1079                    match s {
1080                        FSrc::In(k) => Src2::In(k),
1081                        FSrc::Const(c) => Src2::Const(c),
1082                        FSrc::Val(j) => {
1083                            if routed[0] == j {
1084                                Src2::N
1085                            } else {
1086                                Src2::W
1087                            }
1088                        }
1089                    }
1090                };
1091                g.tiles[ri][ci].alu =
1092                    Some(CellKind::Select(cmp, chan(ca), chan(cb), chan(t), chan(e)));
1093                g.tiles[ri][ci].vs = Drive::Alu;
1094                route_north(&mut g, routed.first().copied(), ci, ri);
1095                route_west(&mut g, routed.get(1).copied(), ci, ri);
1096            }
1097        }
1098    }
1099    for i in 0..cells.len() {
1100        let (ri, ci) = (r_of(i), i);
1101        for r in ri + 1..rows {
1102            if matches!(g.tiles[r][ci].vs, Drive::Pass(Src2::Zero)) {
1103                g.tiles[r][ci].vs = Drive::Pass(Src2::N);
1104            }
1105        }
1106    }
1107    // Outputs: one per next-state register. A `Val` reads its cell's tile; a pass-through state
1108    // (`In`/`Const`) gets a source tile in a spare column.
1109    let mut extra_col = m;
1110    let mut outs: Vec<(usize, usize, Chan)> = Vec::new();
1111    for s in next {
1112        match s {
1113            FSrc::Val(k) => outs.push((r_of(*k), *k, Chan::Vs)),
1114            FSrc::In(k) => {
1115                g.tiles[0][extra_col].vs = Drive::Pass(Src2::In(*k));
1116                outs.push((0, extra_col, Chan::Vs));
1117                extra_col += 1;
1118            }
1119            FSrc::Const(c) => {
1120                g.tiles[0][extra_col].vs = Drive::Pass(Src2::Const(*c));
1121                outs.push((0, extra_col, Chan::Vs));
1122                extra_col += 1;
1123            }
1124        }
1125    }
1126    // A SIDE-EFFECT-ONLY block (a heap write with no register write and an unconditional Jump) has no output
1127    // tap. The body's `Mem` tiles still execute (every tile is evaluated), but the `Grid` needs a primary
1128    // output; a dummy tap on the idle tile (0,0) suffices — the caller has no register write or branch cond
1129    // to read, so the value is never consumed. Non-empty `next` is unaffected.
1130    if outs.is_empty() {
1131        outs.push((0, 0, Chan::Vs));
1132    }
1133    g.out = outs[0];
1134    g.extra_outs = outs[1..].to_vec();
1135    g
1136}
1137
1138// ── Time-multiplexing: one physical array runs MULTIPLE fabrics by reconfiguration ─────────────
1139
1140/// The BoundedExpr for a single tile's two channel outputs `(vs, he)`, given its north/west channel
1141/// inputs — the shared per-tile evaluation used by both the single-grid and time-muxed lowerings.
1142fn tile_bounded(
1143    t: Tile,
1144    n: &crate::codegen_sva::sva_to_verify::BoundedExpr,
1145    w: &crate::codegen_sva::sva_to_verify::BoundedExpr,
1146    width: u32,
1147    name_in: &dyn Fn(usize) -> String,
1148) -> (
1149    crate::codegen_sva::sva_to_verify::BoundedExpr,
1150    crate::codegen_sva::sva_to_verify::BoundedExpr,
1151) {
1152    tile_outputs(&mut BoundedBuild::new(width, name_in), t, n, w).expect(NO_BITBLAST_TILE)
1153}
1154
1155/// The combined output of a TIME-MULTIPLEXED array: one physical grid whose every tile is muxed by
1156/// a `ctx` selector between fabric `a`'s config (ctx=1) and fabric `b`'s config (ctx=0). Because
1157/// the neighbour channels are themselves ctx-muxed, when ctx=1 the whole array carries `a`'s
1158/// dataflow and when ctx=0 it carries `b`'s — proven in the P3 test. `a` and `b` must share dims.
1159pub fn time_mux_bounded(
1160    a: &Grid,
1161    b: &Grid,
1162    width: u32,
1163) -> crate::codegen_sva::sva_to_verify::BoundedExpr {
1164    use crate::codegen_sva::sva_to_verify::{BitVecBoundedOp, BoundedExpr};
1165    assert!(
1166        a.rows == b.rows && a.cols == b.cols,
1167        "time-mux requires equal grid dimensions"
1168    );
1169    let name_in = |k: usize| format!("in{k}");
1170    let konst = |c: i64| BoundedExpr::BitVecConst {
1171        width,
1172        value: c as u64,
1173    };
1174    let is_a = BoundedExpr::BitVecBinary {
1175        op: BitVecBoundedOp::Eq,
1176        left: Box::new(BoundedExpr::BitVecVar("ctx".to_string(), 1)),
1177        right: Box::new(BoundedExpr::BitVecConst { width: 1, value: 1 }),
1178    };
1179    let mux = |va: BoundedExpr, vb: BoundedExpr| BoundedExpr::BitVecIte {
1180        cond: Box::new(is_a.clone()),
1181        then: Box::new(va),
1182        else_: Box::new(vb),
1183    };
1184    let zero = konst(0);
1185    let mut vs = vec![vec![zero.clone(); a.cols]; a.rows];
1186    let mut he = vec![vec![zero.clone(); a.cols]; a.rows];
1187    for r in 0..a.rows {
1188        for c in 0..a.cols {
1189            let n = if r == 0 {
1190                zero.clone()
1191            } else {
1192                vs[r - 1][c].clone()
1193            };
1194            let w = if c == 0 {
1195                zero.clone()
1196            } else {
1197                he[r][c - 1].clone()
1198            };
1199            let (a_vs, a_he) = tile_bounded(a.tiles[r][c], &n, &w, width, &name_in);
1200            let (b_vs, b_he) = tile_bounded(b.tiles[r][c], &n, &w, width, &name_in);
1201            vs[r][c] = mux(a_vs, b_vs);
1202            he[r][c] = mux(a_he, b_he);
1203        }
1204    }
1205    let tap = |o: (usize, usize, Chan)| match o.2 {
1206        Chan::Vs => vs[o.0][o.1].clone(),
1207        Chan::He => he[o.0][o.1].clone(),
1208    };
1209    mux(tap(a.out), tap(b.out))
1210}
1211
1212/// The RECONFIGURABLE array (Phase 5): ONE tile grid runs ANY of `grids` selected by a CONFIG WORD — the
1213/// generalization of [`time_mux_bounded`] from a 1-bit `ctx` (2 fabrics) to a `cfg_width`-bit word (N
1214/// fabrics). Every tile computes all N fabrics' drives over the SHARED muxed neighbour channels, then
1215/// selects by `config`; the output tap muxes likewise. This is "the fabric IS the interpreter": the config
1216/// word (streamed per block from the `LGB1` frame) reconfigures the SAME silicon to be any block's
1217/// datapath. `config` is fixed to a concrete value here so the lock proves, per value `k`, that the
1218/// reconfigured array is EXACTLY the bespoke island `grids[k]` (`config == k ⇒ array ≡ grids[k]`). All
1219/// grids must share dimensions (same-shape fabrics place identically).
1220pub fn time_mux_n_bounded(
1221    grids: &[&Grid],
1222    config: u64,
1223    width: u32,
1224    cfg_width: u32,
1225) -> crate::codegen_sva::sva_to_verify::BoundedExpr {
1226    use crate::codegen_sva::sva_to_verify::{BitVecBoundedOp, BoundedExpr};
1227    assert!(!grids.is_empty(), "time-mux-n needs at least one fabric");
1228    let rows = grids[0].rows;
1229    let cols = grids[0].cols;
1230    for g in grids {
1231        assert!(
1232            g.rows == rows && g.cols == cols,
1233            "time-mux-n requires equal grid dimensions"
1234        );
1235    }
1236    let name_in = |k: usize| format!("in{k}");
1237    let konst = |c: i64| BoundedExpr::BitVecConst {
1238        width,
1239        value: c as u64,
1240    };
1241    // `config == k`, with `config` a concrete constant (the proof fixes it).
1242    let cfg_is = |k: usize| BoundedExpr::BitVecBinary {
1243        op: BitVecBoundedOp::Eq,
1244        left: Box::new(BoundedExpr::BitVecConst {
1245            width: cfg_width,
1246            value: config,
1247        }),
1248        right: Box::new(BoundedExpr::BitVecConst {
1249            width: cfg_width,
1250            value: k as u64,
1251        }),
1252    };
1253    // A right-folded N-way mux: `config==0 ? v0 : config==1 ? v1 : … : v_{last}`.
1254    let mux_n = |vs: &[BoundedExpr]| -> BoundedExpr {
1255        let mut acc = vs[vs.len() - 1].clone();
1256        for k in (0..vs.len() - 1).rev() {
1257            acc = BoundedExpr::BitVecIte {
1258                cond: Box::new(cfg_is(k)),
1259                then: Box::new(vs[k].clone()),
1260                else_: Box::new(acc),
1261            };
1262        }
1263        acc
1264    };
1265    let zero = konst(0);
1266    let mut vs = vec![vec![zero.clone(); cols]; rows];
1267    let mut he = vec![vec![zero.clone(); cols]; rows];
1268    for r in 0..rows {
1269        for c in 0..cols {
1270            let n = if r == 0 {
1271                zero.clone()
1272            } else {
1273                vs[r - 1][c].clone()
1274            };
1275            let w = if c == 0 {
1276                zero.clone()
1277            } else {
1278                he[r][c - 1].clone()
1279            };
1280            let mut dvs = Vec::with_capacity(grids.len());
1281            let mut dhe = Vec::with_capacity(grids.len());
1282            for g in grids {
1283                let (gvs, ghe) = tile_bounded(g.tiles[r][c], &n, &w, width, &name_in);
1284                dvs.push(gvs);
1285                dhe.push(ghe);
1286            }
1287            vs[r][c] = mux_n(&dvs);
1288            he[r][c] = mux_n(&dhe);
1289        }
1290    }
1291    let tap = |o: (usize, usize, Chan)| match o.2 {
1292        Chan::Vs => vs[o.0][o.1].clone(),
1293        Chan::He => he[o.0][o.1].clone(),
1294    };
1295    let taps: Vec<BoundedExpr> = grids.iter().map(|g| tap(g.out)).collect();
1296    mux_n(&taps)
1297}
1298
1299/// Certify the reconfigurable array END TO END (Phase 5): build one grid per source function (they must be
1300/// same-shape residuals so they place identically), then for EACH config value `k` prove via our certified
1301/// SAT that `config == k` reconfigures the ONE array to be EXACTLY the bespoke island `grids[k]`, for ALL
1302/// inputs. Returns the per-config verdicts — all `true` ⇒ the config word (streamed per block from the
1303/// bytecode frame) losslessly selects any block's datapath on one physical array: "the fabric IS the
1304/// interpreter". Each source is `(program, fn_name)` with a straight-line residual body.
1305pub fn certify_reconfig(sources: &[(&str, &str)], width: u32, cfg_width: u32) -> Vec<bool> {
1306    use crate::codegen_sva::bitblast::lower_bool;
1307    use crate::codegen_sva::fabric_rtl::residual_from_source;
1308    use crate::codegen_sva::sva_to_verify::BoundedExpr;
1309    use logicaffeine_proof::sat::{prove_unsat, UnsatOutcome};
1310    use logicaffeine_proof::ProofExpr;
1311
1312    let grids: Vec<Grid> = sources
1313        .iter()
1314        .map(|(src, name)| {
1315            let (ops, n_in, res) = residual_from_source(src, name);
1316            route_straight_line(&ops, n_in, res)
1317        })
1318        .collect();
1319    let (r0, c0) = (grids[0].rows, grids[0].cols);
1320    for g in &grids {
1321        assert!(
1322            g.rows == r0 && g.cols == c0,
1323            "reconfig: all fabrics must place to the same array dimensions (use same-shape residuals)"
1324        );
1325    }
1326    let refs: Vec<&Grid> = grids.iter().collect();
1327    (0..grids.len())
1328        .map(|k| {
1329            let combined = time_mux_n_bounded(&refs, k as u64, width, cfg_width);
1330            let island = grid_to_bounded(&grids[k], width);
1331            // config == k  ⇒  array ≡ island k, for ALL inputs: prove ¬(array == island) is UNSAT.
1332            let eq = lower_bool(&BoundedExpr::Eq(Box::new(combined), Box::new(island)))
1333                .expect("bitvec equality lowers to a boolean");
1334            prove_unsat(&ProofExpr::Not(Box::new(eq))) == UnsatOutcome::Refuted
1335        })
1336        .collect()
1337}
1338
1339/// Pad a placed grid to a larger `target_rows × target_cols` array: copy its tiles into the TOP-LEFT
1340/// corner and leave the rest IDLE. This RESOLVES the same-shape constraint the reconfigurable-array proof
1341/// needs (`time_mux_n_bounded` requires equal dimensions) — grids of DIFFERENT shapes co-reside on ONE
1342/// array without touching any datapath. The output taps keep their positions (valid since the array only
1343/// GREW), and the added IDLE tiles drive zero; because dataflow is feed-forward (south/east only), no
1344/// downstream idle tile ever feeds an upstream one, so the padded grid computes EXACTLY the original
1345/// (`eval_grid(pad_grid(g, R, C)) == eval_grid(g)`). Panics if asked to shrink.
1346pub fn pad_grid(g: &Grid, target_rows: usize, target_cols: usize) -> Grid {
1347    assert!(
1348        target_rows >= g.rows && target_cols >= g.cols,
1349        "pad_grid only grows an array (target {target_rows}x{target_cols} < {}x{})",
1350        g.rows,
1351        g.cols
1352    );
1353    let mut p = Grid::new(target_rows, target_cols, g.n_in);
1354    for r in 0..g.rows {
1355        for c in 0..g.cols {
1356            p.tiles[r][c] = g.tiles[r][c];
1357        }
1358    }
1359    p.out = g.out;
1360    p.extra_outs = g.extra_outs.clone();
1361    p
1362}
1363
1364/// True iff every placed tile is `Arith`/`Select` (or pure routing) — i.e. the grid is fully COMBINATIONAL
1365/// and SAT-lowerable. A `Func` (division/float) tile has no bitblast lowering, so such a grid is proven by
1366/// eval ≡ VM + iverilog, not by `certify_reconfig`/`certify_config_table`; those refuse a non-combinational
1367/// grid rather than panic opaquely inside the SAT lowering.
1368pub fn grid_is_combinational(g: &Grid) -> bool {
1369    g.tiles.iter().flatten().all(|t| {
1370        !matches!(
1371            t.alu,
1372            Some(CellKind::Func(..)) | Some(CellKind::Mem(..)) | Some(CellKind::Simd(..))
1373        )
1374    })
1375}
1376
1377/// Emit the time-multiplexed array as combinational RTL: every tile computes BOTH fabrics' drives
1378/// and muxes each channel on `ctx`; the result muxes the two fabrics' output taps. One silicon
1379/// array, two programs — selected by a configuration bit.
1380pub fn emit_time_multiplexed(name: &str, a: &Grid, b: &Grid) -> String {
1381    assert!(
1382        a.rows == b.rows && a.cols == b.cols,
1383        "time-mux requires equal grid dimensions"
1384    );
1385    let n_in = a.n_in.max(b.n_in);
1386    let inputs = (0..n_in)
1387        .map(|k| format!("input [63:0] in{k}"))
1388        .collect::<Vec<_>>()
1389        .join(", ");
1390    let inputs = if inputs.is_empty() {
1391        String::new()
1392    } else {
1393        format!("{inputs}, ")
1394    };
1395    let mut body = String::new();
1396    for r in 0..a.rows {
1397        for c in 0..a.cols {
1398            let nn = if r == 0 {
1399                "64'sd0".to_string()
1400            } else {
1401                format!("vs_{}_{c}", r - 1)
1402            };
1403            let ww = if c == 0 {
1404                "64'sd0".to_string()
1405            } else {
1406                format!("he_{r}_{}", c - 1)
1407            };
1408            for (tag, g) in [("a", a), ("b", b)] {
1409                let t = g.tiles[r][c];
1410                if let Some(k) = t.alu {
1411                    body.push_str(&format!(
1412                        "  wire [63:0] alu{tag}_{r}_{c} = {};\n",
1413                        cell_v(k, &nn, &ww)
1414                    ));
1415                }
1416                let drv = |d: Drive| match d {
1417                    Drive::Pass(s) => src2_v(s, &nn, &ww),
1418                    Drive::Alu => format!("alu{tag}_{r}_{c}"),
1419                };
1420                body.push_str(&format!("  wire [63:0] vs{tag}_{r}_{c} = {};\n", drv(t.vs)));
1421                body.push_str(&format!("  wire [63:0] he{tag}_{r}_{c} = {};\n", drv(t.he)));
1422            }
1423            body.push_str(&format!(
1424                "  wire [63:0] vs_{r}_{c} = ctx ? vsa_{r}_{c} : vsb_{r}_{c};\n"
1425            ));
1426            body.push_str(&format!(
1427                "  wire [63:0] he_{r}_{c} = ctx ? hea_{r}_{c} : heb_{r}_{c};\n"
1428            ));
1429        }
1430    }
1431    let out_wire = |g: &Grid| match g.out.2 {
1432        Chan::Vs => format!("vs_{}_{}", g.out.0, g.out.1),
1433        Chan::He => format!("he_{}_{}", g.out.0, g.out.1),
1434    };
1435    format!(
1436        "// tmux_{name} — ONE {rows}×{cols} CGRA tile array running TWO fabrics by reconfiguration:\n\
1437         // every tile is muxed on `ctx` between fabric A (ctx=1) and fabric B (ctx=0). Certified by\n\
1438         // our SAT: ctx=1 ⇒ the array ≡ fabric A, ctx=0 ⇒ ≡ fabric B.\n\
1439         module tmux_{name}({inputs}input ctx, output [63:0] result);\n\
1440         {body}  assign result = ctx ? {oa} : {ob};\n\
1441         endmodule\n",
1442        rows = a.rows,
1443        cols = a.cols,
1444        oa = out_wire(a),
1445        ob = out_wire(b),
1446    )
1447}
1448
1449/// Emit the N-CONFIG reconfigurable array as combinational RTL — the synthesizable twin of
1450/// [`time_mux_n_bounded`]. ONE `rows×cols` tile grid runs ANY of `grids` selected by a `cfg`-bit config word:
1451/// every tile computes all N fabrics' drives over the SHARED (config-muxed) neighbour channels and selects by
1452/// `cfg`; the result muxes the N output taps. `grids` must share dimensions (pad with [`pad_grid`] first).
1453/// This is what a `fabric_soc` flashes: send `cfg` (the block index from the bytecode frame) + the inputs and
1454/// the ONE array becomes that block's datapath — proven semantics-preserving by `certify_config_table`.
1455pub fn emit_config_grid_verilog(name: &str, grids: &[&Grid], cfg_width: u32) -> String {
1456    assert!(!grids.is_empty(), "config grid needs at least one fabric");
1457    let rows = grids[0].rows;
1458    let cols = grids[0].cols;
1459    for g in grids {
1460        assert!(
1461            g.rows == rows && g.cols == cols,
1462            "config grid requires equal dims (pad_grid first)"
1463        );
1464    }
1465    let n_in = grids.iter().map(|g| g.n_in).max().unwrap();
1466    let inputs = (0..n_in)
1467        .map(|k| format!("input [63:0] in{k}"))
1468        .collect::<Vec<_>>()
1469        .join(", ");
1470    let inputs = if inputs.is_empty() {
1471        String::new()
1472    } else {
1473        format!("{inputs}, ")
1474    };
1475    // A right-folded N-way mux: `cfg==0 ? v0 : cfg==1 ? v1 : … : v_{last}`.
1476    let mux_n = |vals: &[String]| -> String {
1477        let mut acc = vals[vals.len() - 1].clone();
1478        for k in (0..vals.len() - 1).rev() {
1479            acc = format!("(cfg == {cfg_width}'d{k}) ? {} : ({acc})", vals[k]);
1480        }
1481        acc
1482    };
1483    let mut body = String::new();
1484    for r in 0..rows {
1485        for c in 0..cols {
1486            let nn = if r == 0 {
1487                "64'sd0".to_string()
1488            } else {
1489                format!("vs_{}_{c}", r - 1)
1490            };
1491            let ww = if c == 0 {
1492                "64'sd0".to_string()
1493            } else {
1494                format!("he_{r}_{}", c - 1)
1495            };
1496            let mut vsk = Vec::with_capacity(grids.len());
1497            let mut hek = Vec::with_capacity(grids.len());
1498            for (gi, g) in grids.iter().enumerate() {
1499                let t = g.tiles[r][c];
1500                if let Some(k) = t.alu {
1501                    body.push_str(&format!(
1502                        "  wire [63:0] alu{gi}_{r}_{c} = {};\n",
1503                        cell_v(k, &nn, &ww)
1504                    ));
1505                }
1506                let drv = |d: Drive| match d {
1507                    Drive::Pass(s) => src2_v(s, &nn, &ww),
1508                    Drive::Alu => format!("alu{gi}_{r}_{c}"),
1509                };
1510                body.push_str(&format!("  wire [63:0] vs{gi}_{r}_{c} = {};\n", drv(t.vs)));
1511                body.push_str(&format!("  wire [63:0] he{gi}_{r}_{c} = {};\n", drv(t.he)));
1512                vsk.push(format!("vs{gi}_{r}_{c}"));
1513                hek.push(format!("he{gi}_{r}_{c}"));
1514            }
1515            body.push_str(&format!("  wire [63:0] vs_{r}_{c} = {};\n", mux_n(&vsk)));
1516            body.push_str(&format!("  wire [63:0] he_{r}_{c} = {};\n", mux_n(&hek)));
1517        }
1518    }
1519    let out_wire = |g: &Grid| match g.out.2 {
1520        Chan::Vs => format!("vs_{}_{}", g.out.0, g.out.1),
1521        Chan::He => format!("he_{}_{}", g.out.0, g.out.1),
1522    };
1523    let taps: Vec<String> = grids.iter().map(|g| out_wire(g)).collect();
1524    // The emitted provenance must name the proof this table ACTUALLY has. `certify_config_table`
1525    // asserts out on a Func (division/float) tile — it has no bitblast lowering — so a Func-bearing
1526    // table is never SAT-certified, and a header saying otherwise is a claim nothing backs. Gate the
1527    // sentence on the same predicate the certifier asserts on, so the two can never diverge.
1528    let provenance = if grids.iter().all(|g| grid_is_combinational(g)) {
1529        "// Certified by our SAT (certify_config_table): cfg==k ⇒ the array ≡ fabric k.\n"
1530            .to_string()
1531    } else {
1532        "// Proven by eval ≡ VM + the iverilog differential. NOT SAT-certified: a Func (division/float)\n\
1533         // tile has no bitblast lowering, so the SAT reconfig certifier refuses a table with one in it.\n"
1534            .to_string()
1535    };
1536    format!(
1537        "// config_{name} — ONE {rows}×{cols} CGRA array running {n} fabrics by RECONFIGURATION: every tile is\n\
1538         // muxed on the `cfg` word among all {n} fabrics over the shared switchbox channels; the result muxes\n\
1539         // the {n} output taps.\n\
1540         {provenance}\
1541         // Feed-forward south/east, so combinational — the answer settles in ONE clock.\n\
1542         module config_{name}({inputs}input [{w}:0] cfg, output [63:0] result);\n\
1543         {body}  assign result = {res};\n\
1544         endmodule\n",
1545        n = grids.len(),
1546        w = cfg_width - 1,
1547        res = mux_n(&taps),
1548    )
1549}
1550
1551/// The grid's area footprint: `(rows, cols, used_tiles)` — a tile is USED if it computes or drives
1552/// a non-zero channel. Used to compare placements.
1553pub fn grid_area(g: &Grid) -> (usize, usize, usize) {
1554    let mut used = 0;
1555    for row in &g.tiles {
1556        for t in row {
1557            let live = t.alu.is_some()
1558                || !matches!(t.vs, Drive::Pass(Src2::Zero))
1559                || !matches!(t.he, Drive::Pass(Src2::Zero));
1560            if live {
1561                used += 1;
1562            }
1563        }
1564    }
1565    (g.rows, g.cols, used)
1566}
1567
1568/// **AREA-MINIMIZING placer (level scheduling).** The naive [`route_straight_line`] gives every
1569/// cell two rows (a routing row + a compute row). But a cell whose operands are all global inputs
1570/// or constants (topological LEVEL 0) needs NO routing row — its operands are tapped directly — so
1571/// all level-0 cells share ONE compute row (row 0), packed across columns. Deeper cells still get
1572/// their own routing+compute rows. For DAGs with input-parallelism this collapses the row count
1573/// (a chain saves a little; a wide fan-in tree saves a lot). Correctness is unchanged — the S3 SAT
1574/// proof validates the compact placement exactly as it did the naive one.
1575pub fn route_leveled(cells: &[super::fabric_rtl::FabricOp], n_in: usize, result: usize) -> Grid {
1576    use super::fabric_rtl::Src as FSrc;
1577    let m = cells.len().max(1);
1578    let cols = m;
1579    let has_val = |c: &super::fabric_rtl::FabricOp| {
1580        matches!(c.a, FSrc::Val(_)) || matches!(c.b, FSrc::Val(_))
1581    };
1582    // Level-0 (no Val operand) cells share row 0; every other cell gets a routing+compute pair.
1583    let mut compute_row = vec![0usize; cells.len()];
1584    let mut next = 1usize;
1585    for i in 0..cells.len() {
1586        if has_val(&cells[i]) {
1587            compute_row[i] = next + 1;
1588            next += 2;
1589        } else {
1590            compute_row[i] = 0;
1591        }
1592    }
1593    let rows = next.max(1);
1594    let mut g = Grid::new(rows, cols, n_in);
1595    let operand = |s: FSrc| -> (Src2, Option<usize>) {
1596        match s {
1597            FSrc::In(k) => (Src2::In(k), None),
1598            FSrc::Const(c) => (Src2::Const(c), None),
1599            FSrc::Val(j) => (Src2::Zero, Some(j)),
1600        }
1601    };
1602    for i in 0..cells.len() {
1603        let (ri, ci) = (compute_row[i], i);
1604        let (mut sa, ra) = operand(cells[i].a);
1605        let (mut sb, rb) = operand(cells[i].b);
1606        if ra.is_some() {
1607            sa = Src2::N;
1608        }
1609        if rb.is_some() {
1610            sb = Src2::W;
1611        }
1612        g.tiles[ri][ci].alu = Some(CellKind::Arith(cells[i].op, sa, sb));
1613        g.tiles[ri][ci].vs = Drive::Alu;
1614        // A routed operand's column carries its value down (via the vertical-continue below), so the
1615        // fork at the consumer's routing row / west neighbour resolves it — the helpers are reused.
1616        route_north(&mut g, ra, ci, ri);
1617        route_west(&mut g, rb, ci, ri);
1618    }
1619    for i in 0..cells.len() {
1620        let (ri, ci) = (compute_row[i], i);
1621        for r in ri + 1..rows {
1622            if matches!(g.tiles[r][ci].vs, Drive::Pass(Src2::Zero)) {
1623                g.tiles[r][ci].vs = Drive::Pass(Src2::N);
1624            }
1625        }
1626    }
1627    g.out = (compute_row[result], result, Chan::Vs);
1628    g
1629}
1630
1631/// A self-checking testbench for the (combinational) tile array: drive each input vector, compare
1632/// `result` to the VM oracle from `grid_{name}_{in,out}.hex`.
1633pub fn emit_grid_testbench(name: &str, n_in: usize, n: usize) -> String {
1634    let decls = (0..n_in)
1635        .map(|k| format!("  reg  [63:0] in{k};"))
1636        .collect::<Vec<_>>()
1637        .join("\n");
1638    let ports = (0..n_in)
1639        .map(|k| format!(".in{k}(in{k})"))
1640        .collect::<Vec<_>>()
1641        .join(", ");
1642    let ports = if ports.is_empty() {
1643        String::new()
1644    } else {
1645        format!("{ports}, ")
1646    };
1647    let drive = (0..n_in)
1648        .map(|k| format!("in{k} = oin[i*{n_in}+{k}];"))
1649        .collect::<Vec<_>>()
1650        .join(" ");
1651    format!(
1652        r#"// tb_grid_{name} — the placed+routed CGRA tile array vs the RUNNING VM's output.
1653`timescale 1ns/1ps
1654module tb_grid_{name};
1655{decls}
1656  reg  [63:0] oin [0:{ilast}];
1657  reg  [63:0] oout [0:{olast}];
1658  wire [63:0] result;
1659  integer i, errors = 0;
1660  grid_{name} dut({ports}.result(result));
1661  initial begin
1662    $readmemh("grid_{name}_in.hex", oin);
1663    $readmemh("grid_{name}_out.hex", oout);
1664    for (i = 0; i < {n}; i = i + 1) begin
1665      {drive} #1;
1666      if (result !== oout[i]) begin errors = errors + 1; $display("FAIL case %0d: got %0d exp %0d", i, result, oout[i]); end
1667    end
1668    if (errors == 0) $display("PASS: {n} tile-array evaluations match the VM (placed+routed CGRA)");
1669    else $display("FAIL: %0d grid mismatches", errors);
1670    $finish;
1671  end
1672endmodule
1673"#,
1674        ilast = (n * n_in).saturating_sub(1),
1675        olast = n - 1,
1676    )
1677}
1678
1679#[cfg(test)]
1680#[path = "../../../logicaffeine_tests/unit/logicaffeine_compile/codegen_sva/fabric_grid/tests.rs"]
1681mod tests;