1use crate::verilog::VerilogEmitter;
25use crate::isa::BetIsaEmitter;
26
27const DEFAULT_MAX_CYCLES: usize = 1000;
29
30pub 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 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 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 out.push_str(&self.emit_rom(bytecode));
77 out.push('\n');
78
79 out.push_str(Self::emit_stack_shim());
81 out.push('\n');
82
83 out.push_str(&self.emit_tb_top(bytecode.len()));
85
86 out
87 }
88
89 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 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 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 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 )
286 }
287
288 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 pub fn run_iverilog(tb_path: &str) -> Result<String, String> {
302 let vvp_path = tb_path.replace(".v", ".vvp");
303
304 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 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#[cfg(test)]
341mod tests {
342 use super::*;
343
344 fn minimal_prog() -> Vec<u8> {
345 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]; 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")); 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 let _ = BetSimEmitter::iverilog_available();
425 }
426}