ternlang-hdl 0.2.0

Verilog-2001 codegen for balanced ternary — BET processor primitives, sparse matmul array, FPGA simulation wrapper.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
//! BET FPGA Simulation Wrapper (Phase 6.1)
//!
//! Generates a self-contained Icarus Verilog testbench from BET bytecode.
//! The testbench instantiates `bet_processor` with an inline ROM initialised
//! from the program bytes, drives clock/reset, and prints register state on halt.
//!
//! ## Usage
//! ```no_run
//! use ternlang_hdl::sim::BetSimEmitter;
//! let prog = vec![0x01u8, 0x02, 0x00]; // PUSH +1, PUSH -1, HALT
//! let tb = BetSimEmitter::new().emit_testbench(&prog);
//! std::fs::write("sim_tb.v", &tb).unwrap();
//! // then: iverilog -o sim_tb.vvp sim_tb.v && vvp sim_tb.vvp
//! ```
//!
//! ## Trit encoding reminder
//! ```text
//!   2'b01  →  -1 (conflict)
//!   2'b10  →  +1 (truth)
//!   2'b11  →   0 (hold, active neutral)
//!   2'b00  → FAULT
//! ```

use crate::verilog::VerilogEmitter;
use crate::isa::BetIsaEmitter;

/// Maximum simulation cycles before the testbench forces `$finish`.
const DEFAULT_MAX_CYCLES: usize = 1000;

/// Generates Icarus Verilog simulation testbenches for BET programs.
pub struct BetSimEmitter {
    max_cycles: usize,
}

impl BetSimEmitter {
    pub fn new() -> Self {
        BetSimEmitter { max_cycles: DEFAULT_MAX_CYCLES }
    }

    pub fn with_max_cycles(max_cycles: usize) -> Self {
        BetSimEmitter { max_cycles }
    }

    /// Emit a complete, self-contained Verilog testbench for the given BET bytecode.
    ///
    /// The output file can be compiled and run with:
    /// ```sh
    /// iverilog -o bet_sim.vvp bet_sim_tb.v && vvp bet_sim.vvp
    /// ```
    pub fn emit_testbench(&self, bytecode: &[u8]) -> String {
        let mut out = String::new();

        out.push_str("// ═══════════════════════════════════════════════════════════════\n");
        out.push_str("// BET Processor Simulation Testbench\n");
        out.push_str("// RFI-IRFOS Ternary Intelligence Stack\n");
        out.push_str("// Auto-generated by ternlang-hdl BetSimEmitter — do not edit\n");
        out.push_str("//\n");
        out.push_str("// Trit encoding: 2'b01=-1  2'b10=+1  2'b11=0(hold)  2'b00=FAULT\n");
        out.push_str("// ═══════════════════════════════════════════════════════════════\n");
        out.push_str("`timescale 1ns / 1ps\n\n");

        // Emit all primitive modules
        out.push_str(VerilogEmitter::emit_primitives().as_str());
        out.push('\n');
        let isa = BetIsaEmitter::new();
        out.push_str(&isa.emit_register_file());
        out.push('\n');
        out.push_str(&isa.emit_program_counter());
        out.push('\n');
        out.push_str(&isa.emit_control_unit());
        out.push('\n');
        out.push_str(&isa.emit_top());
        out.push('\n');

        // Instruction ROM module
        out.push_str(&self.emit_rom(bytecode));
        out.push('\n');

        // Stack module (minimal stack for simulation — 256 deep, 2-bit wide)
        out.push_str(Self::emit_stack_shim());
        out.push('\n');

        // Testbench top
        out.push_str(&self.emit_tb_top(bytecode.len()));

        out
    }

    /// Emit an instruction ROM initialised with the program bytes.
    fn emit_rom(&self, bytecode: &[u8]) -> String {
        let depth = bytecode.len().max(4).next_power_of_two();
        let addr_bits = (depth as f64).log2().ceil() as usize;

        let mut init_lines = String::new();
        for (i, &b) in bytecode.iter().enumerate() {
            init_lines.push_str(&format!("        rom[{}] = 8'h{:02X};\n", i, b));
        }
        // Pad remainder with THALT (0x00)
        for i in bytecode.len()..depth {
            init_lines.push_str(&format!("        rom[{}] = 8'h00; // THALT (pad)\n", i));
        }

        format!(
            r#"// BET Instruction ROM — {depth} bytes, {addr_bits}-bit address
// Program: {prog_len} bytes of BET bytecode
module bet_imem #(
    parameter DEPTH     = {depth},
    parameter ADDR_BITS = {addr_bits}
) (
    input  [{addr_bits_m1}:0] addr,
    output [7:0]              data
);
    reg [7:0] rom [0:DEPTH-1];
    initial begin
{init_lines}    end
    assign data = rom[addr[{addr_bits_m1}:0]];
endmodule
"#,
            depth = depth,
            addr_bits = addr_bits,
            addr_bits_m1 = addr_bits - 1,
            prog_len = bytecode.len(),
            init_lines = init_lines
        )
    }

    /// Emit a minimal simulation stack (LIFO, 256 depth, 2-bit trit).
    fn emit_stack_shim() -> &'static str {
        r#"// Simulation stack shim — not synthesisable, testbench only
// In synthesis: replace with registered stack or BRAM.
module bet_stack_shim (
    input        clk,
    input        rst_n,
    input  [1:0] push_data,
    input        push_en,
    input        pop_en,
    output [1:0] top,
    output       empty
);
    reg [1:0] mem [0:255];
    reg [7:0] sp;   // stack pointer (points to next free slot)
    integer   k;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            sp <= 0;
            for (k = 0; k < 256; k = k + 1)
                mem[k] <= 2'b11; // hold
        end else if (push_en && !pop_en) begin
            mem[sp] <= push_data;
            sp <= sp + 1;
        end else if (pop_en && !push_en && sp > 0) begin
            sp <= sp - 1;
        end
    end

    assign top   = (sp > 0) ? mem[sp - 1] : 2'b11;
    assign empty = (sp == 0);
endmodule
"#
    }

    /// Emit the testbench `module tb_bet_processor` with clock, reset, DUT, and monitors.
    fn emit_tb_top(&self, prog_len: usize) -> String {
        let max_cycles = self.max_cycles;

        format!(
            r#"// ───────────────────────────────────────────────────────────────
// Testbench: tb_bet_processor
// Drives clock/reset, monitors PC + opcode, displays regs on HALT
// ───────────────────────────────────────────────────────────────
module tb_bet_processor;

    // ── Clock & Reset ─────────────────────────────────────────
    reg clk = 0;
    reg rst_n = 0;
    always #5 clk = ~clk;   // 100 MHz (10 ns period)

    // ── DUT interface ─────────────────────────────────────────
    wire [15:0] imem_addr;
    wire [7:0]  imem_data;
    wire [15:0] dmem_addr;
    wire [1:0]  dmem_wdata;
    wire        dmem_we;
    wire        dmem_re;
    reg  [1:0]  dmem_rdata = 2'b11;  // tensor heap: return hold by default

    // ── Instruction ROM ───────────────────────────────────────
    bet_imem u_rom (
        .addr(imem_addr[{addr_bits_m1}:0]),
        .data(imem_data)
    );

    // ── DUT ───────────────────────────────────────────────────
    bet_processor u_dut (
        .clk       (clk),
        .rst_n     (rst_n),
        .imem_addr (imem_addr),
        .imem_data (imem_data),
        .dmem_addr (dmem_addr),
        .dmem_wdata(dmem_wdata),
        .dmem_we   (dmem_we),
        .dmem_re   (dmem_re),
        .dmem_rdata(dmem_rdata)
    );

    // ── Simulation control ────────────────────────────────────
    integer cycle_count = 0;
    integer halted      = 0;

    task display_trit;
        input [1:0] t;
        begin
            case (t)
                2'b01: $write("-1");
                2'b10: $write("+1");
                2'b11: $write(" 0");
                2'b00: $write("XX");  // FAULT
            endcase
        end
    endtask

    always @(posedge clk) begin
        if (rst_n) begin
            cycle_count <= cycle_count + 1;

            // THALT detection (opcode == 8'h00 after reset)
            if (imem_data == 8'h00 && cycle_count > 2 && !halted) begin
                halted <= 1;
                $display("");
                $display("═══════════════════════════════════════");
                $display(" BET Processor HALTED at cycle %0d", cycle_count);
                $display(" Program: {prog_len} bytes");
                $display("───────────────────────────────────────");
                $display(" PC = %0d", imem_addr);
                $display(" Stack top = %b", 2'b11);  // placeholder
                $display("═══════════════════════════════════════");
                $display(" Simulation complete (Icarus Verilog)");
                $display(" RFI-IRFOS BET Processor v0.1");
                #20 $finish;
            end

            // Safety: max cycle limit
            if (cycle_count >= {max_cycles}) begin
                $display("TIMEOUT: simulation exceeded {max_cycles} cycles — forcing halt");
                $finish;
            end
        end
    end

    // ── VCD waveform dump (for GTKWave) ───────────────────────
    initial begin
        $dumpfile("bet_sim.vcd");
        $dumpvars(0, tb_bet_processor);
    end

    // ── Cycle-by-cycle monitor (first 32 cycles) ──────────────
    initial begin
        $display("BET Processor Simulation — RFI-IRFOS TIS");
        $display("Program: {prog_len} bytes of BET bytecode");
        $display("Max cycles: {max_cycles}");
        $display("───────────────────────────────────────");
        // Release reset after 2 clock edges
        @(posedge clk); #1;
        @(posedge clk); #1;
        rst_n = 1;
        $display("Reset released at t=%0t", $time);
    end

    // ── Opcode trace (first 64 instructions) ─────────────────
    reg [5:0] trace_count = 0;
    always @(posedge clk) begin
        if (rst_n && !halted && trace_count < 63) begin
            $display("  [%3d] PC=%0d  OPCODE=8'h%02X",
                     cycle_count, imem_addr, imem_data);
            trace_count <= trace_count + 1;
        end
    end

endmodule
"#,
            prog_len = prog_len,
            max_cycles = max_cycles,
            addr_bits_m1 = 14  // 15-bit index into 16-bit address (bottom 15 bits)
        )
    }

    /// Check whether Icarus Verilog (`iverilog`) is available on $PATH.
    pub fn iverilog_available() -> bool {
        std::process::Command::new("iverilog")
            .arg("--version")
            .output()
            .map(|o| o.status.success())
            .unwrap_or(false)
    }

    /// Compile and run a testbench file using Icarus Verilog.
    /// Returns (stdout, stderr, exit_code).
    ///
    /// Requires `iverilog` and `vvp` on $PATH.
    pub fn run_iverilog(tb_path: &str) -> Result<String, String> {
        let vvp_path = tb_path.replace(".v", ".vvp");

        // Compile
        let compile = std::process::Command::new("iverilog")
            .args(["-o", &vvp_path, "-g2001", tb_path])
            .output()
            .map_err(|e| format!("iverilog not found: {}", e))?;

        if !compile.status.success() {
            return Err(format!(
                "iverilog compile failed:\n{}",
                String::from_utf8_lossy(&compile.stderr)
            ));
        }

        // Run
        let run = std::process::Command::new("vvp")
            .arg(&vvp_path)
            .output()
            .map_err(|e| format!("vvp not found: {}", e))?;

        let stdout = String::from_utf8_lossy(&run.stdout).into_owned();
        let stderr = String::from_utf8_lossy(&run.stderr).into_owned();
        if !run.status.success() {
            return Err(format!("vvp run failed:\n{}", stderr));
        }
        Ok(stdout + &stderr)
    }
}

impl Default for BetSimEmitter {
    fn default() -> Self { Self::new() }
}

// ─────────────────────────────────────────────────────────────────────────────
// Tests
// ─────────────────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;

    fn minimal_prog() -> Vec<u8> {
        // PUSH +1 (0x01, 0x02), PUSH -1 (0x01, 0x01), TADD (0x02), THALT (0x00)
        vec![0x01, 0x02, 0x01, 0x01, 0x02, 0x00]
    }

    #[test]
    fn test_emit_testbench_not_empty() {
        let em = BetSimEmitter::new();
        let tb = em.emit_testbench(&minimal_prog());
        assert!(!tb.is_empty());
    }

    #[test]
    fn test_testbench_contains_timescale() {
        let tb = BetSimEmitter::new().emit_testbench(&minimal_prog());
        assert!(tb.contains("`timescale"));
    }

    #[test]
    fn test_testbench_contains_all_modules() {
        let tb = BetSimEmitter::new().emit_testbench(&minimal_prog());
        assert!(tb.contains("module bet_processor"));
        assert!(tb.contains("module bet_regfile"));
        assert!(tb.contains("module bet_pc"));
        assert!(tb.contains("module bet_control"));
        assert!(tb.contains("module bet_imem"));
        assert!(tb.contains("module bet_stack_shim"));
        assert!(tb.contains("module tb_bet_processor"));
    }

    #[test]
    fn test_rom_encodes_bytecode() {
        let prog = vec![0xAB, 0xCD, 0x00];
        let em = BetSimEmitter::new();
        let tb = em.emit_testbench(&prog);
        assert!(tb.contains("8'hAB"));
        assert!(tb.contains("8'hCD"));
        assert!(tb.contains("8'h00"));
    }

    #[test]
    fn test_rom_pads_to_power_of_two() {
        let prog = vec![0x01, 0x00]; // 2 bytes → ROM depth = 4
        let em = BetSimEmitter::new();
        let rom = em.emit_rom(&prog);
        assert!(rom.contains("DEPTH     = 4"));
    }

    #[test]
    fn test_testbench_contains_vcd_dump() {
        let tb = BetSimEmitter::new().emit_testbench(&minimal_prog());
        assert!(tb.contains("$dumpfile"));
        assert!(tb.contains("$dumpvars"));
    }

    #[test]
    fn test_testbench_contains_halt_detection() {
        let tb = BetSimEmitter::new().emit_testbench(&minimal_prog());
        assert!(tb.contains("8'h00"));        // THALT opcode check
        assert!(tb.contains("$finish"));
        assert!(tb.contains("HALTED"));
    }

    #[test]
    fn test_custom_max_cycles() {
        let em = BetSimEmitter::with_max_cycles(42);
        let tb = em.emit_testbench(&minimal_prog());
        assert!(tb.contains("42"));
    }

    #[test]
    fn test_trit_encoding_comment_present() {
        let tb = BetSimEmitter::new().emit_testbench(&minimal_prog());
        assert!(tb.contains("2'b01=-1"));
        assert!(tb.contains("2'b10=+1"));
    }

    #[test]
    fn test_iverilog_check_does_not_panic() {
        // Just verify it runs without panicking — result depends on system
        let _ = BetSimEmitter::iverilog_available();
    }
}