Skip to main content

logicaffeine_compile/codegen_sva/
block_engine_rtl.rs

1//! block_engine_rtl — Stage 5 of the self-synthesizing bytecode XPU: the on-chip EXECUTE engine for a
2//! decoded block. Stage 4's reconfigurable fabric runs a block's certified SPATIAL placement (host-computed).
3//! This engine takes a block's DECODED OPS DIRECTLY — no host placement step — and runs them on a universal
4//! reconfigurable datapath: one hardware array executes ANY bounded straight-line block by loading its op
5//! list as config. This is the unit the LGB1 decoder feeds: bytecode → ops → this engine → result, all
6//! on-chip.
7//!
8//! Each cell `i` is `v[i] = op_i(operand_a, operand_b)` where an operand is a global input, a constant, or an
9//! earlier cell's value `v[j]` (j<i) — selected by config muxes (a full-interconnect "crossbar" datapath, so
10//! there is no routing to place: the ops ARE the config). Feed-forward (`v[i]` reads only `v[<i]`) → combinational.
11//! The engine reproduces `eval_fabric` exactly, so it is proven ≡ the VM under iverilog by `block_engine_lock`.
12//!
13//! Correct-first scope: this is the full-interconnect realization (trivial placement, larger interconnect).
14//! The AREA-OPTIMAL frontier is running these same ops through `route_loop_body`'s nearest-neighbour SPATIAL
15//! placement on the Stage-4 fabric with an on-chip placer — the two meet when the placement decode moves into
16//! silicon.
17
18use super::fabric_rtl::{AluOp, FabricOp, Src};
19
20/// A decoded block transcribed into the execute engine's config: one 32-bit control word per cell (op + the
21/// two operand sources) plus two 64-bit constant slots per cell, and `result` (which cell's value is the
22/// block result).
23pub struct BlockConfig {
24    pub ctrl: Vec<u32>,
25    pub ac: Vec<i64>,
26    pub bc: Vec<i64>,
27    pub result: usize,
28}
29
30fn alu_code(op: AluOp) -> u32 {
31    match op {
32        AluOp::Add => 0,
33        AluOp::Sub => 1,
34        AluOp::Mul => 2,
35        AluOp::And => 3,
36        AluOp::Or => 4,
37        AluOp::Xor => 5,
38        AluOp::Shl => 6,
39        AluOp::Shr => 7,
40        AluOp::UShr => 8,
41    }
42}
43
44/// Encode an operand as `(mode, ref, const)` — mode 0=In(ref), 1=Const(const), 2=Val(ref).
45fn enc(s: Src) -> (u32, u32, i64) {
46    match s {
47        Src::In(k) => (0, k as u32, 0),
48        Src::Const(c) => (1, 0, c),
49        Src::Val(j) => (2, j as u32, 0),
50    }
51}
52
53/// Transcribe a decoded block (`FabricOp` list + result index) into the engine's config, padded to `k` cells
54/// (unused cells are inert — their value is never referenced).
55pub fn serialize_block(ops: &[FabricOp], result: usize, k: usize) -> BlockConfig {
56    assert!(
57        ops.len() <= k,
58        "block has {} cells > engine capacity {k}",
59        ops.len()
60    );
61    let mut ctrl = vec![0u32; k];
62    let mut ac = vec![0i64; k];
63    let mut bc = vec![0i64; k];
64    for (i, o) in ops.iter().enumerate() {
65        let (am, ar, acv) = enc(o.a);
66        let (bm, br, bcv) = enc(o.b);
67        ctrl[i] = alu_code(o.op) | (am << 4) | (ar << 6) | (bm << 10) | (br << 12);
68        ac[i] = acv;
69        bc[i] = bcv;
70    }
71    BlockConfig {
72        ctrl,
73        ac,
74        bc,
75        result,
76    }
77}
78
79/// A ternary chain selecting `in{k}` (0..n_in) by the 4-bit `field`.
80fn in_mux(field: &str, n_in: usize) -> String {
81    let mut acc = "64'sd0".to_string();
82    for k in (0..n_in).rev() {
83        acc = format!("({field}==4'd{k}) ? in{k} : ({acc})");
84    }
85    acc
86}
87
88/// A ternary chain selecting `v_{j}` (0..upto) by the 4-bit `field` — the prior-cell operand mux.
89fn v_mux(field: &str, upto: usize) -> String {
90    let mut acc = "64'sd0".to_string();
91    for j in (0..upto).rev() {
92        acc = format!("({field}==4'd{j}) ? v_{j} : ({acc})");
93    }
94    acc
95}
96
97/// Emit the universal block execute engine: a `k`-cell reconfigurable datapath over `n_in` inputs. Each cell
98/// decodes its control word into an op + two operand selects (input / constant / earlier cell) and computes
99/// combinationally; loading a decoded block's op list runs it. Config is read once via `$readmemh`.
100pub fn emit_block_engine_verilog(name: &str, k: usize, n_in: usize) -> String {
101    let in_ports: String = (0..n_in)
102        .map(|j| format!("input signed [63:0] in{j},\n    "))
103        .collect();
104
105    let mut body = String::new();
106    for i in 0..k {
107        body.push_str(&format!("  wire [31:0] ct_{i} = ctrl[{i}];\n"));
108        let ain = in_mux(&format!("ct_{i}[9:6]"), n_in);
109        let bin = in_mux(&format!("ct_{i}[15:12]"), n_in);
110        let av = if i == 0 {
111            "64'sd0".to_string()
112        } else {
113            v_mux(&format!("ct_{i}[9:6]"), i)
114        };
115        let bv = if i == 0 {
116            "64'sd0".to_string()
117        } else {
118            v_mux(&format!("ct_{i}[15:12]"), i)
119        };
120        body.push_str(&format!(
121            "  wire signed [63:0] opa_{i} = (ct_{i}[5:4]==2'd0) ? ({ain}) : (ct_{i}[5:4]==2'd1) ? ac[{i}] : ({av});\n"
122        ));
123        body.push_str(&format!(
124            "  wire signed [63:0] opb_{i} = (ct_{i}[11:10]==2'd0) ? ({bin}) : (ct_{i}[11:10]==2'd1) ? bc[{i}] : ({bv});\n"
125        ));
126        body.push_str(&format!(
127            "  wire signed [63:0] v_{i} = alu_f(ct_{i}[3:0], opa_{i}, opb_{i});\n"
128        ));
129    }
130    // The result mux: v[result_ref] selected at runtime.
131    let mut res_sel = "64'sd0".to_string();
132    for i in (0..k).rev() {
133        res_sel = format!("(res=={rw}'d{i}) ? v_{i} : ({res_sel})", rw = 16);
134    }
135
136    format!(
137        "// bexec_{name} — a UNIVERSAL {k}-cell block EXECUTE engine over {n_in} inputs. Each cell's op and\n\
138         // operand sources are RUNTIME config ($readmemh); loading a decoded block's op list runs it on ONE\n\
139         // reconfigurable datapath. Feed-forward → combinational. Reproduces eval_fabric; proven ≡ the VM.\n\
140         module bexec_{name}(\n    {in_ports}output signed [63:0] result\n);\n\
141         \x20 reg [31:0] ctrl [0:{km1}];\n\
142         \x20 reg signed [63:0] ac [0:{km1}];\n\
143         \x20 reg signed [63:0] bc [0:{km1}];\n\
144         \x20 reg [15:0] res; reg [63:0] rhdr [0:0];\n\
145         \x20 initial begin\n\
146         \x20   $readmemh(\"bexec_{name}_ctrl.hex\", ctrl);\n\
147         \x20   $readmemh(\"bexec_{name}_ac.hex\", ac);\n\
148         \x20   $readmemh(\"bexec_{name}_bc.hex\", bc);\n\
149         \x20   $readmemh(\"bexec_{name}_res.hex\", rhdr);\n\
150         \x20   res = rhdr[0][15:0];\n\
151         \x20 end\n\
152         \x20 function signed [63:0] alu_f(input [3:0] op, input signed [63:0] a, input signed [63:0] b);\n\
153         \x20   case (op)\n\
154         \x20     4'd0: alu_f = a + b; 4'd1: alu_f = a - b; 4'd2: alu_f = a * b;\n\
155         \x20     4'd3: alu_f = a & b; 4'd4: alu_f = a | b; 4'd5: alu_f = a ^ b;\n\
156         \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\
157         \x20   endcase\n\
158         \x20 endfunction\n\
159         {body}  assign result = {res_sel};\n\
160         endmodule\n",
161        km1 = k - 1,
162    )
163}
164
165/// A self-checking testbench: drive each input vector, compare `result` to the VM oracle in
166/// `bexec_{name}_{pin,eout}.hex` (the engine loads its config from `bexec_{name}_{ctrl,ac,bc,res}.hex`).
167pub fn emit_block_engine_testbench(name: &str, n_in: usize, n_vectors: usize) -> String {
168    let decls: String = (0..n_in)
169        .map(|k| format!("  reg signed [63:0] in{k};\n"))
170        .collect();
171    let ports: String = {
172        let mut v: Vec<String> = (0..n_in).map(|k| format!(".in{k}(in{k})")).collect();
173        v.push(".result(result)".to_string());
174        v.join(", ")
175    };
176    let pin = if n_in > 0 {
177        format!("  reg signed [63:0] pin [0:{}];\n", n_in * n_vectors - 1)
178    } else {
179        String::new()
180    };
181    let readpin = if n_in > 0 {
182        format!("    $readmemh(\"bexec_{name}_pin.hex\", pin);\n")
183    } else {
184        String::new()
185    };
186    let drive: String = (0..n_in)
187        .map(|k| format!("      in{k} = pin[i*{n_in}+{k}];\n"))
188        .collect();
189
190    format!(
191        r#"// tb_bexec_{name} — the block execute engine (config = a decoded op list) vs the RUNNING VM.
192`timescale 1ns/1ps
193module tb_bexec_{name};
194{decls}{pin}  reg signed [63:0] eout [0:{olast}];
195  wire signed [63:0] result;
196  integer i, errors;
197  bexec_{name} dut({ports});
198  initial begin
199    errors = 0;
200{readpin}    $readmemh("bexec_{name}_eout.hex", eout);
201    for (i = 0; i < {n_vectors}; i = i + 1) begin
202{drive}      #1;
203      if (result !== eout[i]) begin errors = errors + 1; $display("FAIL case %0d: got %0d exp %0d", i, result, eout[i]); end
204    end
205    if (errors == 0) $display("PASS: {n_vectors} block-engine evaluations match the VM");
206    else $display("FAIL: %0d block-engine mismatches", errors);
207    $finish;
208  end
209endmodule
210"#,
211        olast = n_vectors - 1,
212    )
213}