use crate::verilog::VerilogEmitter;
use crate::isa::BetIsaEmitter;
const DEFAULT_MAX_CYCLES: usize = 1000;
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 }
}
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");
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');
out.push_str(&self.emit_rom(bytecode));
out.push('\n');
out.push_str(Self::emit_stack_shim());
out.push('\n');
out.push_str(&self.emit_tb_top(bytecode.len()));
out
}
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));
}
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
)
}
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
"#
}
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 )
}
pub fn iverilog_available() -> bool {
std::process::Command::new("iverilog")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
pub fn run_iverilog(tb_path: &str) -> Result<String, String> {
let vvp_path = tb_path.replace(".v", ".vvp");
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)
));
}
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() }
}
#[cfg(test)]
mod tests {
use super::*;
fn minimal_prog() -> Vec<u8> {
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]; 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")); 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() {
let _ = BetSimEmitter::iverilog_available();
}
}