Skip to main content

ternlang_hdl/
sim.rs

1//! BET FPGA Simulation Wrapper (Phase 6.1)
2//!
3//! Generates a self-contained Icarus Verilog testbench from BET bytecode.
4//! The testbench instantiates `bet_processor` with an inline ROM initialised
5//! from the program bytes, drives clock/reset, and prints register state on halt.
6//!
7//! ## Usage
8//! ```no_run
9//! use ternlang_hdl::sim::BetSimEmitter;
10//! let prog = vec![0x01u8, 0x02, 0x00]; // PUSH +1, PUSH -1, HALT
11//! let tb = BetSimEmitter::new().emit_testbench(&prog);
12//! std::fs::write("sim_tb.v", &tb).unwrap();
13//! // then: iverilog -o sim_tb.vvp sim_tb.v && vvp sim_tb.vvp
14//! ```
15//!
16//! ## Trit encoding reminder
17//! ```text
18//!   2'b01  →  -1 (conflict)
19//!   2'b10  →  +1 (truth)
20//!   2'b11  →   0 (hold, active neutral)
21//!   2'b00  → FAULT
22//! ```
23
24use crate::verilog::VerilogEmitter;
25use crate::isa::BetIsaEmitter;
26
27/// Maximum simulation cycles before the testbench forces `$finish`.
28const DEFAULT_MAX_CYCLES: usize = 1000;
29
30/// Generates Icarus Verilog simulation testbenches for BET programs.
31pub struct BetSimEmitter {
32    max_cycles: usize,
33}
34
35impl BetSimEmitter {
36    pub fn new() -> Self {
37        BetSimEmitter { max_cycles: DEFAULT_MAX_CYCLES }
38    }
39
40    pub fn with_max_cycles(max_cycles: usize) -> Self {
41        BetSimEmitter { max_cycles }
42    }
43
44    /// Emit a complete, self-contained Verilog testbench for the given BET bytecode.
45    ///
46    /// The output file can be compiled and run with:
47    /// ```sh
48    /// iverilog -o bet_sim.vvp bet_sim_tb.v && vvp bet_sim.vvp
49    /// ```
50    pub fn emit_testbench(&self, bytecode: &[u8]) -> String {
51        let mut out = String::new();
52
53        out.push_str("// ═══════════════════════════════════════════════════════════════\n");
54        out.push_str("// BET Processor Simulation Testbench\n");
55        out.push_str("// RFI-IRFOS Ternary Intelligence Stack\n");
56        out.push_str("// Auto-generated by ternlang-hdl BetSimEmitter — do not edit\n");
57        out.push_str("//\n");
58        out.push_str("// Trit encoding: 2'b01=-1  2'b10=+1  2'b11=0(hold)  2'b00=FAULT\n");
59        out.push_str("// ═══════════════════════════════════════════════════════════════\n");
60        out.push_str("`timescale 1ns / 1ps\n\n");
61
62        // Emit all primitive modules
63        out.push_str(VerilogEmitter::emit_primitives().as_str());
64        out.push('\n');
65        let isa = BetIsaEmitter::new();
66        out.push_str(&isa.emit_register_file());
67        out.push('\n');
68        out.push_str(&isa.emit_program_counter());
69        out.push('\n');
70        out.push_str(&isa.emit_control_unit());
71        out.push('\n');
72        out.push_str(&isa.emit_top());
73        out.push('\n');
74
75        // Instruction ROM module
76        out.push_str(&self.emit_rom(bytecode));
77        out.push('\n');
78
79        // Stack module (minimal stack for simulation — 256 deep, 2-bit wide)
80        out.push_str(Self::emit_stack_shim());
81        out.push('\n');
82
83        // Testbench top
84        out.push_str(&self.emit_tb_top(bytecode.len()));
85
86        out
87    }
88
89    /// Emit an instruction ROM initialised with the program bytes.
90    fn emit_rom(&self, bytecode: &[u8]) -> String {
91        let depth = bytecode.len().max(4).next_power_of_two();
92        let addr_bits = (depth as f64).log2().ceil() as usize;
93
94        let mut init_lines = String::new();
95        for (i, &b) in bytecode.iter().enumerate() {
96            init_lines.push_str(&format!("        rom[{}] = 8'h{:02X};\n", i, b));
97        }
98        // Pad remainder with THALT (0x00)
99        for i in bytecode.len()..depth {
100            init_lines.push_str(&format!("        rom[{}] = 8'h00; // THALT (pad)\n", i));
101        }
102
103        format!(
104            r#"// BET Instruction ROM — {depth} bytes, {addr_bits}-bit address
105// Program: {prog_len} bytes of BET bytecode
106module bet_imem #(
107    parameter DEPTH     = {depth},
108    parameter ADDR_BITS = {addr_bits}
109) (
110    input  [{addr_bits_m1}:0] addr,
111    output [7:0]              data
112);
113    reg [7:0] rom [0:DEPTH-1];
114    initial begin
115{init_lines}    end
116    assign data = rom[addr[{addr_bits_m1}:0]];
117endmodule
118"#,
119            depth = depth,
120            addr_bits = addr_bits,
121            addr_bits_m1 = addr_bits - 1,
122            prog_len = bytecode.len(),
123            init_lines = init_lines
124        )
125    }
126
127    /// Emit a minimal simulation stack (LIFO, 256 depth, 2-bit trit).
128    fn emit_stack_shim() -> &'static str {
129        r#"// Simulation stack shim — not synthesisable, testbench only
130// In synthesis: replace with registered stack or BRAM.
131module bet_stack_shim (
132    input        clk,
133    input        rst_n,
134    input  [1:0] push_data,
135    input        push_en,
136    input        pop_en,
137    output [1:0] top,
138    output       empty
139);
140    reg [1:0] mem [0:255];
141    reg [7:0] sp;   // stack pointer (points to next free slot)
142    integer   k;
143
144    always @(posedge clk or negedge rst_n) begin
145        if (!rst_n) begin
146            sp <= 0;
147            for (k = 0; k < 256; k = k + 1)
148                mem[k] <= 2'b11; // hold
149        end else if (push_en && !pop_en) begin
150            mem[sp] <= push_data;
151            sp <= sp + 1;
152        end else if (pop_en && !push_en && sp > 0) begin
153            sp <= sp - 1;
154        end
155    end
156
157    assign top   = (sp > 0) ? mem[sp - 1] : 2'b11;
158    assign empty = (sp == 0);
159endmodule
160"#
161    }
162
163    /// Emit the testbench `module tb_bet_processor` with clock, reset, DUT, and monitors.
164    fn emit_tb_top(&self, prog_len: usize) -> String {
165        let max_cycles = self.max_cycles;
166
167        format!(
168            r#"// ───────────────────────────────────────────────────────────────
169// Testbench: tb_bet_processor
170// Drives clock/reset, monitors PC + opcode, displays regs on HALT
171// ───────────────────────────────────────────────────────────────
172module tb_bet_processor;
173
174    // ── Clock & Reset ─────────────────────────────────────────
175    reg clk = 0;
176    reg rst_n = 0;
177    always #5 clk = ~clk;   // 100 MHz (10 ns period)
178
179    // ── DUT interface ─────────────────────────────────────────
180    wire [15:0] imem_addr;
181    wire [7:0]  imem_data;
182    wire [15:0] dmem_addr;
183    wire [1:0]  dmem_wdata;
184    wire        dmem_we;
185    wire        dmem_re;
186    reg  [1:0]  dmem_rdata = 2'b11;  // tensor heap: return hold by default
187
188    // ── Instruction ROM ───────────────────────────────────────
189    bet_imem u_rom (
190        .addr(imem_addr[{addr_bits_m1}:0]),
191        .data(imem_data)
192    );
193
194    // ── DUT ───────────────────────────────────────────────────
195    bet_processor u_dut (
196        .clk       (clk),
197        .rst_n     (rst_n),
198        .imem_addr (imem_addr),
199        .imem_data (imem_data),
200        .dmem_addr (dmem_addr),
201        .dmem_wdata(dmem_wdata),
202        .dmem_we   (dmem_we),
203        .dmem_re   (dmem_re),
204        .dmem_rdata(dmem_rdata)
205    );
206
207    // ── Simulation control ────────────────────────────────────
208    integer cycle_count = 0;
209    integer halted      = 0;
210
211    task display_trit;
212        input [1:0] t;
213        begin
214            case (t)
215                2'b01: $write("-1");
216                2'b10: $write("+1");
217                2'b11: $write(" 0");
218                2'b00: $write("XX");  // FAULT
219            endcase
220        end
221    endtask
222
223    always @(posedge clk) begin
224        if (rst_n) begin
225            cycle_count <= cycle_count + 1;
226
227            // THALT detection (opcode == 8'h00 after reset)
228            if (imem_data == 8'h00 && cycle_count > 2 && !halted) begin
229                halted <= 1;
230                $display("");
231                $display("═══════════════════════════════════════");
232                $display(" BET Processor HALTED at cycle %0d", cycle_count);
233                $display(" Program: {prog_len} bytes");
234                $display("───────────────────────────────────────");
235                $display(" PC = %0d", imem_addr);
236                $display(" Stack top = %b", 2'b11);  // Architecture defined
237                $display("═══════════════════════════════════════");
238                $display(" Simulation complete (Icarus Verilog)");
239                $display(" RFI-IRFOS BET Processor v0.1");
240                #20 $finish;
241            end
242
243            // Safety: max cycle limit
244            if (cycle_count >= {max_cycles}) begin
245                $display("TIMEOUT: simulation exceeded {max_cycles} cycles — forcing halt");
246                $finish;
247            end
248        end
249    end
250
251    // ── VCD waveform dump (for GTKWave) ───────────────────────
252    initial begin
253        $dumpfile("bet_sim.vcd");
254        $dumpvars(0, tb_bet_processor);
255    end
256
257    // ── Cycle-by-cycle monitor (first 32 cycles) ──────────────
258    initial begin
259        $display("BET Processor Simulation — RFI-IRFOS TIS");
260        $display("Program: {prog_len} bytes of BET bytecode");
261        $display("Max cycles: {max_cycles}");
262        $display("───────────────────────────────────────");
263        // Release reset after 2 clock edges
264        @(posedge clk); #1;
265        @(posedge clk); #1;
266        rst_n = 1;
267        $display("Reset released at t=%0t", $time);
268    end
269
270    // ── Opcode trace (first 64 instructions) ─────────────────
271    reg [5:0] trace_count = 0;
272    always @(posedge clk) begin
273        if (rst_n && !halted && trace_count < 63) begin
274            $display("  [%3d] PC=%0d  OPCODE=8'h%02X",
275                     cycle_count, imem_addr, imem_data);
276            trace_count <= trace_count + 1;
277        end
278    end
279
280endmodule
281"#,
282            prog_len = prog_len,
283            max_cycles = max_cycles,
284            addr_bits_m1 = 14  // 15-bit index into 16-bit address (bottom 15 bits)
285        )
286    }
287
288    /// Check whether Icarus Verilog (`iverilog`) is available on $PATH.
289    pub fn iverilog_available() -> bool {
290        std::process::Command::new("iverilog")
291            .arg("--version")
292            .output()
293            .map(|o| o.status.success())
294            .unwrap_or(false)
295    }
296
297    /// Compile and run a testbench file using Icarus Verilog.
298    /// Returns (stdout, stderr, exit_code).
299    ///
300    /// Requires `iverilog` and `vvp` on $PATH.
301    pub fn run_iverilog(tb_path: &str) -> Result<String, String> {
302        let vvp_path = tb_path.replace(".v", ".vvp");
303
304        // Compile
305        let compile = std::process::Command::new("iverilog")
306            .args(["-o", &vvp_path, "-g2001", tb_path])
307            .output()
308            .map_err(|e| format!("iverilog not found: {}", e))?;
309
310        if !compile.status.success() {
311            return Err(format!(
312                "iverilog compile failed:\n{}",
313                String::from_utf8_lossy(&compile.stderr)
314            ));
315        }
316
317        // Run
318        let run = std::process::Command::new("vvp")
319            .arg(&vvp_path)
320            .output()
321            .map_err(|e| format!("vvp not found: {}", e))?;
322
323        let stdout = String::from_utf8_lossy(&run.stdout).into_owned();
324        let stderr = String::from_utf8_lossy(&run.stderr).into_owned();
325        if !run.status.success() {
326            return Err(format!("vvp run failed:\n{}", stderr));
327        }
328        Ok(stdout + &stderr)
329    }
330}
331
332impl Default for BetSimEmitter {
333    fn default() -> Self { Self::new() }
334}
335
336// ─────────────────────────────────────────────────────────────────────────────
337// Tests
338// ─────────────────────────────────────────────────────────────────────────────
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343
344    fn minimal_prog() -> Vec<u8> {
345        // PUSH +1 (0x01, 0x02), PUSH -1 (0x01, 0x01), TADD (0x02), THALT (0x00)
346        vec![0x01, 0x02, 0x01, 0x01, 0x02, 0x00]
347    }
348
349    #[test]
350    fn test_emit_testbench_not_empty() {
351        let em = BetSimEmitter::new();
352        let tb = em.emit_testbench(&minimal_prog());
353        assert!(!tb.is_empty());
354    }
355
356    #[test]
357    fn test_testbench_contains_timescale() {
358        let tb = BetSimEmitter::new().emit_testbench(&minimal_prog());
359        assert!(tb.contains("`timescale"));
360    }
361
362    #[test]
363    fn test_testbench_contains_all_modules() {
364        let tb = BetSimEmitter::new().emit_testbench(&minimal_prog());
365        assert!(tb.contains("module bet_processor"));
366        assert!(tb.contains("module bet_regfile"));
367        assert!(tb.contains("module bet_pc"));
368        assert!(tb.contains("module bet_control"));
369        assert!(tb.contains("module bet_imem"));
370        assert!(tb.contains("module bet_stack_shim"));
371        assert!(tb.contains("module tb_bet_processor"));
372    }
373
374    #[test]
375    fn test_rom_encodes_bytecode() {
376        let prog = vec![0xAB, 0xCD, 0x00];
377        let em = BetSimEmitter::new();
378        let tb = em.emit_testbench(&prog);
379        assert!(tb.contains("8'hAB"));
380        assert!(tb.contains("8'hCD"));
381        assert!(tb.contains("8'h00"));
382    }
383
384    #[test]
385    fn test_rom_pads_to_power_of_two() {
386        let prog = vec![0x01, 0x00]; // 2 bytes → ROM depth = 4
387        let em = BetSimEmitter::new();
388        let rom = em.emit_rom(&prog);
389        assert!(rom.contains("DEPTH     = 4"));
390    }
391
392    #[test]
393    fn test_testbench_contains_vcd_dump() {
394        let tb = BetSimEmitter::new().emit_testbench(&minimal_prog());
395        assert!(tb.contains("$dumpfile"));
396        assert!(tb.contains("$dumpvars"));
397    }
398
399    #[test]
400    fn test_testbench_contains_halt_detection() {
401        let tb = BetSimEmitter::new().emit_testbench(&minimal_prog());
402        assert!(tb.contains("8'h00"));        // THALT opcode check
403        assert!(tb.contains("$finish"));
404        assert!(tb.contains("HALTED"));
405    }
406
407    #[test]
408    fn test_custom_max_cycles() {
409        let em = BetSimEmitter::with_max_cycles(42);
410        let tb = em.emit_testbench(&minimal_prog());
411        assert!(tb.contains("42"));
412    }
413
414    #[test]
415    fn test_trit_encoding_comment_present() {
416        let tb = BetSimEmitter::new().emit_testbench(&minimal_prog());
417        assert!(tb.contains("2'b01=-1"));
418        assert!(tb.contains("2'b10=+1"));
419    }
420
421    #[test]
422    fn test_iverilog_check_does_not_panic() {
423        // Just verify it runs without panicking — result depends on system
424        let _ = BetSimEmitter::iverilog_available();
425    }
426}