Skip to main content

logicaffeine_compile/codegen_sva/
reconfig_fabric_rtl.rs

1//! reconfig_fabric_rtl — Stage 4 of the self-synthesizing bytecode XPU: the placer IN SILICON as a
2//! RECONFIGURABLE fabric. Stage 1 HARDWIRED a grid per block; Stage 3 proved `route_loop_body` correct for
3//! every bounded body. This stage makes the fabric CONFIG-DRIVEN: ONE fixed R×C tile array whose every
4//! tile's operation and channel routing are RUNTIME inputs, so loading the certified placement of ANY block
5//! runs it on the same physical silicon — the reconfigurable array the on-chip placer configures.
6//!
7//! A tile's config is a 32-bit control word (alu op + the two operand sources + the vs/he channel drives)
8//! plus two 64-bit constant slots (for `Const` operands). `serialize_grid_config` transcribes a placed
9//! `Grid` (from `route_loop_body`, padded to the array size) into that config; the emitted fabric decodes it
10//! per tile and computes the same feed-forward dataflow the hardwired grid does. Proven ≡ the VM under
11//! iverilog by `reconfig_fabric_lock`: for a corpus of straight-line programs, the certified placement loaded
12//! into ONE universal array computes exactly what the VM computes.
13//!
14//! Correct-first scope: Arith-tile grids (the Stage-3 certified family) with a single `Val` output — the
15//! spatial place-and-route the array runs is `route_loop_body`'s canonical placement. Select/Func/Mem tiles
16//! and the on-chip placement of the config (the placement decode moving from host to chip) are the
17//! follow-ons; here the certified placement is loaded, and the ARRAY is proven universal + reconfigurable.
18
19use super::fabric_grid::{CellKind, Chan, Drive, Grid, Src2};
20use super::fabric_rtl::AluOp;
21
22/// A placed grid transcribed into the reconfigurable fabric's config: one 32-bit control word per tile, two
23/// 64-bit constant slots per tile (operand `a`/`b` `Const` values), and the output tap (`out_sel` = flat tile
24/// index, `out_chan` = 0 for the vertical channel / 1 for the horizontal).
25pub struct ReconfigConfig {
26    pub ctrl: Vec<u32>,
27    pub sac: Vec<i64>,
28    pub sbc: Vec<i64>,
29    pub out_sel: usize,
30    pub out_chan: u8,
31}
32
33fn alu_code(op: AluOp) -> u32 {
34    match op {
35        AluOp::Add => 0,
36        AluOp::Sub => 1,
37        AluOp::Mul => 2,
38        AluOp::And => 3,
39        AluOp::Or => 4,
40        AluOp::Xor => 5,
41        AluOp::Shl => 6,
42        AluOp::Shr => 7,
43        AluOp::UShr => 8,
44    }
45}
46
47/// Encode an operand source as `(mode, in_index, const)` — mode 0=Zero, 1=N, 2=W, 3=In, 4=Const.
48fn enc_src2(s: Src2) -> (u32, u32, i64) {
49    match s {
50        Src2::Zero => (0, 0, 0),
51        Src2::N => (1, 0, 0),
52        Src2::W => (2, 0, 0),
53        Src2::In(k) => (3, k as u32, 0),
54        Src2::Const(c) => (4, 0, c),
55    }
56}
57
58/// Encode a channel drive as a 2-bit mode: 0=Zero, 1=N, 2=W, 3=Alu. (Arith-body grids with a `Val` output
59/// drive their channels only with these — a routed input/const in a channel is the In/Const-output case,
60/// excluded from the Stage-4 correct-first scope.)
61fn enc_drive(d: Drive) -> u32 {
62    match d {
63        Drive::Alu => 3,
64        Drive::Pass(Src2::Zero) => 0,
65        Drive::Pass(Src2::N) => 1,
66        Drive::Pass(Src2::W) => 2,
67        Drive::Pass(other) => panic!(
68            "reconfig fabric: channel drive Pass({other:?}) is outside the correct-first scope \
69             (Arith-body grids drive channels with Zero/N/W/Alu only)"
70        ),
71    }
72}
73
74/// Transcribe a placed `Grid` (Arith tiles, single `Val` output) into the reconfigurable fabric's config,
75/// laid out row-major over `rows*cols` tiles (the grid must already be padded to `rows`×`cols`).
76pub fn serialize_grid_config(g: &Grid) -> ReconfigConfig {
77    let rc = g.rows * g.cols;
78    let mut ctrl = vec![0u32; rc];
79    let mut sac = vec![0i64; rc];
80    let mut sbc = vec![0i64; rc];
81    for r in 0..g.rows {
82        for c in 0..g.cols {
83            let idx = r * g.cols + c;
84            let t = g.tiles[r][c];
85            let (op, sam, sai, sac_v, sbm, sbi, sbc_v) = match t.alu {
86                None => (0u32, 0u32, 0u32, 0i64, 0u32, 0u32, 0i64),
87                Some(CellKind::Arith(op, a, b)) => {
88                    let (am, ai, ac) = enc_src2(a);
89                    let (bm, bi, bc) = enc_src2(b);
90                    (alu_code(op), am, ai, ac, bm, bi, bc)
91                }
92                Some(other) => panic!(
93                    "reconfig fabric: tile {other:?} is outside the Stage-4 correct-first scope (Arith grids)"
94                ),
95            };
96            let vs = enc_drive(t.vs);
97            let he = enc_drive(t.he);
98            ctrl[idx] =
99                op | (sam << 4) | (sai << 7) | (sbm << 11) | (sbi << 14) | (vs << 18) | (he << 20);
100            sac[idx] = sac_v;
101            sbc[idx] = sbc_v;
102        }
103    }
104    let (or, oc, och) = g.out;
105    ReconfigConfig {
106        ctrl,
107        sac,
108        sbc,
109        out_sel: or * g.cols + oc,
110        out_chan: if matches!(och, Chan::He) { 1 } else { 0 },
111    }
112}
113
114/// The ternary chain selecting `in{k}` by the 4-bit `in_index` field `field` of a tile's control word.
115fn in_mux(field: &str, n_in: usize) -> String {
116    if n_in == 0 {
117        return "64'sd0".to_string();
118    }
119    let mut acc = "64'sd0".to_string();
120    for k in (0..n_in).rev() {
121        acc = format!("({field}==4'd{k}) ? in{k} : ({acc})");
122    }
123    acc
124}
125
126/// Emit the universal reconfigurable fabric: a fixed `rows`×`cols` config-driven CGRA. Each tile decodes its
127/// control word (loaded via `$readmemh`) into an operation + channel routing and computes the same
128/// feed-forward dataflow the hardwired grid does; loading `route_loop_body`'s certified placement runs any
129/// bounded Arith block on this ONE array. Combinational (feed-forward south/east) — the answer settles in
130/// zero clocks; the config is read once at elaboration.
131pub fn emit_reconfig_fabric_verilog(name: &str, rows: usize, cols: usize, n_in: usize) -> String {
132    let rc = rows * cols;
133    let in_ports: String = (0..n_in)
134        .map(|k| format!("input signed [63:0] in{k},\n    "))
135        .collect();
136
137    let mut body = String::new();
138    for r in 0..rows {
139        for c in 0..cols {
140            let idx = r * cols + c;
141            let nn = if r == 0 {
142                "64'sd0".to_string()
143            } else {
144                format!("vs_{}_{c}", r - 1)
145            };
146            let ww = if c == 0 {
147                "64'sd0".to_string()
148            } else {
149                format!("he_{r}_{}", c - 1)
150            };
151            body.push_str(&format!("  wire [31:0] ct_{r}_{c} = ctrl[{idx}];\n"));
152            let ain = in_mux(&format!("ct_{r}_{c}[10:7]"), n_in);
153            let bin = in_mux(&format!("ct_{r}_{c}[17:14]"), n_in);
154            body.push_str(&format!(
155                "  wire signed [63:0] opa_{r}_{c} = (ct_{r}_{c}[6:4]==3'd0) ? 64'sd0 : (ct_{r}_{c}[6:4]==3'd1) ? {nn} : (ct_{r}_{c}[6:4]==3'd2) ? {ww} : (ct_{r}_{c}[6:4]==3'd3) ? ({ain}) : sac[{idx}];\n"
156            ));
157            body.push_str(&format!(
158                "  wire signed [63:0] opb_{r}_{c} = (ct_{r}_{c}[13:11]==3'd0) ? 64'sd0 : (ct_{r}_{c}[13:11]==3'd1) ? {nn} : (ct_{r}_{c}[13:11]==3'd2) ? {ww} : (ct_{r}_{c}[13:11]==3'd3) ? ({bin}) : sbc[{idx}];\n"
159            ));
160            body.push_str(&format!(
161                "  wire signed [63:0] al_{r}_{c} = alu_f(ct_{r}_{c}[3:0], opa_{r}_{c}, opb_{r}_{c});\n"
162            ));
163            body.push_str(&format!(
164                "  wire signed [63:0] vs_{r}_{c} = drive_f(ct_{r}_{c}[19:18], {nn}, {ww}, al_{r}_{c});\n"
165            ));
166            body.push_str(&format!(
167                "  wire signed [63:0] he_{r}_{c} = drive_f(ct_{r}_{c}[21:20], {nn}, {ww}, al_{r}_{c});\n"
168            ));
169        }
170    }
171
172    // The output tap: a mux over all tiles selected by the runtime out_sel + out_chan.
173    let mut vs_sel = "64'sd0".to_string();
174    let mut he_sel = "64'sd0".to_string();
175    for r in (0..rows).rev() {
176        for c in (0..cols).rev() {
177            let idx = r * cols + c;
178            vs_sel = format!(
179                "(out_sel=={rc_w}'d{idx}) ? vs_{r}_{c} : ({vs_sel})",
180                rc_w = 16
181            );
182            he_sel = format!(
183                "(out_sel=={rc_w}'d{idx}) ? he_{r}_{c} : ({he_sel})",
184                rc_w = 16
185            );
186        }
187    }
188
189    format!(
190        "// reconfig_{name} — a UNIVERSAL config-driven CGRA ({rows}x{cols}). Every tile's operation and\n\
191         // channel routing are RUNTIME config (loaded via $readmemh); loading route_loop_body's certified\n\
192         // placement (Stage 3) runs ANY bounded Arith block on ONE physical array. Feed-forward → combinational.\n\
193         module reconfig_{name}(\n    {in_ports}output signed [63:0] result\n);\n\
194         \x20 reg [31:0] ctrl [0:{rcm1}];\n\
195         \x20 reg signed [63:0] sac [0:{rcm1}];\n\
196         \x20 reg signed [63:0] sbc [0:{rcm1}];\n\
197         \x20 reg [63:0] ohdr [0:1];\n\
198         \x20 reg [15:0] out_sel; reg out_chan;\n\
199         \x20 initial begin\n\
200         \x20   $readmemh(\"reconfig_{name}_ctrl.hex\", ctrl);\n\
201         \x20   $readmemh(\"reconfig_{name}_sac.hex\", sac);\n\
202         \x20   $readmemh(\"reconfig_{name}_sbc.hex\", sbc);\n\
203         \x20   $readmemh(\"reconfig_{name}_out.hex\", ohdr);\n\
204         \x20   out_sel = ohdr[0][15:0]; out_chan = ohdr[1][0];\n\
205         \x20 end\n\
206         \x20 function signed [63:0] alu_f(input [3:0] op, input signed [63:0] a, input signed [63:0] b);\n\
207         \x20   case (op)\n\
208         \x20     4'd0: alu_f = a + b; 4'd1: alu_f = a - b; 4'd2: alu_f = a * b;\n\
209         \x20     4'd3: alu_f = a & b; 4'd4: alu_f = a | b; 4'd5: alu_f = a ^ b;\n\
210         \x20     4'd6: alu_f = a <<< b; 4'd7: alu_f = a >>> b; 4'd8: alu_f = $unsigned(a) >> b; default: alu_f = 64'sd0;\n\
211         \x20   endcase\n\
212         \x20 endfunction\n\
213         \x20 function signed [63:0] drive_f(input [1:0] m, input signed [63:0] nn, input signed [63:0] ww, input signed [63:0] al);\n\
214         \x20   case (m) 2'd0: drive_f = 64'sd0; 2'd1: drive_f = nn; 2'd2: drive_f = ww; 2'd3: drive_f = al; default: drive_f = 64'sd0; endcase\n\
215         \x20 endfunction\n\
216         {body}  assign result = out_chan ? ({he_sel}) : ({vs_sel});\n\
217         endmodule\n",
218        rcm1 = rc - 1,
219    )
220}
221
222/// A self-checking testbench for the reconfigurable fabric: drive each input vector and compare `result`
223/// to the VM oracle in `reconfig_{name}_{pin,eout}.hex` (the fabric loads its config from
224/// `reconfig_{name}_{ctrl,sac,sbc,out}.hex`). One block's config + vectors are written per run; the fabric
225/// RTL is fixed, so it compiles once and re-runs per block.
226pub fn emit_reconfig_testbench(name: &str, n_in: usize, n_vectors: usize) -> String {
227    let decls: String = (0..n_in)
228        .map(|k| format!("  reg signed [63:0] in{k};\n"))
229        .collect();
230    let ports: String = {
231        let mut v: Vec<String> = (0..n_in).map(|k| format!(".in{k}(in{k})")).collect();
232        v.push(".result(result)".to_string());
233        v.join(", ")
234    };
235    let pin = if n_in > 0 {
236        format!("  reg signed [63:0] pin [0:{}];\n", n_in * n_vectors - 1)
237    } else {
238        String::new()
239    };
240    let readpin = if n_in > 0 {
241        format!("    $readmemh(\"reconfig_{name}_pin.hex\", pin);\n")
242    } else {
243        String::new()
244    };
245    let drive: String = (0..n_in)
246        .map(|k| format!("      in{k} = pin[i*{n_in}+{k}];\n"))
247        .collect();
248
249    format!(
250        r#"// tb_reconfig_{name} — the reconfigurable fabric (config = a certified placement) vs the RUNNING VM.
251`timescale 1ns/1ps
252module tb_reconfig_{name};
253{decls}{pin}  reg signed [63:0] eout [0:{olast}];
254  wire signed [63:0] result;
255  integer i, errors;
256  reconfig_{name} dut({ports});
257  initial begin
258    errors = 0;
259{readpin}    $readmemh("reconfig_{name}_eout.hex", eout);
260    for (i = 0; i < {n_vectors}; i = i + 1) begin
261{drive}      #1;
262      if (result !== eout[i]) begin errors = errors + 1; $display("FAIL case %0d: got %0d exp %0d", i, result, eout[i]); end
263    end
264    if (errors == 0) $display("PASS: {n_vectors} reconfigurable-fabric evaluations match the VM");
265    else $display("FAIL: %0d reconfig mismatches", errors);
266    $finish;
267  end
268endmodule
269"#,
270        olast = n_vectors - 1,
271    )
272}