Skip to main content

logicaffeine_compile/codegen_sva/
sequencer_rtl.rs

1//! sequencer_rtl — Stage 1 of the self-synthesizing bytecode XPU: the AUTONOMOUS block-walk sequencer as
2//! synthesizable RTL. The CONTROL PLANE that `run_program_cfg_on_grid` modelled in software now exists in
3//! silicon: a register file that survives block reconfiguration, per-block placed grids, a block program-
4//! counter, a terminator FSM (Jump/Branch/Return), and a cycle counter — a hardwired micro-sequencer that
5//! walks the whole function itself, one block per clock, with no host help.
6//!
7//! It is the RTL of exactly `run_program_cfg_on_grid`'s loop (`fabric_grid.rs`): each basic block's
8//! straight-line body is PLACED on a fresh tile array by `route_loop_body` (the block's live-in registers as
9//! inputs; its register writes then its terminator operand as the grid's outputs, in that order — the
10//! `all_outs()` order the placer guarantees). Every block's grid is a combinational island wired to the
11//! CURRENT register file; on each clock the sequencer latches the ACTIVE block's writes back into the file
12//! and follows its terminator: a `Jump` sets the next block, a `Branch` lets the block's predicate output
13//! pick the successor, a `Return` captures the operand and halts. So the whole branchy program runs as
14//! spatial dataflow driven by a tiny sequencer — proven bit-identical to `run_program_cfg_on_grid`
15//! (≡ `eval_program_cfg_fabric` ≡ the VM) under iverilog by `fabric_sequencer_lock`.
16//!
17//! The block grids are emitted by the existing `emit_grid_verilog` (multi-output form); this module adds
18//! only the register file + sequencing FSM around them. Blocks are Arith/Select/Func islands (the stateless
19//! control plane); the heap state plane is the Stage-2 follow-on (`sequencer_rtl` gains a shared BRAM there).
20
21use super::fabric_grid::{emit_grid_verilog, eval_grid_all_with_heap, route_loop_body, Grid};
22use super::fabric_rtl::{ProgramCfgFabric, Src, Term};
23
24/// Evaluate a whole-function CFG fabric on the sequencer's model and return `(result, block_steps)`. This IS
25/// `run_program_cfg_on_grid` — the same place-per-block (`route_loop_body`) + `eval_grid_all` walk over a
26/// register file — with a step counter added, so the returned value is identical to the grid walk (≡ the CFG
27/// model ≡ the VM) and `block_steps` is the exact number of block executions the RTL must take (the
28/// trajectory, not merely the answer). The reference the iverilog differential checks against.
29pub fn eval_sequencer(pf: &ProgramCfgFabric, params: &[i64]) -> (i64, u64) {
30    use std::collections::HashMap;
31    let mut file: HashMap<u16, i64> = (0..pf.n_params).map(|p| (p as u16, params[p])).collect();
32    // The shared state-plane heap, threaded across every block exactly as `run_program_cfg_on_grid` does.
33    let mut heap: Vec<i64> = Vec::new();
34    let mut blk = 0usize;
35    let mut steps = 0u64;
36    for _ in 0..10_000_000 {
37        steps += 1;
38        let b = &pf.blocks[blk];
39        let inputs: Vec<i64> = b
40            .inputs
41            .iter()
42            .map(|r| *file.get(r).unwrap_or(&0))
43            .collect();
44        let mut needed: Vec<Src> = b.writes.iter().map(|(_, s)| *s).collect();
45        let term_off = needed.len();
46        match &b.term {
47            Term::Jump(_) => {}
48            Term::Branch { cond, .. } => needed.push(*cond),
49            Term::Return(s) => needed.push(*s),
50        }
51        let g = route_loop_body(&b.body, b.inputs.len(), &needed);
52        let outs = eval_grid_all_with_heap(&g, &inputs, &mut heap);
53        for (i, (r, _)) in b.writes.iter().enumerate() {
54            file.insert(*r, outs[i]);
55        }
56        match &b.term {
57            Term::Jump(t) => blk = *t,
58            Term::Branch {
59                then_blk, else_blk, ..
60            } => {
61                blk = if outs[term_off] != 0 {
62                    *then_blk
63                } else {
64                    *else_blk
65                }
66            }
67            Term::Return(_) => return (outs[term_off], steps),
68        }
69    }
70    panic!("sequencer: controller did not return within the step budget (a back-edge slipped through?)")
71}
72
73/// The register-file width: one 64-bit word per distinct register the program touches (live-ins + writes),
74/// never fewer than the parameter count (params occupy `file[0..n_params]`). At least one word.
75fn n_regs(pf: &ProgramCfgFabric) -> usize {
76    let mut max_reg = 0u16;
77    for b in &pf.blocks {
78        for r in &b.inputs {
79            max_reg = max_reg.max(*r);
80        }
81        for (r, _) in &b.writes {
82            max_reg = max_reg.max(*r);
83        }
84    }
85    (max_reg as usize + 1).max(pf.n_params).max(1)
86}
87
88/// Smallest bit width that can index `n` blocks (≥ 1).
89fn clog2(n: usize) -> usize {
90    let mut b = 1;
91    while (1usize << b) < n {
92        b += 1;
93    }
94    b
95}
96
97/// The `next`/output list for a block: each register write's `Src`, then (last) the terminator's
98/// predicate/return operand — exactly the order `run_program_cfg_on_grid` places as the grid's outputs, so
99/// `all_outs()[i]` is write `i` and `all_outs()[term_off]` is the terminator operand.
100fn block_needed(b: &super::fabric_rtl::CfgBlock) -> (Vec<Src>, usize) {
101    let mut needed: Vec<Src> = b.writes.iter().map(|(_, s)| *s).collect();
102    let term_off = needed.len();
103    match &b.term {
104        Term::Jump(_) => {}
105        Term::Branch { cond, .. } => needed.push(*cond),
106        Term::Return(s) => needed.push(*s),
107    }
108    (needed, term_off)
109}
110
111/// Emit the autonomous block-walk sequencer for a whole-function CFG fabric: the per-block placed-grid
112/// modules followed by a `seq_{name}` top that carries the register file + block-PC + terminator FSM +
113/// cycle counter. One block step per clock; `done` pulses high with the return value on `result` and the
114/// block-step count on `cycles`.
115pub fn emit_sequencer_verilog(name: &str, pf: &ProgramCfgFabric) -> String {
116    let nb = pf.blocks.len();
117    assert!(nb >= 1, "sequencer: a program needs at least one block");
118    let nreg = n_regs(pf);
119    let pcw = clog2(nb);
120
121    // Per-block placed grid + its output arity.
122    let mut grids: Vec<(Grid, usize, usize)> = Vec::with_capacity(nb); // (grid, term_off, n_out)
123    let mut modules = String::new();
124    for (k, b) in pf.blocks.iter().enumerate() {
125        let (needed, term_off) = block_needed(b);
126        assert!(
127            !needed.is_empty(),
128            "sequencer: block {k} of `{name}` has no outputs (no register writes and an unconditional Jump) — \
129             the honest bound (route_loop_body needs ≥1 output tap); a real block writes a register or ends in \
130             a conditional/return terminator"
131        );
132        let g = route_loop_body(&b.body, b.inputs.len(), &needed);
133        let n_out = g.all_outs().len();
134        modules.push_str(&emit_grid_verilog(&format!("seq_{name}_b{k}"), &g));
135        modules.push('\n');
136        grids.push((g, term_off, n_out));
137    }
138
139    // seq_{name} port list.
140    let mut ports: Vec<String> = vec!["input clk".into(), "input rst".into()];
141    for p in 0..pf.n_params {
142        ports.push(format!("input [63:0] p{p}"));
143    }
144    ports.push("output [63:0] result".into());
145    ports.push("output done".into());
146    ports.push("output [63:0] cycles".into());
147
148    let mut body = String::new();
149    body.push_str(&format!("  reg [63:0] file [0:{}];\n", nreg - 1));
150    body.push_str(&format!("  reg [{}:0] pc;\n", pcw - 1));
151    body.push_str("  reg done_r;\n  reg [63:0] result_r;\n  reg [63:0] cyc_r;\n");
152    body.push_str(
153        "  assign done = done_r;\n  assign result = result_r;\n  assign cycles = cyc_r;\n\n",
154    );
155
156    // Continuous reads of the register file — clean nets for the block-grid input ports.
157    for r in 0..nreg {
158        body.push_str(&format!("  wire [63:0] fr_{r} = file[{r}];\n"));
159    }
160    body.push('\n');
161
162    // Per-block combinational grid instances, wired from the current register file.
163    for (k, b) in pf.blocks.iter().enumerate() {
164        let (_g, _term_off, n_out) = &grids[k];
165        for o in 0..*n_out {
166            body.push_str(&format!("  wire [63:0] b{k}_o{o};\n"));
167        }
168        let mut conns: Vec<String> = Vec::new();
169        for (j, r) in b.inputs.iter().enumerate() {
170            conns.push(format!(".in{j}(fr_{r})"));
171        }
172        if *n_out == 1 {
173            conns.push(format!(".result(b{k}_o0)"));
174        } else {
175            for o in 0..*n_out {
176                conns.push(format!(".out{o}(b{k}_o{o})"));
177            }
178        }
179        body.push_str(&format!(
180            "  grid_seq_{name}_b{k} u_b{k} ({});\n\n",
181            conns.join(", ")
182        ));
183    }
184
185    // The sequencing FSM.
186    body.push_str("  always @(posedge clk) begin\n");
187    body.push_str("    if (rst) begin\n");
188    body.push_str(&format!("      pc <= {pcw}'d0;\n      done_r <= 1'b0;\n      result_r <= 64'd0;\n      cyc_r <= 64'd0;\n"));
189    for r in 0..nreg {
190        if r < pf.n_params {
191            body.push_str(&format!("      file[{r}] <= p{r};\n"));
192        } else {
193            body.push_str(&format!("      file[{r}] <= 64'd0;\n"));
194        }
195    }
196    body.push_str("    end else if (!done_r) begin\n");
197    body.push_str("      cyc_r <= cyc_r + 64'd1;\n");
198    body.push_str("      case (pc)\n");
199    for (k, b) in pf.blocks.iter().enumerate() {
200        let (_g, term_off, _n_out) = &grids[k];
201        body.push_str(&format!("        {pcw}'d{k}: begin\n"));
202        for (i, (r, _)) in b.writes.iter().enumerate() {
203            body.push_str(&format!("          file[{r}] <= b{k}_o{i};\n"));
204        }
205        match &b.term {
206            Term::Jump(t) => {
207                body.push_str(&format!("          pc <= {pcw}'d{t};\n"));
208            }
209            Term::Branch {
210                then_blk, else_blk, ..
211            } => {
212                body.push_str(&format!(
213                    "          pc <= (b{k}_o{term_off} != 64'd0) ? {pcw}'d{then_blk} : {pcw}'d{else_blk};\n"
214                ));
215            }
216            Term::Return(_) => {
217                body.push_str(&format!(
218                    "          result_r <= b{k}_o{term_off};\n          done_r <= 1'b1;\n"
219                ));
220            }
221        }
222        body.push_str("        end\n");
223    }
224    body.push_str("        default: done_r <= 1'b1;\n");
225    body.push_str("      endcase\n");
226    body.push_str("    end\n  end\n");
227
228    format!(
229        "// seq_{name} — the AUTONOMOUS block-walk sequencer: a register file + block program-counter +\n\
230         // terminator FSM (Jump/Branch/Return) + cycle counter around {nb} per-block placed grids. Walks the\n\
231         // whole function itself, one block per clock, ≡ run_program_cfg_on_grid ≡ the VM.\n\
232         {modules}\
233         module seq_{name}(\n    {ports}\n);\n{body}endmodule\n",
234        ports = ports.join(",\n    "),
235    )
236}
237
238/// A self-checking testbench for the sequencer: for each vector, load the params, pulse reset, run the FSM
239/// to `done`, and compare BOTH `result` and `cycles` to the VM oracle in `seq_{name}_{out,cyc}.hex`
240/// (inputs in `seq_{name}_in.hex`). The step-count check pins the trajectory, not just the answer.
241pub fn emit_sequencer_testbench(name: &str, n_params: usize, n_vectors: usize) -> String {
242    let mut decls = String::new();
243    if n_params > 0 {
244        decls.push_str(&format!(
245            "  reg [63:0] pin [0:{}];\n",
246            n_params * n_vectors - 1
247        ));
248    }
249    decls.push_str(&format!("  reg [63:0] oout [0:{}];\n", n_vectors - 1));
250    decls.push_str(&format!("  reg [63:0] ocyc [0:{}];\n", n_vectors - 1));
251    for p in 0..n_params {
252        decls.push_str(&format!("  reg [63:0] p{p};\n"));
253    }
254
255    let mut dut_ports: Vec<String> = vec![".clk(clk)".into(), ".rst(rst)".into()];
256    for p in 0..n_params {
257        dut_ports.push(format!(".p{p}(p{p})"));
258    }
259    dut_ports.push(".result(result)".into());
260    dut_ports.push(".done(done)".into());
261    dut_ports.push(".cycles(cycles)".into());
262
263    let readin = if n_params > 0 {
264        format!("    $readmemh(\"seq_{name}_in.hex\", pin);\n")
265    } else {
266        String::new()
267    };
268    let mut load = String::new();
269    for p in 0..n_params {
270        load.push_str(&format!("      p{p} = pin[i*{n_params}+{p}];\n"));
271    }
272
273    format!(
274        r#"// tb_seq_{name} — the autonomous sequencer vs the RUNNING VM (result + exact block-step count).
275`timescale 1ns/1ps
276module tb_seq_{name};
277  reg clk = 1'b0;
278  reg rst;
279{decls}  wire [63:0] result;
280  wire done;
281  wire [63:0] cycles;
282  integer i, errors, guard;
283  seq_{name} dut({ports});
284  always #5 clk = ~clk;
285  initial begin
286    errors = 0;
287{readin}    $readmemh("seq_{name}_out.hex", oout);
288    $readmemh("seq_{name}_cyc.hex", ocyc);
289    for (i = 0; i < {n_vectors}; i = i + 1) begin
290{load}      rst = 1'b1; @(posedge clk); #1;
291      rst = 1'b0;
292      guard = 0;
293      while (done !== 1'b1 && guard < 100000) begin @(posedge clk); #1; guard = guard + 1; end
294      if (result !== oout[i]) begin errors = errors + 1; $display("FAIL case %0d: result got %0d exp %0d", i, result, oout[i]); end
295      if (cycles !== ocyc[i]) begin errors = errors + 1; $display("FAIL case %0d: cycles got %0d exp %0d", i, cycles, ocyc[i]); end
296    end
297    if (errors == 0) $display("PASS: {n_vectors} sequencer runs match the VM (result + cycle count)");
298    else $display("FAIL: %0d sequencer mismatches", errors);
299    $finish;
300  end
301endmodule
302"#,
303        ports = dut_ports.join(", "),
304    )
305}