Skip to main content

logicaffeine_compile/codegen_sva/
fabric_pipe.rs

1//! fabric_pipe — the compiled residual circuit cut into register stages: a new input every clock, a short period.
2//!
3//! [`fabric_rtl`](super::fabric_rtl) lowers a dispatch-free residual to a dataflow circuit that resolves in one clock,
4//! so its period is the propagation delay of the whole residual (for the Horner polynomial, two chained 64-bit
5//! multiplies and three carry chains). This module keeps the same op vocabulary and pipelines that circuit:
6//!
7//! 1. **Carry-save lowering** ([`carry_save_lower`]). Every additive tree (the `Add`/`Sub`/`Mul` ops a value is built
8//!    from, through intermediates read exactly once) becomes one set of rows. A variable multiply contributes one
9//!    masked, shifted copy of its multiplicand per multiplier bit; a constant multiply one shifted row per canonical
10//!    signed digit; a subtraction the inverted rows plus their +1s; constants fold into a single row. A tree of 3:2
11//!    compressors reduces the rows to two, and one Kogge-Stone carry-propagate add resolves them. The lowered circuit
12//!    holds only bitwise ops and constant shifts, so no op is more than one gate deep and a register can sit between
13//!    any two of them.
14//! 2. **Scheduling** ([`schedule_pipeline`]). Ops take the earliest stage whose accumulated depth stays within a
15//!    per-stage budget, in the units of [`op_delay`]. Wiring ([`is_wiring`]: a constant shift, or a bitwise op with a
16//!    constant) has no depth and no stage of its own: each reader rebuilds it from its operand's register, so only
17//!    computed values cross registers.
18//! 3. **Emission** ([`emit_fabric_pipelined_verilog`]). Inputs register into stage 0 ([`Inputs::Registered`]), or
19//!    stage 0 reads the ports the registers upstream hold ([`Inputs::Ports`]); each value is delayed through as many
20//!    registers as stages separate it from its last reader, and the result registers after its stage. The result of
21//!    the input presented before clock `t` is on `result` after clock `t + latency - 1`.
22//!
23//! [`simulate_pipeline`] is the cycle model of exactly those registers, so a schedule is checked without a simulator,
24//! and the lowering takes a width so our SAT can check it exhaustively at a narrow one.
25
26use super::fabric_rtl::{
27    emit_loop_fabric_verilog_pipelined, loop_fabric_from_source, poly_program, residual_from_source, AluOp, CmpKind,
28    DecisionFabric, FabricOp, LoopCell, Src,
29};
30
31/// Wiring costs no logic depth: a shift by a constant moves bits, and a bitwise op with a constant operand is
32/// constant bits or inverters. It takes its operands' stage, and each reader rebuilds it from their registers.
33pub fn is_wiring(op: &FabricOp) -> bool {
34    let konst = |s: &Src| matches!(s, Src::Const(_));
35    match op.op {
36        AluOp::Shl | AluOp::Shr | AluOp::UShr => konst(&op.b),
37        AluOp::And | AluOp::Or | AluOp::Xor => konst(&op.a) || konst(&op.b),
38        AluOp::Add | AluOp::Sub | AluOp::Mul => false,
39    }
40}
41
42/// The logic depth of one op, in units of a two-input XOR at the synthesis corner. Wiring is zero. A carry chain or a
43/// multiplier left unlowered is one indivisible op, so it occupies a stage of its own whatever the budget.
44pub fn op_delay(op: &FabricOp) -> f64 {
45    if is_wiring(op) {
46        return 0.0;
47    }
48    match op.op {
49        AluOp::Xor => 1.0,
50        AluOp::And | AluOp::Or => 0.6,
51        AluOp::Add | AluOp::Sub => 9.0,
52        AluOp::Mul => 40.0,
53        AluOp::Shl | AluOp::Shr | AluOp::UShr => 4.0,
54    }
55}
56
57/// Builds the lowered op list, folding constants and dropping identities so no op is emitted that wiring replaces. A
58/// sharing builder emits each op once: an op it has already emitted, its operands in either order when it commutes, is
59/// read again rather than built twice.
60struct Builder {
61    ops: Vec<FabricOp>,
62    shared: Option<std::collections::HashMap<FabricOp, Src>>,
63}
64
65impl Builder {
66    fn new() -> Builder {
67        Builder { ops: Vec::new(), shared: None }
68    }
69
70    fn sharing() -> Builder {
71        Builder { ops: Vec::new(), shared: Some(std::collections::HashMap::new()) }
72    }
73
74    fn push(&mut self, op: AluOp, a: Src, b: Src) -> Src {
75        let key = FabricOp { op, a, b };
76        if let Some(seen) = &self.shared {
77            let commutes = matches!(op, AluOp::Add | AluOp::Mul | AluOp::And | AluOp::Or | AluOp::Xor);
78            if let Some(&s) = seen.get(&key).or_else(|| commutes.then(|| seen.get(&FabricOp { op, a: b, b: a })).flatten()) {
79                return s;
80            }
81        }
82        self.ops.push(key);
83        let s = Src::Val(self.ops.len() - 1);
84        if let Some(seen) = &mut self.shared {
85            seen.insert(key, s);
86        }
87        s
88    }
89
90    /// Constant operands fold only through ops whose low bits depend on low bits alone, so a fold at 64 bits is the
91    /// same fold at every narrower width; right shifts of constants are never formed.
92    fn emit(&mut self, op: AluOp, a: Src, b: Src) -> Src {
93        use AluOp::*;
94        match (op, a, b) {
95            (And | Or | Xor | Add | Sub | Mul | Shl, Src::Const(x), Src::Const(y)) => Src::Const(op.eval(x, y)),
96            (And, Src::Const(0), _) | (And, _, Src::Const(0)) => Src::Const(0),
97            (And, Src::Const(-1), s) | (And, s, Src::Const(-1)) => s,
98            (Or | Xor, Src::Const(0), s) | (Or | Xor, s, Src::Const(0)) => s,
99            (Shl | Shr | UShr, s, Src::Const(0)) => s,
100            _ => self.push(op, a, b),
101        }
102    }
103}
104
105/// The canonical signed digits of `c` mod 2^`width`: `(shift, ±1)` pairs whose signed sum of powers of two is `c`,
106/// no two adjacent. The top digit is taken positive, since ±2^(width-1) agree mod 2^width.
107pub fn csd_digits(c: i64, width: u32) -> Vec<(u32, i64)> {
108    let modulus = 1u128 << width;
109    let mut v = (c as u64 as u128) % modulus;
110    let mut digits = Vec::new();
111    let mut k = 0u32;
112    while v != 0 && k < width {
113        if v & 1 == 1 {
114            let d: i64 = if k + 1 == width || v & 3 == 1 { 1 } else { -1 };
115            digits.push((k, d));
116            v = if d == 1 { v - 1 } else { v + 1 };
117        }
118        v >>= 1;
119        k += 1;
120    }
121    digits
122}
123
124fn width_mask(width: u32) -> i64 {
125    if width == 64 {
126        -1
127    } else {
128        ((1u64 << width) - 1) as i64
129    }
130}
131
132/// The words of one additive tree before reduction: each a value and whether it is subtracted, plus one constant.
133#[derive(Default)]
134struct Rows {
135    words: Vec<(Src, bool)>,
136    constant: i64,
137}
138
139impl Rows {
140    fn push(&mut self, s: Src, negated: bool) {
141        match s {
142            Src::Const(c) if negated => self.constant = self.constant.wrapping_sub(c),
143            Src::Const(c) => self.constant = self.constant.wrapping_add(c),
144            s => self.words.push((s, negated)),
145        }
146    }
147}
148
149struct Lowering<'a> {
150    ops: &'a [FabricOp],
151    width: u32,
152    final_add: FinalAdd,
153    resolves: Vec<bool>,
154    /// A sum whose rows its additive readers take even though it resolves for its other readers.
155    folds: Vec<bool>,
156    lowered: Vec<Option<Src>>,
157    out: Builder,
158}
159
160fn is_additive(op: &FabricOp) -> bool {
161    matches!(op.op, AluOp::Add | AluOp::Sub | AluOp::Mul)
162}
163
164impl Lowering<'_> {
165    fn leaf(&self, s: Src) -> Src {
166        match s {
167            Src::Val(k) => self.lowered[k].expect("a value is lowered before its readers"),
168            other => other,
169        }
170    }
171
172    fn expand(&mut self, s: Src, negated: bool, rows: &mut Rows) {
173        match s {
174            Src::Val(k) if is_additive(&self.ops[k]) && (!self.resolves[k] || self.folds[k]) => {
175                self.expand_op(k, negated, rows)
176            }
177            other => {
178                let leaf = self.leaf(other);
179                rows.push(leaf, negated);
180            }
181        }
182    }
183
184    fn expand_op(&mut self, k: usize, negated: bool, rows: &mut Rows) {
185        let op = self.ops[k];
186        match op.op {
187            AluOp::Add => {
188                self.expand(op.a, negated, rows);
189                self.expand(op.b, negated, rows);
190            }
191            AluOp::Sub => {
192                self.expand(op.a, negated, rows);
193                self.expand(op.b, !negated, rows);
194            }
195            AluOp::Mul => self.multiply_rows(op.a, op.b, negated, rows),
196            _ => unreachable!("only additive ops expand into rows"),
197        }
198    }
199
200    /// A multiply's rows. Its operands resolve to binary first: a variable multiplier is read bit by bit, and each bit
201    /// masks one shifted copy of the multiplicand.
202    fn multiply_rows(&mut self, a: Src, b: Src, negated: bool, rows: &mut Rows) {
203        let w = self.width;
204        match (self.leaf(a), self.leaf(b)) {
205            (Src::Const(x), Src::Const(y)) => rows.push(Src::Const(x.wrapping_mul(y)), negated),
206            (s, Src::Const(c)) | (Src::Const(c), s) => {
207                for (k, d) in csd_digits(c, w) {
208                    let row = self.out.emit(AluOp::Shl, s, Src::Const(k as i64));
209                    rows.push(row, negated != (d < 0));
210                }
211            }
212            (x, y) => {
213                for i in 0..w {
214                    let shifted = self.out.emit(AluOp::Shl, x, Src::Const(i as i64));
215                    let top = self.out.emit(AluOp::Shl, y, Src::Const((w - 1 - i) as i64));
216                    let mask = self.out.emit(AluOp::Shr, top, Src::Const((w - 1) as i64));
217                    let row = self.out.emit(AluOp::And, shifted, mask);
218                    rows.push(row, negated);
219                }
220            }
221        }
222    }
223
224    /// One 3:2 compressor across whole words: the bitwise sum, and the bitwise majority moved up one place.
225    fn compress(&mut self, a: Src, b: Src, c: Src) -> (Src, Src) {
226        let ab = self.out.emit(AluOp::Xor, a, b);
227        let sum = self.out.emit(AluOp::Xor, ab, c);
228        let both = self.out.emit(AluOp::And, a, b);
229        let carried = self.out.emit(AluOp::And, c, ab);
230        let majority = self.out.emit(AluOp::Or, both, carried);
231        let carry = self.out.emit(AluOp::Shl, majority, Src::Const(1));
232        (sum, carry)
233    }
234
235    /// The Kogge-Stone carry-propagate add: group generate and propagate doubled in span each level, so the carry into
236    /// every bit is ready after log2(width) levels.
237    fn prefix_add(&mut self, a: Src, b: Src) -> Src {
238        if let (Src::Const(x), Src::Const(y)) = (a, b) {
239            return Src::Const(x.wrapping_add(y));
240        }
241        let mut generate = self.out.emit(AluOp::And, a, b);
242        let half_sum = self.out.emit(AluOp::Xor, a, b);
243        let mut propagate = half_sum;
244        let mut span = 1u32;
245        while span < self.width {
246            let lower = self.out.emit(AluOp::Shl, generate, Src::Const(span as i64));
247            let through = self.out.emit(AluOp::And, propagate, lower);
248            generate = self.out.emit(AluOp::Or, generate, through);
249            if 2 * span < self.width {
250                let lower = self.out.emit(AluOp::Shl, propagate, Src::Const(span as i64));
251                propagate = self.out.emit(AluOp::And, propagate, lower);
252            }
253            span *= 2;
254        }
255        let carries = self.out.emit(AluOp::Shl, generate, Src::Const(1));
256        self.out.emit(AluOp::Xor, half_sum, carries)
257    }
258
259    fn resolve(&mut self, rows: Rows) -> Src {
260        let mut constant = rows.constant;
261        let mut words: Vec<Src> = Vec::with_capacity(rows.words.len() + 1);
262        for (s, negated) in rows.words {
263            if negated {
264                words.push(self.out.emit(AluOp::Xor, s, Src::Const(-1)));
265                constant = constant.wrapping_add(1);
266            } else {
267                words.push(s);
268            }
269        }
270        if constant & width_mask(self.width) != 0 {
271            words.push(Src::Const(constant));
272        }
273        if words.is_empty() {
274            return Src::Const(0);
275        }
276        while words.len() > 2 {
277            let mut next = Vec::with_capacity(words.len() * 2 / 3 + 2);
278            let mut triples = words.chunks_exact(3);
279            for t in &mut triples {
280                let (sum, carry) = self.compress(t[0], t[1], t[2]);
281                next.push(sum);
282                next.push(carry);
283            }
284            next.extend_from_slice(triples.remainder());
285            words = next;
286        }
287        if words.len() == 1 {
288            words[0]
289        } else if self.final_add == FinalAdd::Native {
290            self.out.emit(AluOp::Add, words[0], words[1])
291        } else {
292            self.prefix_add(words[0], words[1])
293        }
294    }
295}
296
297/// How a sum that other sums read is lowered.
298#[derive(Clone, Copy, Debug, PartialEq, Eq)]
299pub enum SumSharing {
300    /// A sum read more than once resolves to binary once and every reader reads the resolved word: the fewest adders.
301    Resolve,
302    /// Every add or subtract that reads a sum of at most [`FOLD_ROW_LIMIT`] words takes the sum's rows into its own,
303    /// so no carry-propagate add waits on another; the sum still resolves for any reader that is not an add or a
304    /// subtract (a compare's overflow fix reads its sign), and every op is emitted once. More adders, each one carry
305    /// chain from the inputs: the lowering for standard cells, where every carry chain in series costs its full delay.
306    Fold,
307}
308
309/// The words a folded sum may bring into each reader's rows; a larger sum resolves once and is read as one word.
310pub const FOLD_ROW_LIMIT: usize = 8;
311
312/// How the two words a sum's compressor tree leaves are added.
313#[derive(Clone, Copy, Debug, PartialEq, Eq)]
314pub enum FinalAdd {
315    /// A Kogge-Stone prefix network written out in bitwise ops: no op more than one gate deep, so a register can sit
316    /// inside the add.
317    PrefixTree,
318    /// One add, left to synthesis, which picks the adder the clock needs (on standard cells, a smaller one than the
319    /// widest prefix network wherever the clock allows).
320    Native,
321}
322
323/// Carry-save lowering of a residual at datapath width `width` (64 on silicon; narrower so our SAT can check the
324/// lowering exhaustively), each shared sum resolved once ([`SumSharing::Resolve`]). Only the values the result depends
325/// on are lowered. Returns the new op list, which holds no `Add`, `Sub` or `Mul`, and the index of its result.
326///
327/// A value resolves to binary when it is the result, is read more than once, or is read by anything but an add or a
328/// subtract: a multiply reads its operands bit by bit. Every other additive value is absorbed into its reader's rows.
329pub fn carry_save_lower(ops: &[FabricOp], result: usize, width: u32) -> (Vec<FabricOp>, usize) {
330    carry_save_lower_with(ops, result, width, SumSharing::Resolve, FinalAdd::PrefixTree)
331}
332
333/// [`carry_save_lower`] with the shared sums lowered as `sharing` says and each compressor tree's last two words added
334/// as `final_add` says. Under [`SumSharing::Fold`] an add or subtract read only by adds and subtracts never resolves,
335/// however many read it, and one its rows fold into resolves only for its other readers; a multiply read more than
336/// once still resolves once. Under [`FinalAdd::Native`] the lowering holds one `Add` per resolved sum and nothing else
337/// additive.
338pub fn carry_save_lower_with(
339    ops: &[FabricOp],
340    result: usize,
341    width: u32,
342    sharing: SumSharing,
343    final_add: FinalAdd,
344) -> (Vec<FabricOp>, usize) {
345    assert!((2..=64).contains(&width), "the datapath width is 2..=64 bits, not {width}");
346    assert!(result < ops.len(), "the result names an op of the residual");
347    let n = ops.len();
348    let live = needed_by(ops, result);
349    let mut readers = vec![0usize; n];
350    let mut resolves = vec![false; n];
351    resolves[result] = true;
352    for op in ops.iter().zip(&live).filter(|(_, l)| **l).map(|(op, _)| op) {
353        for s in [op.a, op.b] {
354            if let Src::Val(k) = s {
355                readers[k] += 1;
356                if !matches!(op.op, AluOp::Add | AluOp::Sub) {
357                    resolves[k] = true;
358                }
359            }
360        }
361    }
362    let mut folds = vec![false; n];
363    match sharing {
364        SumSharing::Resolve => {
365            for k in 0..n {
366                resolves[k] |= readers[k] > 1;
367            }
368        }
369        SumSharing::Fold => {
370            // The words each sum brings into a reader's rows, in dataflow order: a folded or absorbed sum its own
371            // rows, an unresolved multiply one row per multiplier bit, anything else one word.
372            let mut words = vec![1usize; n];
373            for k in 0..n {
374                let term = |s: Src, words: &[usize], resolves: &[bool], folds: &[bool]| match s {
375                    Src::Const(_) => 0,
376                    Src::In(_) => 1,
377                    Src::Val(j) if is_additive(&ops[j]) && (!resolves[j] || folds[j]) => words[j],
378                    Src::Val(_) => 1,
379                };
380                match ops[k].op {
381                    AluOp::Add | AluOp::Sub => {
382                        words[k] = term(ops[k].a, &words, &resolves, &folds) + term(ops[k].b, &words, &resolves, &folds);
383                        folds[k] = words[k] <= FOLD_ROW_LIMIT;
384                        resolves[k] |= !folds[k] && readers[k] > 1;
385                    }
386                    AluOp::Mul => {
387                        resolves[k] |= readers[k] > 1;
388                        words[k] = width as usize;
389                    }
390                    _ => {}
391                }
392            }
393        }
394    }
395    let mut lo = Lowering {
396        ops,
397        width,
398        final_add,
399        resolves,
400        folds,
401        lowered: vec![None; n],
402        out: match sharing {
403            SumSharing::Resolve => Builder::new(),
404            SumSharing::Fold => Builder::sharing(),
405        },
406    };
407    for i in 0..n {
408        if !live[i] {
409            continue;
410        }
411        let op = ops[i];
412        if is_additive(&op) {
413            if !lo.resolves[i] {
414                continue;
415            }
416            let mut rows = Rows::default();
417            lo.expand_op(i, false, &mut rows);
418            let s = lo.resolve(rows);
419            lo.lowered[i] = Some(s);
420        } else {
421            let (a, b) = (lo.leaf(op.a), lo.leaf(op.b));
422            let s = lo.out.emit(op.op, a, b);
423            lo.lowered[i] = Some(s);
424        }
425    }
426    let result = match lo.lowered[result].expect("the result is lowered") {
427        Src::Val(k) => k,
428        other => {
429            lo.out.push(AluOp::Or, other, Src::Const(0));
430            lo.out.ops.len() - 1
431        }
432    };
433    (lo.out.ops, result)
434}
435
436/// The stage budget a decision is cut to on a fabric with carry chains, in the units of [`op_delay`]: one unlowered
437/// add or subtract (a carry chain, 9) and an AND. The XORs that finish a compare's sign start the next stage, so the
438/// mask a select fans out to its 64 bits leaves a register, never the end of a carry chain. MEASURED (2026-10-17, K1's
439/// `decide`, Artix-7 fabric map, yosys 0.33 synth_xilinx + nextpnr-xilinx, seeds 1-3, median): 12 (the compare and
440/// its select in one stage) = 852 LUTs at 119.2 MHz, 10 = 848 at 147.3, 9.6 = 784 at 145.4, 6 = 848 at 147.8; at 8
441/// and 9 one seed in three does not finish routing within 300 s.
442pub const DECISION_CHAIN_BUDGET: f64 = 9.6;
443
444/// The stage budget a decision is cut to on standard cells, in the units of [`op_delay`]: three carry chains and their
445/// selects. A register is ~20 µm² of flip-flop a bit there, where on a fabric it rides in a LUT's slice, so a decision
446/// takes few stages and reads its ports ([`Inputs::Ports`]); each compare takes the sums it reads into its own rows
447/// ([`SumSharing::Fold`]), resolved by one add synthesis picks ([`FinalAdd::Native`]). MEASURED (2026-10-18, K1's
448/// `decide` so lowered, its ports read, Sky130 HD: yosys 0.33 with OpenLane v1's `DELAY 0` ABC script at a 2 ns
449/// target, OpenSTA at the typical corner, no wires): 18 = 5 stages, 44,306 µm² at 4.65 ns; 30 = 3 stages, 34,005 µm²
450/// at 3.57 ns; 36 = 2 stages, 30,843 µm² at 5.03 ns; one stage, 32,013 µm² at 7.71 ns. Without the folds, 27 and 30
451/// were 32,750 µm² at 4.10 ns and 31,559 µm² at 4.34 ns.
452pub const DECISION_CELLS_BUDGET: f64 = 30.0;
453
454/// A decision fabric ([`decision_fabric_from_source`](super::fabric_rtl::decision_fabric_from_source): arithmetic
455/// cells and compare-selects) as a residual in the pipeline's op vocabulary, and the index of its result. A compare
456/// becomes the mask of its outcome, all ones or zero (`compare_mask`); a select is `e ^ ((t ^ e) & mask)`. Every op is
457/// emitted once, so a predicate many selects test has one mask. A fabric with any other cell (a functional unit, a
458/// memory or heap tile) has no combinational form and is refused.
459pub fn decision_ops(df: &DecisionFabric) -> Result<(Vec<FabricOp>, usize), String> {
460    let mut out = Builder::sharing();
461    let mut vals: Vec<Src> = Vec::with_capacity(df.cells.len());
462    let at = |s: Src, vals: &[Src]| match s {
463        Src::Val(k) => vals[k],
464        other => other,
465    };
466    for (i, cell) in df.cells.iter().enumerate() {
467        let v = match *cell {
468            LoopCell::Arith { op, a, b } => out.emit(op, at(a, &vals), at(b, &vals)),
469            LoopCell::Select { cmp, ca, cb, t, e } => {
470                let mask = compare_mask(&mut out, cmp, at(ca, &vals), at(cb, &vals));
471                let (t, e) = (at(t, &vals), at(e, &vals));
472                let differ = out.emit(AluOp::Xor, t, e);
473                let taken = out.emit(AluOp::And, differ, mask);
474                out.emit(AluOp::Xor, e, taken)
475            }
476            ref other => return Err(format!("cell {i} ({other:?}) has no combinational form in a pipeline")),
477        };
478        vals.push(v);
479    }
480    let result = match at(df.result, &vals) {
481        Src::Val(k) => k,
482        other => {
483            out.push(AluOp::Or, other, Src::Const(0));
484            out.ops.len() - 1
485        }
486    };
487    Ok((out.ops, result))
488}
489
490/// All ones when `x cmp y` holds on the signed words, zero when it does not. `x < y` is the sign of the true
491/// difference: the sign of `d = x − y`, flipped when the subtraction overflowed (`x` and `y` differ in sign and `d`
492/// differs from `x`), spread across the word by an arithmetic shift. `x ≠ y` ORs every bit of `z = x ^ y` up into the
493/// sign (`z | z << 1`, then `<< 2` … `<< 32`), an OR tree with no carry chain, and spreads it the same way. The other
494/// compares swap or invert those.
495fn compare_mask(out: &mut Builder, cmp: CmpKind, x: Src, y: Src) -> Src {
496    fn less(out: &mut Builder, a: Src, b: Src) -> Src {
497        let d = out.emit(AluOp::Sub, a, b);
498        let signs = out.emit(AluOp::Xor, a, b);
499        let moved = out.emit(AluOp::Xor, a, d);
500        let overflow = out.emit(AluOp::And, signs, moved);
501        let sign = out.emit(AluOp::Xor, d, overflow);
502        out.emit(AluOp::Shr, sign, Src::Const(63))
503    }
504    fn differ(out: &mut Builder, a: Src, b: Src) -> Src {
505        let mut any = out.emit(AluOp::Xor, a, b);
506        for k in [1, 2, 4, 8, 16, 32] {
507            let up = out.emit(AluOp::Shl, any, Src::Const(k));
508            any = out.emit(AluOp::Or, any, up);
509        }
510        out.emit(AluOp::Shr, any, Src::Const(63))
511    }
512    let mask = match cmp {
513        CmpKind::Lt | CmpKind::Ge => less(out, x, y),
514        CmpKind::Gt | CmpKind::Le => less(out, y, x),
515        CmpKind::Ne | CmpKind::Eq => differ(out, x, y),
516    };
517    match cmp {
518        CmpKind::Ge | CmpKind::Le | CmpKind::Eq => out.emit(AluOp::Xor, mask, Src::Const(-1)),
519        CmpKind::Lt | CmpKind::Gt | CmpKind::Ne => mask,
520    }
521}
522
523/// Where a pipeline's first stage reads its inputs.
524#[derive(Clone, Copy, Debug, PartialEq, Eq)]
525pub enum Inputs {
526    /// The pipeline registers its inputs, and stage 0 reads those registers, whatever drives the ports.
527    Registered,
528    /// Stage 0 reads the ports, which the registers driving them hold for the clock: a pipeline whose upstream
529    /// registers what it presents (the comparative bench's harness, a register file) registers nothing twice.
530    Ports,
531}
532
533/// Where every op of a residual computes, and which op leaves the pipeline.
534#[derive(Clone, Debug, PartialEq)]
535pub struct PipelineSchedule {
536    /// The stage of each op; wiring carries its operands' stage.
537    pub stage: Vec<usize>,
538    pub result: usize,
539    /// The per-stage depth budget the schedule was cut to, in the units of [`op_delay`].
540    pub budget: f64,
541    /// Where stage 0 reads the inputs.
542    pub inputs: Inputs,
543}
544
545impl PipelineSchedule {
546    /// The register boundaries between the inputs and the output register, plus one.
547    pub fn stages(&self) -> usize {
548        self.stage[self.result] + 1
549    }
550
551    /// Clocks from presenting an input to its result on the output register: the input register when there is one,
552    /// the boundaries between stages, and the output register.
553    pub fn latency(&self) -> usize {
554        self.stage[self.result] + 1 + usize::from(self.inputs == Inputs::Registered)
555    }
556
557    /// The same stages, reading the inputs from `inputs`.
558    pub fn reading(self, inputs: Inputs) -> PipelineSchedule {
559        PipelineSchedule { inputs, ..self }
560    }
561
562    /// The deepest path inside any one stage of the emitted pipeline, in the units of [`op_delay`]: a value read in
563    /// its own stage arrives when it is computed, and a value read in a later stage arrives from a register at depth
564    /// zero. Ops the result does not need are never emitted and never counted.
565    pub fn stage_depth(&self, ops: &[FabricOp]) -> f64 {
566        let needed = needed_by(ops, self.result);
567        stage_arrivals(ops, &self.stage)
568            .into_iter()
569            .zip(needed)
570            .filter(|(_, n)| *n)
571            .map(|(a, _)| a)
572            .fold(0.0, f64::max)
573    }
574
575    /// The schedule respects dataflow: no op computes before an operand, and wiring sits in its operands' stage.
576    pub fn check(&self, ops: &[FabricOp]) -> Result<(), String> {
577        if self.stage.len() != ops.len() {
578            return Err(format!("{} stages for {} ops", self.stage.len(), ops.len()));
579        }
580        for (i, op) in ops.iter().enumerate() {
581            let mut latest = 0;
582            for s in [op.a, op.b] {
583                if let Src::Val(k) = s {
584                    if k >= i {
585                        return Err(format!("op {i} reads v{k}, which is not computed before it"));
586                    }
587                    if self.stage[k] > self.stage[i] {
588                        return Err(format!(
589                            "op {i} in stage {} reads v{k} from the later stage {}",
590                            self.stage[i], self.stage[k]
591                        ));
592                    }
593                    latest = latest.max(self.stage[k]);
594                }
595            }
596            if is_wiring(op) && self.stage[i] != latest {
597                return Err(format!(
598                    "wiring op {i} sits in stage {} rather than its operands' stage {latest}",
599                    self.stage[i]
600                ));
601            }
602        }
603        Ok(())
604    }
605}
606
607/// The ops `result` depends on, itself included.
608fn needed_by(ops: &[FabricOp], result: usize) -> Vec<bool> {
609    let mut needed = vec![false; ops.len()];
610    needed[result] = true;
611    for i in (0..ops.len()).rev() {
612        if needed[i] {
613            for s in [ops[i].a, ops[i].b] {
614                if let Src::Val(k) = s {
615                    needed[k] = true;
616                }
617            }
618        }
619    }
620    needed
621}
622
623fn stage_arrivals(ops: &[FabricOp], stage: &[usize]) -> Vec<f64> {
624    let mut arrival = vec![0.0f64; ops.len()];
625    for (i, op) in ops.iter().enumerate() {
626        let start = [op.a, op.b]
627            .into_iter()
628            .filter_map(|s| match s {
629                Src::Val(k) if stage[k] == stage[i] => Some(arrival[k]),
630                _ => None,
631            })
632            .fold(0.0, f64::max);
633        arrival[i] = start + op_delay(op);
634    }
635    arrival
636}
637
638/// The bits of each value its construction fixes, as (mask of known bits, their values): shifts bring in zeros,
639/// masks and constants pin bits, a carry row's bit 0 is zero. A register on a known bit holds a constant, which
640/// synthesis removes, so a schedule is charged only for the live bits it holds.
641pub fn known_bits(ops: &[FabricOp]) -> Vec<(u64, u64)> {
642    let mut known: Vec<(u64, u64)> = Vec::with_capacity(ops.len());
643    for op in ops {
644        let of = |s: Src, known: &[(u64, u64)]| match s {
645            Src::Const(c) => (u64::MAX, c as u64),
646            Src::In(_) => (0, 0),
647            Src::Val(k) => known[k],
648        };
649        let ((ma, va), (mb, vb)) = (of(op.a, &known), of(op.b, &known));
650        let low_zeros = |m: u64, v: u64| (!(!m | v)).trailing_ones();
651        let (mask, value) = match (op.op, op.b) {
652            (AluOp::And, _) => {
653                let zeros = (ma & !va) | (mb & !vb);
654                let mask = (ma & mb) | zeros;
655                (mask, va & vb & mask)
656            }
657            (AluOp::Or, _) => {
658                let ones = (ma & va) | (mb & vb);
659                let mask = (ma & mb) | ones;
660                (mask, (va | vb) & mask)
661            }
662            (AluOp::Xor, _) => (ma & mb, (va ^ vb) & ma & mb),
663            (AluOp::Shl, Src::Const(k)) => {
664                let k = (k & 63) as u32;
665                let low = if k == 0 { 0 } else { u64::MAX >> (64 - k) };
666                (ma.wrapping_shl(k) | low, va.wrapping_shl(k))
667            }
668            (AluOp::UShr, Src::Const(k)) => {
669                let k = (k & 63) as u32;
670                let high = if k == 0 { 0 } else { u64::MAX << (64 - k) };
671                (ma.wrapping_shr(k) | high, va.wrapping_shr(k))
672            }
673            (AluOp::Shr, Src::Const(k)) => {
674                let k = (k & 63) as u32;
675                let sign_known = ma >> 63 == 1;
676                let high = if k == 0 || !sign_known { 0 } else { u64::MAX << (64 - k) };
677                let mask = ma.wrapping_shr(k) | high;
678                (mask, ((va as i64).wrapping_shr(k) as u64) & mask)
679            }
680            (AluOp::Add | AluOp::Sub, _) => {
681                let t = low_zeros(ma, va).min(low_zeros(mb, vb));
682                (if t == 0 { 0 } else { u64::MAX >> (64 - t) }, 0)
683            }
684            (AluOp::Mul, _) => {
685                let t = (low_zeros(ma, va) + low_zeros(mb, vb)).min(64);
686                (if t == 0 { 0 } else { u64::MAX >> (64 - t.min(64)) }, 0)
687            }
688            _ => (0, 0),
689        };
690        known.push((mask, value & mask));
691    }
692    known
693}
694
695fn live_bits(known: (u64, u64)) -> u64 {
696    64 - u64::from(known.0.count_ones())
697}
698
699/// A residual's stage-assignment problem under a depth budget: the computed values the result depends on, what
700/// each reads through wiring, the difference constraints any legal assignment satisfies, and each value's earliest
701/// and latest stage in the shortest pipeline.
702struct StageProblem {
703    /// The ops the result depends on.
704    needed: Vec<bool>,
705    /// For each needed computed op, the inputs and computed values it reads through wiring.
706    reads: Vec<Vec<Held>>,
707    /// The inputs and computed values the output register reads through wiring.
708    result_reads: Vec<Held>,
709    /// `(x, y, w)`: stage(y) ≥ stage(x) + w, over computed ops.
710    constraints: Vec<(usize, usize, usize)>,
711    earliest: Vec<usize>,
712    latest: Vec<usize>,
713    /// The stage the output register reads in: the last stage of the shortest pipeline.
714    last: usize,
715}
716
717fn sources(ops: &[FabricOp], s: Src, memo: &mut Vec<Option<Vec<Held>>>) -> Vec<Held> {
718    match s {
719        Src::Const(_) => Vec::new(),
720        Src::In(k) => vec![Held::Input(k)],
721        Src::Val(k) if is_wiring(&ops[k]) => {
722            if memo[k].is_none() {
723                let mut v = sources(ops, ops[k].a, memo);
724                v.extend(sources(ops, ops[k].b, memo));
725                v.sort();
726                v.dedup();
727                memo[k] = Some(v);
728            }
729            memo[k].clone().expect("memoized")
730        }
731        Src::Val(k) => vec![Held::Value(k)],
732    }
733}
734
735impl StageProblem {
736    fn new(ops: &[FabricOp], result: usize, budget: f64) -> StageProblem {
737        assert!(budget > 0.0, "a stage budget is positive");
738        assert!(result < ops.len(), "the result names an op of the residual");
739        let n = ops.len();
740        let needed = needed_by(ops, result);
741        let computed = |i: usize| needed[i] && !is_wiring(&ops[i]);
742        let mut memo = vec![None; n];
743        let mut reads = vec![Vec::new(); n];
744        for i in (0..n).filter(|&i| computed(i)) {
745            let mut v = sources(ops, ops[i].a, &mut memo);
746            v.extend(sources(ops, ops[i].b, &mut memo));
747            v.sort();
748            v.dedup();
749            reads[i] = v;
750        }
751        let result_reads = sources(ops, Src::Val(result), &mut memo);
752        let mut readers = vec![Vec::new(); n];
753        for i in (0..n).filter(|&i| computed(i)) {
754            for h in &reads[i] {
755                if let Held::Value(k) = h {
756                    readers[*k].push(i);
757                }
758            }
759        }
760
761        let mut constraints = Vec::new();
762        for i in (0..n).filter(|&i| computed(i)) {
763            for h in &reads[i] {
764                if let Held::Value(k) = h {
765                    constraints.push((*k, i, 0));
766                }
767            }
768        }
769        let mut reach = vec![f64::NAN; n];
770        for u in (0..n).filter(|&u| computed(u)) {
771            let mut frontier = std::collections::BinaryHeap::new();
772            let mut touched = vec![u];
773            reach[u] = op_delay(&ops[u]);
774            if reach[u] <= budget {
775                frontier.extend(readers[u].iter().map(|&r| std::cmp::Reverse(r)));
776            } else {
777                constraints.extend(readers[u].iter().map(|&r| (u, r, 1)));
778            }
779            while let Some(std::cmp::Reverse(v)) = frontier.pop() {
780                if !reach[v].is_nan() {
781                    continue;
782                }
783                let start = reads[v]
784                    .iter()
785                    .filter_map(|h| match h {
786                        Held::Value(p) if !reach[*p].is_nan() => Some(reach[*p]),
787                        _ => None,
788                    })
789                    .fold(0.0, f64::max);
790                reach[v] = start + op_delay(&ops[v]);
791                touched.push(v);
792                if reach[v] > budget {
793                    constraints.push((u, v, 1));
794                } else {
795                    frontier.extend(readers[v].iter().map(|&r| std::cmp::Reverse(r)));
796                }
797            }
798            for t in touched {
799                reach[t] = f64::NAN;
800            }
801        }
802        constraints.sort();
803        constraints.dedup();
804        let mut strongest: std::collections::BTreeMap<(usize, usize), usize> = Default::default();
805        for &(x, y, w) in &constraints {
806            let e = strongest.entry((x, y)).or_insert(0);
807            *e = (*e).max(w);
808        }
809        let constraints: Vec<(usize, usize, usize)> = strongest.into_iter().map(|((x, y), w)| (x, y, w)).collect();
810
811        let mut earliest = vec![0usize; n];
812        let mut incoming: Vec<Vec<(usize, usize)>> = vec![Vec::new(); n];
813        let mut outgoing: Vec<Vec<(usize, usize)>> = vec![Vec::new(); n];
814        for &(x, y, w) in &constraints {
815            incoming[y].push((x, w));
816            outgoing[x].push((y, w));
817        }
818        for i in (0..n).filter(|&i| computed(i)) {
819            earliest[i] = incoming[i].iter().map(|&(x, w)| earliest[x] + w).max().unwrap_or(0);
820        }
821        let last = result_reads
822            .iter()
823            .map(|h| match h {
824                Held::Value(k) => earliest[*k],
825                Held::Input(_) => 0,
826            })
827            .max()
828            .unwrap_or(0);
829        let mut latest = vec![last; n];
830        for i in (0..n).rev().filter(|&i| computed(i)) {
831            for &(y, w) in &outgoing[i] {
832                latest[i] = latest[i].min(latest[y] - w);
833            }
834            debug_assert!(earliest[i] <= latest[i], "op {i}: earliest {} after latest {}", earliest[i], latest[i]);
835        }
836        StageProblem {
837            needed,
838            reads,
839            result_reads,
840            constraints,
841            earliest,
842            latest,
843            last,
844        }
845    }
846
847    /// A schedule from the stages of the computed ops: wiring and the ops the result does not need take their
848    /// operands' latest stage.
849    fn schedule(&self, ops: &[FabricOp], result: usize, budget: f64, computed_stage: &[usize]) -> PipelineSchedule {
850        let mut stage = vec![0usize; ops.len()];
851        for (i, op) in ops.iter().enumerate() {
852            stage[i] = if self.needed[i] && !is_wiring(op) {
853                computed_stage[i]
854            } else {
855                [op.a, op.b]
856                    .into_iter()
857                    .filter_map(|s| match s {
858                        Src::Val(k) => Some(stage[k]),
859                        _ => None,
860                    })
861                    .max()
862                    .unwrap_or(0)
863            };
864        }
865        PipelineSchedule {
866            stage,
867            result,
868            budget,
869            inputs: Inputs::Registered,
870        }
871    }
872}
873
874/// Every computed value in its earliest stage: the shortest pipeline whose stages each stay within `budget`.
875pub fn earliest_schedule(ops: &[FabricOp], result: usize, budget: f64) -> PipelineSchedule {
876    let p = StageProblem::new(ops, result, budget);
877    p.schedule(ops, result, budget, &p.earliest)
878}
879
880/// Every computed value in its latest stage of that same shortest pipeline.
881pub fn latest_schedule(ops: &[FabricOp], result: usize, budget: f64) -> PipelineSchedule {
882    let p = StageProblem::new(ops, result, budget);
883    p.schedule(ops, result, budget, &p.latest)
884}
885
886/// Cut a residual into the shortest pipeline whose stages each stay within `budget` (in the units of [`op_delay`]),
887/// placing every computed value so the pipeline holds the fewest live register bits. An op deeper than the budget
888/// on its own still gets a stage, alone.
889///
890/// The placement is exact. A legal assignment of stages is a set of difference constraints (a reader never sits
891/// before what it reads; two values joined by a path deeper than the budget sit in different stages), and the bits
892/// a value holds are its live bits times the stages between it and its last reader. With each value's stage
893/// bounded by its earliest and latest, "stage(x) ≥ k" becomes a boolean, every constraint an implication between
894/// them, and the cost a weight on each: the cheapest assignment is a minimum-weight closure, found as a minimum cut.
895pub fn schedule_pipeline(ops: &[FabricOp], result: usize, budget: f64) -> PipelineSchedule {
896    let p = StageProblem::new(ops, result, budget);
897    let n = ops.len();
898    let known = known_bits(ops);
899    let bits = |h: Held| -> i64 {
900        match h {
901            Held::Input(_) => 64,
902            Held::Value(k) => live_bits(known[k]) as i64,
903        }
904    };
905
906    // Variables: every computed op, and one mirror per held value standing for its last reader's stage.
907    let mut lo: Vec<usize> = Vec::new();
908    let mut hi: Vec<usize> = Vec::new();
909    let mut weight: Vec<i64> = Vec::new();
910    let mut var_of_op = vec![usize::MAX; n];
911    for i in 0..n {
912        if p.needed[i] && !is_wiring(&ops[i]) {
913            var_of_op[i] = lo.len();
914            lo.push(p.earliest[i]);
915            hi.push(p.latest[i]);
916            weight.push(0);
917        }
918    }
919    let mut readers_of: std::collections::BTreeMap<Held, Vec<usize>> = Default::default();
920    for i in 0..n {
921        if var_of_op[i] != usize::MAX {
922            for &h in &p.reads[i] {
923                readers_of.entry(h).or_default().push(var_of_op[i]);
924            }
925        }
926    }
927    for &h in &p.result_reads {
928        readers_of.entry(h).or_default();
929    }
930    let mut edges: Vec<(usize, usize, usize)> = p
931        .constraints
932        .iter()
933        .map(|&(x, y, w)| (var_of_op[x], var_of_op[y], w))
934        .collect();
935    for (&h, readers) in &readers_of {
936        let reads_result = p.result_reads.contains(&h);
937        let mlo = readers
938            .iter()
939            .map(|&r| lo[r])
940            .chain(reads_result.then_some(p.last))
941            .chain(match h {
942                Held::Value(k) => Some(p.earliest[k]),
943                Held::Input(_) => Some(0),
944            })
945            .max()
946            .unwrap_or(0);
947        let mhi = readers
948            .iter()
949            .map(|&r| hi[r])
950            .chain(reads_result.then_some(p.last))
951            .max()
952            .unwrap_or(mlo)
953            .max(mlo);
954        let m = lo.len();
955        lo.push(mlo);
956        hi.push(mhi);
957        weight.push(bits(h));
958        if let Held::Value(k) = h {
959            weight[var_of_op[k]] -= bits(h);
960        }
961        edges.extend(readers.iter().map(|&r| (r, m, 0)));
962    }
963
964    // Closure graph: node (x, k) for k in (lo_x, hi_x] means stage(x) ≥ k.
965    let mut level_base = Vec::with_capacity(lo.len());
966    let mut nodes = 0usize;
967    for x in 0..lo.len() {
968        level_base.push(nodes);
969        nodes += hi[x] - lo[x];
970    }
971    let node = |x: usize, k: usize| level_base[x] + (k - lo[x] - 1);
972    let (source, sink) = (nodes, nodes + 1);
973    let positive: i64 = (0..lo.len())
974        .map(|x| (-weight[x]).max(0) * (hi[x] - lo[x]) as i64)
975        .sum();
976    let infinite = positive + 1;
977    let mut flow = MaxFlow::new(nodes + 2);
978    for x in 0..lo.len() {
979        for k in lo[x] + 1..=hi[x] {
980            let gain = -weight[x];
981            if gain > 0 {
982                flow.add(source, node(x, k), gain);
983            } else if gain < 0 {
984                flow.add(node(x, k), sink, -gain);
985            }
986            if k > lo[x] + 1 {
987                flow.add(node(x, k), node(x, k - 1), infinite);
988            }
989        }
990    }
991    for &(x, y, w) in &edges {
992        for k in lo[x] + 1..=hi[x] {
993            let j = k + w;
994            if j <= lo[y] {
995                continue;
996            }
997            assert!(j <= hi[y], "stage bounds admit a constraint violation: {x} → {y} (+{w}) at level {k}");
998            flow.add(node(x, k), node(y, j), infinite);
999        }
1000    }
1001    flow.max_flow(source, sink);
1002    let chosen = flow.reachable(source);
1003    let mut computed_stage = vec![0usize; n];
1004    for i in 0..n {
1005        let x = var_of_op[i];
1006        if x != usize::MAX {
1007            computed_stage[i] = lo[x] + (lo[x] + 1..=hi[x]).filter(|&k| chosen[node(x, k)]).count();
1008        }
1009    }
1010    p.schedule(ops, result, budget, &computed_stage)
1011}
1012
1013/// Dinic's maximum flow over integer capacities, with the residual reachability that gives the minimum cut.
1014struct MaxFlow {
1015    head: Vec<usize>,
1016    next: Vec<usize>,
1017    to: Vec<usize>,
1018    cap: Vec<i64>,
1019}
1020
1021impl MaxFlow {
1022    const NONE: usize = usize::MAX;
1023
1024    fn new(n: usize) -> MaxFlow {
1025        MaxFlow {
1026            head: vec![Self::NONE; n],
1027            next: Vec::new(),
1028            to: Vec::new(),
1029            cap: Vec::new(),
1030        }
1031    }
1032
1033    fn add(&mut self, u: usize, v: usize, c: i64) {
1034        for (a, b, c) in [(u, v, c), (v, u, 0)] {
1035            self.next.push(self.head[a]);
1036            self.to.push(b);
1037            self.cap.push(c);
1038            self.head[a] = self.to.len() - 1;
1039        }
1040    }
1041
1042    fn levels(&self, s: usize) -> Vec<usize> {
1043        let mut level = vec![usize::MAX; self.head.len()];
1044        let mut queue = std::collections::VecDeque::from([s]);
1045        level[s] = 0;
1046        while let Some(u) = queue.pop_front() {
1047            let mut e = self.head[u];
1048            while e != Self::NONE {
1049                if self.cap[e] > 0 && level[self.to[e]] == usize::MAX {
1050                    level[self.to[e]] = level[u] + 1;
1051                    queue.push_back(self.to[e]);
1052                }
1053                e = self.next[e];
1054            }
1055        }
1056        level
1057    }
1058
1059    fn max_flow(&mut self, s: usize, t: usize) -> i64 {
1060        let mut total = 0;
1061        loop {
1062            let level = self.levels(s);
1063            if level[t] == usize::MAX {
1064                return total;
1065            }
1066            let mut cursor = self.head.clone();
1067            loop {
1068                let pushed = self.augment(s, t, i64::MAX, &level, &mut cursor);
1069                if pushed == 0 {
1070                    break;
1071                }
1072                total += pushed;
1073            }
1074        }
1075    }
1076
1077    /// One blocking-flow augmenting path, found iteratively so deep level graphs never exhaust the stack.
1078    fn augment(&mut self, s: usize, t: usize, limit: i64, level: &[usize], cursor: &mut [usize]) -> i64 {
1079        let mut path: Vec<usize> = Vec::new();
1080        let mut u = s;
1081        loop {
1082            if u == t {
1083                let pushed = path.iter().map(|&e| self.cap[e]).fold(limit, i64::min);
1084                for &e in &path {
1085                    self.cap[e] -= pushed;
1086                    self.cap[e ^ 1] += pushed;
1087                }
1088                return pushed;
1089            }
1090            let mut advanced = false;
1091            while cursor[u] != Self::NONE {
1092                let e = cursor[u];
1093                let v = self.to[e];
1094                if self.cap[e] > 0 && level[v] == level[u] + 1 {
1095                    path.push(e);
1096                    u = v;
1097                    advanced = true;
1098                    break;
1099                }
1100                cursor[u] = self.next[e];
1101            }
1102            if !advanced {
1103                if u == s {
1104                    return 0;
1105                }
1106                let e = path.pop().expect("a non-source node is entered by an edge");
1107                let back = self.to[e ^ 1];
1108                cursor[back] = self.next[cursor[back]];
1109                u = back;
1110            }
1111        }
1112    }
1113
1114    fn reachable(&self, s: usize) -> Vec<bool> {
1115        let mut seen = vec![false; self.head.len()];
1116        let mut stack = vec![s];
1117        seen[s] = true;
1118        while let Some(u) = stack.pop() {
1119            let mut e = self.head[u];
1120            while e != Self::NONE {
1121                if self.cap[e] > 0 && !seen[self.to[e]] {
1122                    seen[self.to[e]] = true;
1123                    stack.push(self.to[e]);
1124                }
1125                e = self.next[e];
1126            }
1127        }
1128        seen
1129    }
1130}
1131
1132/// What a reader at some stage reads: an input or a computed value, possibly through a chain of delay registers.
1133#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
1134enum Held {
1135    Input(usize),
1136    Value(usize),
1137}
1138
1139/// The registers a schedule needs: for each input and computed value, how many stages past its own its last reader
1140/// sits, through wiring. Also which ops the result depends on.
1141struct Registers {
1142    depth: std::collections::BTreeMap<Held, usize>,
1143    needed: Vec<bool>,
1144}
1145
1146fn registers(ops: &[FabricOp], sched: &PipelineSchedule) -> Registers {
1147    let n = ops.len();
1148    let needed = needed_by(ops, sched.result);
1149    let mut depth = std::collections::BTreeMap::new();
1150    fn want(
1151        ops: &[FabricOp],
1152        stage: &[usize],
1153        s: Src,
1154        at: usize,
1155        depth: &mut std::collections::BTreeMap<Held, usize>,
1156    ) {
1157        let (held, born) = match s {
1158            Src::Const(_) => return,
1159            Src::Val(k) if is_wiring(&ops[k]) => {
1160                want(ops, stage, ops[k].a, at, depth);
1161                want(ops, stage, ops[k].b, at, depth);
1162                return;
1163            }
1164            Src::Val(k) => (Held::Value(k), stage[k]),
1165            Src::In(k) => (Held::Input(k), 0),
1166        };
1167        let d = depth.entry(held).or_insert(0);
1168        *d = (*d).max(at - born);
1169    }
1170    for i in 0..n {
1171        if needed[i] && !is_wiring(&ops[i]) {
1172            want(ops, &sched.stage, ops[i].a, sched.stage[i], &mut depth);
1173            want(ops, &sched.stage, ops[i].b, sched.stage[i], &mut depth);
1174        }
1175    }
1176    want(
1177        ops,
1178        &sched.stage,
1179        Src::Val(sched.result),
1180        sched.stage[sched.result],
1181        &mut depth,
1182    );
1183    Registers { depth, needed }
1184}
1185
1186impl PipelineSchedule {
1187    /// The live bits the emitted pipeline's registers hold: each input register, each value's live bits once per
1188    /// stage it is delayed across, and the result register's live bits.
1189    pub fn register_bits(&self, ops: &[FabricOp]) -> u64 {
1190        let known = known_bits(ops);
1191        let held: u64 = registers(ops, self)
1192            .depth
1193            .iter()
1194            .map(|(&h, &d)| match h {
1195                Held::Input(_) => 64 * (d as u64 + u64::from(self.inputs == Inputs::Registered)),
1196                Held::Value(k) => live_bits(known[k]) * d as u64,
1197            })
1198            .sum();
1199        held + live_bits(known[self.result])
1200    }
1201
1202    /// The 64-bit registers the emitted pipeline holds: one per input when it registers them, one per stage each value
1203    /// is delayed across, and the result register.
1204    pub fn register_words(&self, ops: &[FabricOp]) -> usize {
1205        let regs = registers(ops, self);
1206        let inputs = match self.inputs {
1207            Inputs::Registered => regs.depth.keys().filter(|h| matches!(h, Held::Input(_))).count(),
1208            Inputs::Ports => 0,
1209        };
1210        inputs + regs.depth.values().sum::<usize>() + 1
1211    }
1212}
1213
1214fn held_name(h: Held, inputs: Inputs) -> String {
1215    match (h, inputs) {
1216        (Held::Input(k), Inputs::Registered) => format!("in{k}_r"),
1217        (Held::Input(k), Inputs::Ports) => format!("in{k}"),
1218        (Held::Value(k), _) => format!("v{k}"),
1219    }
1220}
1221
1222fn delayed_name(h: Held, d: usize, inputs: Inputs) -> String {
1223    if d == 0 {
1224        held_name(h, inputs)
1225    } else {
1226        format!("{}_d{d}", held_name(h, inputs))
1227    }
1228}
1229
1230fn const_verilog(c: i64) -> String {
1231    format!("64'h{:016x}", c as u64)
1232}
1233
1234struct Emission<'a> {
1235    ops: &'a [FabricOp],
1236    sched: &'a PipelineSchedule,
1237    rebuilt: std::collections::BTreeSet<(usize, usize)>,
1238    wires: String,
1239}
1240
1241impl Emission<'_> {
1242    /// The Verilog name a reader in stage `at` uses for `s`, emitting the rebuilt wiring it goes through first.
1243    fn operand(&mut self, s: Src, at: usize) -> String {
1244        match s {
1245            Src::Const(c) => const_verilog(c),
1246            Src::In(k) => delayed_name(Held::Input(k), at, self.sched.inputs),
1247            Src::Val(k) if is_wiring(&self.ops[k]) => {
1248                if self.rebuilt.insert((k, at)) {
1249                    let op = self.ops[k];
1250                    let a = self.operand(op.a, at);
1251                    let b = self.operand(op.b, at);
1252                    self.wires.push_str(&format!(
1253                        "  wire [63:0] w{k}_s{at} = {};\n",
1254                        op.op.verilog(&a, &b)
1255                    ));
1256                }
1257                format!("w{k}_s{at}")
1258            }
1259            Src::Val(k) => delayed_name(Held::Value(k), at - self.sched.stage[k], self.sched.inputs),
1260        }
1261    }
1262}
1263
1264/// Emit the scheduled residual as a pipeline: module `fabric_{name}` with ports `clk`, `in0..`, and the registered
1265/// `result`. Every assignment is its own net, so no operator's signedness leaks into another's.
1266pub fn emit_fabric_pipelined_verilog(
1267    name: &str,
1268    n_in: usize,
1269    ops: &[FabricOp],
1270    sched: &PipelineSchedule,
1271) -> String {
1272    sched
1273        .check(ops)
1274        .unwrap_or_else(|e| panic!("emitting an invalid schedule: {e}"));
1275    let regs = registers(ops, sched);
1276    let mut decls = String::new();
1277    let mut updates = String::new();
1278    let mode = sched.inputs;
1279    for (&held, &depth) in &regs.depth {
1280        if let (Held::Input(k), Inputs::Registered) = (held, mode) {
1281            decls.push_str(&format!("  reg [63:0] in{k}_r;\n"));
1282            updates.push_str(&format!("    in{k}_r <= in{k};\n"));
1283        }
1284        for d in 1..=depth {
1285            decls.push_str(&format!("  reg [63:0] {};\n", delayed_name(held, d, mode)));
1286            updates.push_str(&format!(
1287                "    {} <= {};\n",
1288                delayed_name(held, d, mode),
1289                delayed_name(held, d - 1, mode)
1290            ));
1291        }
1292    }
1293    let mut em = Emission {
1294        ops,
1295        sched,
1296        rebuilt: Default::default(),
1297        wires: String::new(),
1298    };
1299    for (i, op) in ops.iter().enumerate() {
1300        if !regs.needed[i] || is_wiring(op) {
1301            continue;
1302        }
1303        let at = sched.stage[i];
1304        let a = em.operand(op.a, at);
1305        let b = em.operand(op.b, at);
1306        em.wires.push_str(&format!(
1307            "  wire [63:0] v{i} = {};\n",
1308            op.op.verilog(&a, &b)
1309        ));
1310    }
1311    let out = em.operand(Src::Val(sched.result), sched.stage[sched.result]);
1312    updates.push_str(&format!("    result <= {out};\n"));
1313    let inputs = (0..n_in)
1314        .map(|k| format!("input [63:0] in{k}"))
1315        .collect::<Vec<_>>()
1316        .join(", ");
1317    let reads = match sched.inputs {
1318        Inputs::Registered => "",
1319        Inputs::Ports => "\n// Its first stage reads the ports: the registers that drive them hold its inputs, so none are registered twice.",
1320    };
1321    format!(
1322        "// fabric_{name} — a compiled dispatch-free residual as a {stages}-stage pipeline: a new input every clock,\n\
1323         // its result {latency} clocks later. Lowered from the REAL compiler's residual; multiplies and sums are\n\
1324         // carry-save rows reduced by 3:2 compressor trees and resolved by Kogge-Stone adds, cut into stages of at\n\
1325         // most {budget} XOR-depth units. 64-bit, so it wraps identically to the VM's Int; proven == the running VM by\n\
1326         // iverilog, streaming.{reads}\n\
1327         module fabric_{name}(input clk, {inputs}, output reg [63:0] result);\n\
1328         {decls}{wires}  always @(posedge clk) begin\n\
1329         {updates}  end\n\
1330         endmodule\n",
1331        stages = sched.stages(),
1332        latency = sched.latency(),
1333        budget = sched.budget,
1334        wires = em.wires,
1335    )
1336}
1337
1338/// The cycle model of the emitted pipeline. Before each clock `t` the input vector `stream[t]` is presented; the
1339/// returned vector holds the result register after each clock. Registers start at zero.
1340pub fn simulate_pipeline(
1341    ops: &[FabricOp],
1342    sched: &PipelineSchedule,
1343    n_in: usize,
1344    stream: &[Vec<i64>],
1345) -> Vec<i64> {
1346    sched
1347        .check(ops)
1348        .unwrap_or_else(|e| panic!("simulating an invalid schedule: {e}"));
1349    let regs = registers(ops, sched);
1350    let mut held: std::collections::BTreeMap<(Held, usize), i64> = regs
1351        .depth
1352        .iter()
1353        .flat_map(|(&h, &depth)| {
1354            let first = if matches!(h, Held::Input(_)) && sched.inputs == Inputs::Registered { 0 } else { 1 };
1355            (first..=depth).map(move |d| ((h, d), 0i64))
1356        })
1357        .collect();
1358    let mut result_reg = 0i64;
1359    let mut out = Vec::with_capacity(stream.len());
1360    for inputs in stream {
1361        assert_eq!(inputs.len(), n_in, "every clock presents all {n_in} inputs");
1362        let mut comb = vec![0i64; ops.len()];
1363        let ports = (sched.inputs == Inputs::Ports).then_some(inputs.as_slice());
1364        #[allow(clippy::too_many_arguments)]
1365        fn read(
1366            ops: &[FabricOp],
1367            stage: &[usize],
1368            comb: &[i64],
1369            held: &std::collections::BTreeMap<(Held, usize), i64>,
1370            ports: Option<&[i64]>,
1371            s: Src,
1372            at: usize,
1373        ) -> i64 {
1374            match s {
1375                Src::Const(c) => c,
1376                Src::In(k) => match (ports, at) {
1377                    (Some(now), 0) => now[k],
1378                    _ => held[&(Held::Input(k), at)],
1379                },
1380                Src::Val(k) if is_wiring(&ops[k]) => {
1381                    let op = ops[k];
1382                    op.op.eval(
1383                        read(ops, stage, comb, held, ports, op.a, at),
1384                        read(ops, stage, comb, held, ports, op.b, at),
1385                    )
1386                }
1387                Src::Val(k) if at == stage[k] => comb[k],
1388                Src::Val(k) => held[&(Held::Value(k), at - stage[k])],
1389            }
1390        }
1391        for (i, op) in ops.iter().enumerate() {
1392            if regs.needed[i] && !is_wiring(op) {
1393                let at = sched.stage[i];
1394                comb[i] = op.op.eval(
1395                    read(ops, &sched.stage, &comb, &held, ports, op.a, at),
1396                    read(ops, &sched.stage, &comb, &held, ports, op.b, at),
1397                );
1398            }
1399        }
1400        let next_result = read(
1401            ops,
1402            &sched.stage,
1403            &comb,
1404            &held,
1405            ports,
1406            Src::Val(sched.result),
1407            sched.stage[sched.result],
1408        );
1409        let mut next = held.clone();
1410        for (&(h, d), v) in next.iter_mut() {
1411            *v = match (h, d) {
1412                (Held::Input(k), 0) => inputs[k],
1413                (Held::Input(k), 1) if sched.inputs == Inputs::Ports => inputs[k],
1414                (Held::Value(k), 1) => comb[k],
1415                (h, d) => held[&(h, d - 1)],
1416            };
1417        }
1418        held = next;
1419        result_reg = next_result;
1420        out.push(result_reg);
1421    }
1422    out
1423}
1424
1425/// The stage budget `fabric_horner_seq` is cut to, in the units of [`op_delay`]. MEASURED (2026-10-17, the
1426/// synthesis sequence of OpenLane's synth.tcl under host yosys 0.33, AREA 0, abc target 1 ns, sky130_fd_sc_hd
1427/// tt_025C_1v80, worst stage by abc `stime`): budget 4 is 1.36 ns over 311,772 µm² of cells, 5 is 1.43 ns over
1428/// 258,289 µm², 6 is 1.99 ns, 7 is 2.65 ns; the single-stage residual is 13.85 ns. Five is the knee: the next
1429/// tighter budget buys 5% of period for 21% more area.
1430pub const HORNER_SEQ_BUDGET: f64 = 5.0;
1431
1432/// The residual `fabric_horner_seq` hardens: the REAL compiler's residual of the Horner polynomial
1433/// ([`POLY_BODY`](super::fabric_rtl::POLY_BODY)), carry-save lowered at 64 bits and cut to [`HORNER_SEQ_BUDGET`].
1434pub fn horner_seq_pipeline() -> (Vec<FabricOp>, PipelineSchedule) {
1435    let (ops, result, _) = residual_from_source(&poly_program("Show poly(4)."), "poly");
1436    let (lowered, result) = carry_save_lower(&ops, result, 64);
1437    let sched = schedule_pipeline(&lowered, result, HORNER_SEQ_BUDGET);
1438    (lowered, sched)
1439}
1440
1441/// `benchmarks/silicon/fabric_horner_seq.v`: the RTL the flow hardens as `fabric_horner_seq`.
1442pub fn emit_horner_seq_verilog() -> String {
1443    let (ops, sched) = horner_seq_pipeline();
1444    emit_fabric_pipelined_verilog("horner_seq", 1, &ops, &sched)
1445}
1446
1447/// The `u`-wide unrolled int8 dot product in LOGOS: each iteration accumulates `u` independent products, so the
1448/// compiler places `u` multiply tiles that fire together, `u` MACs per clock.
1449pub fn dot_unrolled_source(u: usize) -> String {
1450    let terms: Vec<String> = (0..u)
1451        .map(|j| {
1452            if j == 0 {
1453                "k * (n - k)".into()
1454            } else {
1455                format!("(k + {j}) * (n - k - {j})")
1456            }
1457        })
1458        .collect();
1459    format!(
1460        "## To dotU (n: Int) -> Int:\n    Let mutable s be 0.\n    Repeat for k from 1 to n:\n        Set s to s + {}.\n    Return s.\n",
1461        terms.join(" + ")
1462    )
1463}
1464
1465/// A pin-light top for a MAC array's CGRA `cgra_{name}`, so placement and routing measure the array's paths rather
1466/// than its I/O: a serial input shifts into the loop bound, the finished result latches, its parity drives one pin.
1467pub fn matmul_array_top(name: &str) -> String {
1468    format!(
1469        "module matmul_array_top(input clk, input rst, input serin, output reg serout);\n\
1470        \x20 reg [63:0] in0 = 64'd0;\n  wire [63:0] result; wire done;\n  reg [63:0] acc = 64'd0;\n\
1471        \x20 cgra_{name} dut(.clk(clk), .rst(rst), .in0(in0), .result(result), .done(done));\n\
1472        \x20 always @(posedge clk) begin\n    in0 <= {{in0[62:0], serin}};\n    if (done) acc <= result;\n    serout <= ^acc;\n  end\nendmodule\n"
1473    )
1474}
1475
1476/// The int8 width of the GEMM array's multiply tiles.
1477pub const GEMM_MUL_BITS: u32 = 8;
1478
1479/// The term stage budget of the GEMM array, in the units of `loop_cell_delay`: the operand arithmetic in one stage,
1480/// the int8 multiplies in the next.
1481pub const GEMM_TERM_BUDGET: f64 = 2.0;
1482
1483/// `benchmarks/silicon/gemm_mac_array.v`: the eight-wide int8 MAC array `cgra_arr8` as the pipelined loop CGRA
1484/// (operands, then products, ahead of a carry-save accumulator), with its pin-light top.
1485pub fn gemm_mac_array_verilog() -> String {
1486    let lf = loop_fabric_from_source(&dot_unrolled_source(8), "dotU");
1487    let pipelined = emit_loop_fabric_verilog_pipelined("arr8", &lf, Some(GEMM_MUL_BITS), GEMM_TERM_BUDGET)
1488        .unwrap_or_else(|why| panic!("the dot-product loop takes the pipelined form: {why}"));
1489    format!("{}\n{}", pipelined.verilog, matmul_array_top("arr8"))
1490}
1491
1492/// A parameterized Verilog module `{module}`: an unsigned AW×BW multiplier as a tree of 3:2 compressors over its
1493/// partial-product rows, registered after CUT tree levels and (with FINAL) once more before a Kogge-Stone final add.
1494/// One generator for every consumer, so the BigInt limb multiplier and the FPU carry the same tree under their own
1495/// names.
1496pub fn emit_csa_multiplier_verilog(module: &str) -> String {
1497    format!(
1498r#"// {module} — an unsigned AW×BW multiplier as a tree of 3:2 compressors over its BW partial-product rows. The
1499// compressor outputs register after CUT levels (CUT = 0 or beyond the tree's depth: no register inside it), the last
1500// two words register again when FINAL is set, and those two words resolve through a Kogge-Stone add. `p` is the
1501// product of the operands sampled as many clocks earlier as there are registers on its path.
1502module {module} #(parameter integer AW = 53, parameter integer BW = 53, parameter integer CUT = 4,
1503                 parameter integer FINAL = 1) (
1504  input                clk,
1505  input  [AW-1:0]      a,
1506  input  [BW-1:0]      b,
1507  output [AW+BW-1:0]   p
1508);
1509  localparam integer W = AW + BW;
1510
1511  // The words entering tree level `level`: each level turns every three words into two and passes the rest.
1512  function integer words(input integer level);
1513    integer n, l;
1514    begin
1515      n = BW;
1516      for (l = 0; l < level; l = l + 1) n = 2 * (n / 3) + (n % 3);
1517      words = n;
1518    end
1519  endfunction
1520
1521  function integer depth(input integer unused);
1522    integer n, l;
1523    begin
1524      n = BW; l = 0;
1525      while (n > 2) begin n = 2 * (n / 3) + (n % 3); l = l + 1; end
1526      depth = l;
1527    end
1528  endfunction
1529
1530  localparam integer L = depth(0);
1531
1532  // Level-major bus: level `l` holds its words at word slots l*BW .. l*BW + words(l) - 1.
1533  wire [W*BW*(L+1)-1:0] lv;
1534
1535  genvar i, l, g;
1536  generate
1537    for (i = 0; i < BW; i = i + 1) begin : row
1538      assign lv[i*W +: W] = b[i] ? ({{{{BW{{1'b0}}}}, a}} << i) : {{W{{1'b0}}}};
1539    end
1540    for (l = 0; l < L; l = l + 1) begin : level
1541      localparam integer N = words(l);
1542      localparam integer G = N / 3;
1543      for (g = 0; g < G; g = g + 1) begin : compress
1544        wire [W-1:0] x = lv[((l*BW) + 3*g) * W +: W];
1545        wire [W-1:0] y = lv[((l*BW) + 3*g + 1) * W +: W];
1546        wire [W-1:0] z = lv[((l*BW) + 3*g + 2) * W +: W];
1547        wire [W-1:0] xy = x ^ y;
1548        wire [W-1:0] s = xy ^ z;
1549        wire [W-1:0] c = ((x & y) | (z & xy)) << 1;
1550        if (l + 1 == CUT) begin : registered
1551          reg [W-1:0] sr, cr;
1552          always @(posedge clk) begin sr <= s; cr <= c; end
1553          assign lv[(((l+1)*BW) + 2*g) * W +: W] = sr;
1554          assign lv[(((l+1)*BW) + 2*g + 1) * W +: W] = cr;
1555        end else begin : wired
1556          assign lv[(((l+1)*BW) + 2*g) * W +: W] = s;
1557          assign lv[(((l+1)*BW) + 2*g + 1) * W +: W] = c;
1558        end
1559      end
1560      for (g = 0; g < N % 3; g = g + 1) begin : carry_over
1561        if (l + 1 == CUT) begin : registered
1562          reg [W-1:0] r;
1563          always @(posedge clk) r <= lv[((l*BW) + 3*G + g) * W +: W];
1564          assign lv[(((l+1)*BW) + 2*G + g) * W +: W] = r;
1565        end else begin : wired
1566          assign lv[(((l+1)*BW) + 2*G + g) * W +: W] = lv[((l*BW) + 3*G + g) * W +: W];
1567        end
1568      end
1569    end
1570  endgenerate
1571
1572  wire [W-1:0] s_word = lv[(L*BW) * W +: W];
1573  wire [W-1:0] c_word;
1574  generate
1575    if (BW == 1) begin : one_row
1576      assign c_word = {{W{{1'b0}}}};
1577    end else begin : two_rows
1578      assign c_word = lv[(L*BW + 1) * W +: W];
1579    end
1580  endgenerate
1581  reg  [W-1:0] s_r, c_r;
1582  always @(posedge clk) begin s_r <= s_word; c_r <= c_word; end
1583  wire [W-1:0] s_in = FINAL ? s_r : s_word;
1584  wire [W-1:0] c_in = FINAL ? c_r : c_word;
1585
1586  // The Kogge-Stone add of the last two words: group generate and propagate doubled in span each level.
1587  function [W-1:0] prefix_add(input [W-1:0] u, input [W-1:0] v);
1588    reg [W-1:0] gg, pp, p0; integer d;
1589    begin
1590      gg = u & v; p0 = u ^ v; pp = p0;
1591      for (d = 1; d < W; d = d * 2) begin
1592        gg = gg | (pp & (gg << d));
1593        pp = pp & (pp << d);
1594      end
1595      prefix_add = p0 ^ (gg << 1);
1596    end
1597  endfunction
1598
1599  assign p = prefix_add(s_in, c_in);
1600endmodule
1601"#
1602    )
1603}