Skip to main content

bitloom_sim/
lib.rs

1//! Cycle-accurate simulation over FrozenHir (AD-5, AD-15, AD-17).
2
3use std::collections::BTreeMap;
4use std::fs::File;
5use std::io::{self, Write};
6use std::path::{Path, PathBuf};
7
8use bitloom_hir::{AssignExpr, AssignTarget, FrozenHir, GroundType, PortValues, ProcessKind, Stmt};
9
10pub use bitloom_hir::PortValues as Values;
11
12mod coverage;
13pub use coverage::{
14    Coverage, parse_branch_report, parse_report, parse_state_report, write_coverage_artifacts,
15};
16mod engine;
17pub use engine::TickEngine;
18mod equiv;
19pub use equiv::{
20    EquivStatus, check_functional_equiv, check_functional_equiv_generated, reset_then_run,
21};
22
23mod shared_stimulus;
24pub use shared_stimulus::SharedStimulusScoreboard;
25
26mod formal_equiv;
27pub use formal_equiv::FormalEquivProduct;
28
29mod ip_dual;
30pub use ip_dual::{
31    Axi4LiteSlaveFunctional, GpioFunctional, I2cMasterFunctional, IpDualModelMatrix,
32    SpiMasterFunctional, SyncFifoFunctional, UartRxFunctional, UartTxFunctional,
33    axi4_lite_slave_dual_stimulus, gpio_dual_stimulus, i2c_master_dual_stimulus,
34    spi_master_dual_stimulus, sync_fifo_dual_stimulus, uart_rx_dual_stimulus,
35    uart_tx_dual_stimulus,
36};
37
38mod generate;
39pub use generate::{
40    GeneratedFunctional, emit_functional_crate, generate_functional_sim,
41    generate_functional_sim_with_bin,
42};
43
44mod cycle;
45pub use cycle::{
46    CycleAccurateSim, check_generated_bridge, check_generated_bridge_with,
47    emit_cycle_accurate_crate, generate_cycle_accurate_sim,
48};
49
50mod fst;
51pub use fst::{FstError, resolve_vcd2fst};
52
53mod systemc_tlm;
54pub use systemc_tlm::{
55    SYSTEMC_PIN_VERSION, SystemcToolchain, build_and_run_tlm_lt_smoke, emit_systemc_tlm_lt,
56    generate_systemc_tlm_lt, resolve_systemc,
57};
58mod systemc_tlm_at;
59pub use systemc_tlm_at::{
60    build_and_run_tlm_at_smoke, emit_systemc_tlm_at, generate_systemc_tlm_at,
61};
62
63/// Simulator state for one FrozenHir circuit.
64pub struct Sim {
65    hir: FrozenHir,
66    regs: BTreeMap<String, u64>,
67    mems: BTreeMap<String, Vec<u64>>,
68    /// SyncReadMem: data captured last cycle, applied this cycle (latency 1).
69    pending_mem_reads: BTreeMap<String, u64>,
70    ports: PortValues,
71    time: u64,
72    vcd: Option<VcdWriter>,
73    fst: Option<FstPlan>,
74    engine: TickEngine,
75    kernel: engine::CompiledKernel,
76    coverage: Coverage,
77}
78
79struct FstPlan {
80    converter: PathBuf,
81    vcd: PathBuf,
82    fst: PathBuf,
83}
84
85struct VcdWriter {
86    file: File,
87    vars: Vec<String>,
88}
89
90impl Sim {
91    pub fn new(hir: FrozenHir) -> Self {
92        Self::with_engine(hir, TickEngine::Interpreter)
93    }
94
95    pub fn with_engine(hir: FrozenHir, engine: TickEngine) -> Self {
96        let mut regs = BTreeMap::new();
97        let mut mems = BTreeMap::new();
98        for m in &hir.circuit().modules {
99            for stmt in &m.body {
100                match stmt {
101                    Stmt::RegDecl { name, .. } => {
102                        regs.insert(name.clone(), 0);
103                    }
104                    Stmt::MemDecl {
105                        name,
106                        depth,
107                        width,
108                        init,
109                        ..
110                    } => {
111                        let _ = width;
112                        let words = match init {
113                            Some(v) => v.clone(),
114                            None => vec![0; *depth as usize],
115                        };
116                        mems.insert(name.clone(), words);
117                    }
118                    _ => {}
119                }
120            }
121        }
122        let kernel = engine::compile(&hir);
123        Self {
124            hir,
125            regs,
126            mems,
127            pending_mem_reads: BTreeMap::new(),
128            ports: PortValues::default(),
129            time: 0,
130            vcd: None,
131            fst: None,
132            engine,
133            kernel,
134            coverage: Coverage::default(),
135        }
136    }
137
138    pub fn engine(&self) -> TickEngine {
139        self.engine
140    }
141
142    pub fn enable_vcd(&mut self, path: impl AsRef<Path>) -> io::Result<()> {
143        let mut file = File::create(path)?;
144        writeln!(file, "$timescale 1ns $end")?;
145        writeln!(file, "$scope module {} $end", self.hir.abi_name)?;
146        let mut vars = Vec::new();
147        if let Some(m) = self.hir.circuit().modules.first() {
148            for p in &m.ports {
149                let w = match &p.ty {
150                    GroundType::UInt { width } | GroundType::SInt { width } => *width,
151                    _ => 1,
152                };
153                writeln!(file, "$var wire {w} {} {} $end", p.name, p.name)?;
154                vars.push(p.name.clone());
155            }
156            for stmt in &m.body {
157                if let Stmt::RegDecl { name, ty, .. } = stmt {
158                    let w = match ty {
159                        GroundType::UInt { width } | GroundType::SInt { width } => *width,
160                        _ => 1,
161                    };
162                    writeln!(file, "$var reg {w} {name} {name} $end")?;
163                    vars.push(name.clone());
164                }
165            }
166        }
167        writeln!(file, "$upscope $end")?;
168        writeln!(file, "$enddefinitions $end")?;
169        self.vcd = Some(VcdWriter { file, vars });
170        self.dump_vcd()?;
171        Ok(())
172    }
173
174    /// Optional FST: still dumps VCD, then converts with `vcd2fst` (AD-24).
175    pub fn enable_fst(&mut self, path: impl AsRef<Path>) -> Result<(), FstError> {
176        let converter = resolve_vcd2fst()?;
177        self.enable_fst_with(path, converter)
178    }
179
180    /// Same as `enable_fst` with an explicit converter (tests / `RHDL_VCD2FST`).
181    pub fn enable_fst_with(
182        &mut self,
183        path: impl AsRef<Path>,
184        converter: impl AsRef<Path>,
185    ) -> Result<(), FstError> {
186        let fst = path.as_ref().to_path_buf();
187        let vcd = fst.with_extension("vcd");
188        self.enable_vcd(&vcd)?;
189        self.fst = Some(FstPlan {
190            converter: converter.as_ref().to_path_buf(),
191            vcd,
192            fst,
193        });
194        Ok(())
195    }
196
197    /// Flush VCD and, if FST was requested, run the documented converter.
198    pub fn finish_waves(&mut self) -> Result<(), FstError> {
199        if let Some(mut w) = self.vcd.take() {
200            w.file.flush()?;
201        }
202        if let Some(plan) = self.fst.take() {
203            fst::convert_vcd_to_fst(&plan.converter, &plan.vcd, &plan.fst)?;
204        }
205        Ok(())
206    }
207
208    fn dump_vcd(&mut self) -> io::Result<()> {
209        let Some(vcd) = self.vcd.as_mut() else {
210            return Ok(());
211        };
212        writeln!(vcd.file, "#{}", self.time)?;
213        for name in &vcd.vars {
214            let val = self
215                .ports
216                .get(name)
217                .or_else(|| self.regs.get(name).copied())
218                .unwrap_or(0);
219            writeln!(vcd.file, "b{val:b} {name}")?;
220        }
221        Ok(())
222    }
223
224    pub fn set_inputs(&mut self, inputs: PortValues) {
225        for (k, v) in inputs.values {
226            self.ports.set(k, v);
227        }
228    }
229
230    pub fn ports(&self) -> &PortValues {
231        &self.ports
232    }
233
234    fn lookup(&self, name: &str) -> u64 {
235        self.ports
236            .get(name)
237            .or_else(|| self.regs.get(name).copied())
238            .unwrap_or(0)
239    }
240
241    fn eval(&mut self, expr: &AssignExpr) -> u64 {
242        match expr {
243            AssignExpr::Ref(n) => self.lookup(n),
244            AssignExpr::Lit(v) => *v,
245            AssignExpr::Inc(n) => self.lookup(n).wrapping_add(1),
246            AssignExpr::Add(a, b) => self.lookup(a).wrapping_add(self.lookup(b)),
247            AssignExpr::Sub(a, b) => self.lookup(a).wrapping_sub(self.lookup(b)),
248            AssignExpr::And(a, b) => self.lookup(a) & self.lookup(b),
249            AssignExpr::Or(a, b) => self.lookup(a) | self.lookup(b),
250            AssignExpr::Xor(a, b) => self.lookup(a) ^ self.lookup(b),
251            AssignExpr::Shl(a, b) => self.lookup(a) << (self.lookup(b) & 63),
252            AssignExpr::Shr(a, b) => self.lookup(a) >> (self.lookup(b) & 63),
253            AssignExpr::Eq(a, b) => u64::from(self.lookup(a) == self.lookup(b)),
254            AssignExpr::Mux { sel, t, f } => {
255                let took_true = self.lookup(sel) != 0;
256                self.coverage.sample_mux_branch(sel, took_true);
257                if took_true {
258                    self.lookup(t)
259                } else {
260                    self.lookup(f)
261                }
262            }
263            AssignExpr::MemRead { mem, addr } => {
264                let a = self.lookup(addr) as usize;
265                self.mems
266                    .get(mem)
267                    .and_then(|m| m.get(a).copied())
268                    .unwrap_or(0)
269            }
270        }
271    }
272
273    fn reg_meta(&self, name: &str) -> (bool, bool) {
274        for m in &self.hir.circuit().modules {
275            for stmt in &m.body {
276                if let Stmt::RegDecl {
277                    name: n,
278                    async_reset,
279                    has_enable,
280                    ..
281                } = stmt
282                {
283                    if n == name {
284                        return (*async_reset, *has_enable);
285                    }
286                }
287            }
288        }
289        (false, false)
290    }
291
292    fn mem_is_sync(&self, name: &str) -> bool {
293        for m in &self.hir.circuit().modules {
294            for stmt in &m.body {
295                if let Stmt::MemDecl {
296                    name: n, sync_read, ..
297                } = stmt
298                {
299                    if n == name {
300                        return *sync_read;
301                    }
302                }
303            }
304        }
305        false
306    }
307
308    fn enable_active(&self) -> bool {
309        let has_en_port = self
310            .hir
311            .circuit()
312            .modules
313            .iter()
314            .any(|m| m.ports.iter().any(|p| p.name == "en"));
315        if has_en_port {
316            self.lookup("en") != 0
317        } else {
318            true
319        }
320    }
321
322    fn reset_active(&self, m: &bitloom_hir::Module) -> bool {
323        let rst = m
324            .ports
325            .iter()
326            .find(|p| matches!(p.ty, GroundType::Reset))
327            .map(|p| p.name.as_str())
328            .unwrap_or("rst");
329        self.lookup(rst) != 0
330    }
331
332    /// One rising edge of the module Clock (AD-15). Driven by FrozenHir assigns.
333    pub fn tick(&mut self) {
334        match self.engine {
335            TickEngine::Interpreter => self.tick_interpreter(),
336            TickEngine::Compiled => self.tick_compiled(),
337        }
338        self.sample_coverage();
339        self.time += 1;
340        let _ = self.dump_vcd();
341    }
342
343    fn sample_coverage(&mut self) {
344        let names: Vec<String> = {
345            let mut n = Vec::new();
346            if let Some(m) = self.hir.circuit().modules.first() {
347                for p in &m.ports {
348                    n.push(p.name.clone());
349                }
350                for stmt in &m.body {
351                    if let Stmt::RegDecl { name, .. } = stmt {
352                        n.push(name.clone());
353                    }
354                }
355            }
356            n
357        };
358        for name in names {
359            let val = self.lookup(&name);
360            self.coverage.sample(name, val);
361        }
362    }
363
364    pub fn coverage_report(&self) -> String {
365        self.coverage.report()
366    }
367
368    /// Borrow the live coverage recorder (FR105/FR109/FR114).
369    pub fn coverage(&self) -> &Coverage {
370        &self.coverage
371    }
372
373    /// Write FR114 `coverage.lcov` + `coverage.html` under `out_dir`.
374    pub fn write_coverage_artifacts(&self, out_dir: &Path) -> io::Result<(PathBuf, PathBuf)> {
375        write_coverage_artifacts(&self.coverage, out_dir)
376    }
377
378    /// Register FSM state labels for FR109 / C3 state-visit coverage.
379    pub fn register_fsm_states<I, S>(&mut self, fsm: &str, states: I)
380    where
381        I: IntoIterator<Item = S>,
382        S: Into<String>,
383    {
384        self.coverage.register_fsm_states(fsm, states);
385    }
386
387    /// Record a visit to an FSM state (after [`Self::register_fsm_states`]).
388    pub fn sample_state_visit(&mut self, fsm: &str, state: &str) {
389        self.coverage.sample_state_visit(fsm, state);
390    }
391
392    fn tick_interpreter(&mut self) {
393        self.tick_sequential();
394        self.tick_combinational();
395    }
396
397    /// Recompute combinational nets from current ports/regs without a clock edge.
398    ///
399    /// Use after [`Self::set_inputs`] when sequential logic samples same-cycle
400    /// enables derived from inputs (e.g. `wr_en && !full`).
401    pub fn settle(&mut self) {
402        self.tick_combinational();
403    }
404
405    fn tick_compiled(&mut self) {
406        // Compiled schedule covers plain RegD/Net; mem / enable share the interpreter path.
407        if self.mems.is_empty()
408            && !self.hir.circuit().modules.iter().any(|m| {
409                m.body.iter().any(|s| {
410                    matches!(
411                        s,
412                        Stmt::RegDecl {
413                            has_enable: true,
414                            ..
415                        }
416                    )
417                })
418            })
419        {
420            let Some(m) = self.hir.circuit().modules.first().cloned() else {
421                return;
422            };
423            let reset = self.reset_active(&m);
424            let seq = self.kernel.seq.clone();
425            let comb = self.kernel.comb.clone();
426            // NBA: evaluate all next values before committing register updates.
427            let mut next_regs: BTreeMap<String, u64> = BTreeMap::new();
428            for (name, expr) in seq {
429                let next = if reset { 0 } else { self.eval(&expr) };
430                next_regs.insert(name, next);
431            }
432            for (name, val) in next_regs {
433                self.regs.insert(name, val);
434            }
435            for (name, expr) in comb {
436                let val = self.eval(&expr);
437                self.ports.set(name, val);
438            }
439            return;
440        }
441        self.tick_interpreter();
442    }
443
444    fn tick_sequential(&mut self) {
445        let Some(m) = self.hir.circuit().modules.first().cloned() else {
446            return;
447        };
448        let reset = self.reset_active(&m);
449        let enable = self.enable_active();
450
451        // Apply SyncReadMem pending data from previous cycle (latency 1).
452        let pending = std::mem::take(&mut self.pending_mem_reads);
453        for (name, val) in pending {
454            if reset {
455                self.regs.insert(name, 0);
456            } else {
457                self.regs.insert(name, val);
458            }
459        }
460
461        let mut next_pending = BTreeMap::new();
462        // NBA: evaluate all RegD next-values against the pre-edge register file,
463        // then commit — required for 2-flop CDC latency (FR79 / AD-29).
464        let mut next_regs: BTreeMap<String, u64> = BTreeMap::new();
465        for stmt in &m.body {
466            if let Stmt::Process(p) = stmt {
467                if p.kind != ProcessKind::Sequential {
468                    continue;
469                }
470                for a in &p.assigns {
471                    match &a.target {
472                        AssignTarget::RegD(name) => {
473                            let (_async_rst, has_en) = self.reg_meta(name);
474                            if reset {
475                                next_regs.insert(name.clone(), 0);
476                                continue;
477                            }
478                            if has_en && !enable {
479                                continue;
480                            }
481                            match &a.expr {
482                                AssignExpr::MemRead { mem, addr } if self.mem_is_sync(mem) => {
483                                    let val = self.eval(&AssignExpr::MemRead {
484                                        mem: mem.clone(),
485                                        addr: addr.clone(),
486                                    });
487                                    next_pending.insert(name.clone(), val);
488                                }
489                                _ => {
490                                    let next = self.eval(&a.expr);
491                                    next_regs.insert(name.clone(), next);
492                                }
493                            }
494                        }
495                        AssignTarget::MemWrite { mem, addr, we } => {
496                            if reset {
497                                continue;
498                            }
499                            if let Some(en) = we {
500                                if self.lookup(en) == 0 {
501                                    continue;
502                                }
503                            }
504                            let a_idx = self.lookup(addr) as usize;
505                            let data = self.eval(&a.expr);
506                            if let Some(bank) = self.mems.get_mut(mem) {
507                                if a_idx < bank.len() {
508                                    bank[a_idx] = data;
509                                }
510                            }
511                        }
512                        _ => {}
513                    }
514                }
515            }
516        }
517        for (name, val) in next_regs {
518            self.regs.insert(name, val);
519        }
520        self.pending_mem_reads = next_pending;
521    }
522
523    fn tick_combinational(&mut self) {
524        let Some(m) = self.hir.circuit().modules.first().cloned() else {
525            return;
526        };
527        for stmt in &m.body {
528            if let Stmt::Process(p) = stmt {
529                if p.kind == ProcessKind::Combinational {
530                    for a in &p.assigns {
531                        if let AssignTarget::Net(name) = &a.target {
532                            let val = self.eval(&a.expr);
533                            self.ports.set(name.clone(), val);
534                        }
535                    }
536                }
537            }
538        }
539    }
540}
541
542/// A handwritten abstraction/bridge cycle (FR29). Compared only via `PortValues`.
543pub trait AbstractionView {
544    fn cycle(&mut self, inputs: &PortValues) -> PortValues;
545}
546
547/// One named port that differed between two views.
548#[derive(Debug, Clone, PartialEq, Eq)]
549pub struct PortMismatch {
550    pub name: String,
551    pub left: Option<u64>,
552    pub right: Option<u64>,
553}
554
555/// Compare `PortValues` on the intersection of keys (FR29: PortValues only, no TLM).
556pub fn compare_port_values(left: &PortValues, right: &PortValues) -> Result<(), Vec<PortMismatch>> {
557    let mut mismatches = Vec::new();
558    for name in left.values.keys() {
559        if !right.values.contains_key(name) {
560            continue;
561        }
562        let l = left.get(name);
563        let r = right.get(name);
564        if l != r {
565            mismatches.push(PortMismatch {
566                name: name.clone(),
567                left: l,
568                right: r,
569            });
570        }
571    }
572    if mismatches.is_empty() {
573        Ok(())
574    } else {
575        Err(mismatches)
576    }
577}
578
579/// Tick RTL and one handwritten view; fail when documented ports disagree.
580pub fn check_mixed_both<A: AbstractionView>(
581    sim: &mut Sim,
582    abs: &mut A,
583    inputs: PortValues,
584) -> Result<(), Vec<PortMismatch>> {
585    sim.set_inputs(inputs.clone());
586    sim.tick();
587    let abs_out = abs.cycle(&inputs);
588    compare_port_values(sim.ports(), &abs_out)
589}
590
591#[cfg(test)]
592mod tests {
593    use bitloom_builder::{ElaborateSession, GroundType, Span};
594
595    use super::*;
596
597    fn counter_hir() -> FrozenHir {
598        let mut s = ElaborateSession::new("t");
599        s.begin_module("Counter", Span::default());
600        s.add_input("clk", GroundType::Clock, Span::default());
601        s.add_input("rst", GroundType::Reset, Span::default());
602        s.add_input("data_in", GroundType::UInt { width: 8 }, Span::default());
603        s.add_output("data_out", GroundType::UInt { width: 8 }, Span::default());
604        s.declare_reg("count", GroundType::UInt { width: 8 }, Span::default());
605        s.begin_combinational(Span::default());
606        s.assign_net("data_out", "count", Span::default());
607        s.end_process();
608        s.begin_sequential(Span::default());
609        s.assign_reg_d_inc("count", Span::default());
610        s.end_process();
611        s.end_module();
612        s.finish().unwrap()
613    }
614
615    fn passthrough_hir() -> FrozenHir {
616        let mut s = ElaborateSession::new("t");
617        s.begin_module("Pass", Span::default());
618        s.add_input("clk", GroundType::Clock, Span::default());
619        s.add_input("rst", GroundType::Reset, Span::default());
620        s.add_input("data_in", GroundType::UInt { width: 8 }, Span::default());
621        s.add_output("data_out", GroundType::UInt { width: 8 }, Span::default());
622        s.begin_combinational(Span::default());
623        s.assign_net("data_out", "data_in", Span::default());
624        s.end_process();
625        s.end_module();
626        s.finish().unwrap()
627    }
628
629    #[test]
630    fn tick_counts_after_reset() {
631        let mut sim = Sim::new(counter_hir());
632        let mut pv = PortValues::default();
633        pv.set("rst", 1);
634        sim.set_inputs(pv.clone());
635        sim.tick();
636        pv.set("rst", 0);
637        sim.set_inputs(pv);
638        sim.tick();
639        sim.tick();
640        sim.tick();
641        assert_eq!(sim.ports().get("data_out"), Some(3));
642    }
643
644    #[test]
645    fn tick_passthrough_non_counter() {
646        let mut sim = Sim::new(passthrough_hir());
647        let mut pv = PortValues::default();
648        pv.set("rst", 0);
649        pv.set("data_in", 0xA5);
650        sim.set_inputs(pv);
651        sim.tick();
652        assert_eq!(sim.ports().get("data_out"), Some(0xA5));
653        let mut pv2 = PortValues::default();
654        pv2.set("data_in", 0x3C);
655        sim.set_inputs(pv2);
656        sim.tick();
657        assert_eq!(sim.ports().get("data_out"), Some(0x3C));
658    }
659
660    fn adder_hir() -> FrozenHir {
661        let mut s = ElaborateSession::new("t");
662        s.begin_module("Add8", Span::default());
663        s.add_input("clk", GroundType::Clock, Span::default());
664        s.add_input("rst", GroundType::Reset, Span::default());
665        s.add_input("a", GroundType::UInt { width: 8 }, Span::default());
666        s.add_input("b", GroundType::UInt { width: 8 }, Span::default());
667        s.add_output("y", GroundType::UInt { width: 8 }, Span::default());
668        s.begin_combinational(Span::default());
669        s.assign_add("y", "a", "b", Span::default());
670        s.end_process();
671        s.end_module();
672        s.finish().unwrap()
673    }
674
675    #[test]
676    fn tick_same_width_add() {
677        let mut sim = Sim::new(adder_hir());
678        let mut pv = PortValues::default();
679        pv.set("rst", 0);
680        pv.set("a", 3);
681        pv.set("b", 5);
682        sim.set_inputs(pv);
683        sim.tick();
684        assert_eq!(sim.ports().get("y"), Some(8));
685    }
686
687    #[test]
688    fn vcd_written() {
689        let dir = std::env::temp_dir().join("rhdl_sim_vcd_test.vcd");
690        let mut sim = Sim::new(counter_hir());
691        sim.enable_vcd(&dir).unwrap();
692        let mut pv = PortValues::default();
693        pv.set("rst", 0);
694        sim.set_inputs(pv);
695        sim.tick();
696        let text = std::fs::read_to_string(&dir).unwrap();
697        assert!(text.contains("$var"));
698        assert!(text.contains("count"));
699        let _ = std::fs::remove_file(dir);
700    }
701
702    #[test]
703    fn fst_off_still_writes_vcd() {
704        let dir = std::env::temp_dir().join("rhdl_sim_fst_off.vcd");
705        let mut sim = Sim::new(counter_hir());
706        sim.enable_vcd(&dir).unwrap();
707        sim.tick();
708        let text = std::fs::read_to_string(&dir).unwrap();
709        assert!(text.contains("$var"));
710        assert!(!dir.with_extension("fst").is_file());
711        let _ = std::fs::remove_file(&dir);
712    }
713
714    #[test]
715    fn fst_missing_converter_errors() {
716        let err =
717            crate::fst::resolve_vcd2fst_from(None, Some("/no-such-rhdl-path".into())).unwrap_err();
718        let msg = err.to_string();
719        assert!(msg.contains("vcd2fst"));
720    }
721
722    #[test]
723    fn fst_via_documented_converter() {
724        let tmp = std::env::temp_dir().join("rhdl_fst_stub");
725        let _ = std::fs::create_dir_all(&tmp);
726        let stub = tmp.join("vcd2fst");
727        std::fs::write(&stub, "#!/bin/sh\ncp \"$1\" \"$2\"\n").unwrap();
728        #[cfg(unix)]
729        {
730            use std::os::unix::fs::PermissionsExt;
731            let mut p = std::fs::metadata(&stub).unwrap().permissions();
732            p.set_mode(0o755);
733            std::fs::set_permissions(&stub, p).unwrap();
734        }
735        let fst = tmp.join("wave.fst");
736        let mut sim = Sim::new(counter_hir());
737        sim.enable_fst_with(&fst, &stub).unwrap();
738        let mut pv = PortValues::default();
739        pv.set("rst", 0);
740        sim.set_inputs(pv);
741        sim.tick();
742        sim.finish_waves().unwrap();
743        assert!(fst.is_file());
744        assert!(fst.with_extension("vcd").is_file());
745        let _ = std::fs::remove_file(&fst);
746        let _ = std::fs::remove_file(fst.with_extension("vcd"));
747    }
748
749    #[test]
750    fn functional_model_matches_tick_portvalues() {
751        struct CounterFm {
752            count: u64,
753        }
754        impl CounterFm {
755            fn cycle(&mut self, inputs: &PortValues) -> PortValues {
756                if inputs.get("rst").unwrap_or(0) != 0 {
757                    self.count = 0;
758                } else {
759                    self.count = self.count.wrapping_add(1);
760                }
761                let mut out = inputs.clone();
762                out.set("data_out", self.count);
763                out
764            }
765        }
766
767        let mut sim = Sim::new(counter_hir());
768        let mut fm = CounterFm { count: 0 };
769        let mut pv = PortValues::default();
770        pv.set("rst", 1);
771        sim.set_inputs(pv.clone());
772        sim.tick();
773        let _ = fm.cycle(&pv);
774        pv.set("rst", 0);
775        for _ in 0..5 {
776            sim.set_inputs(pv.clone());
777            sim.tick();
778            let fm_out = fm.cycle(&pv);
779            assert_eq!(sim.ports().get("data_out"), fm_out.get("data_out"));
780        }
781    }
782
783    struct CounterAbs {
784        count: u64,
785    }
786    impl AbstractionView for CounterAbs {
787        fn cycle(&mut self, inputs: &PortValues) -> PortValues {
788            if inputs.get("rst").unwrap_or(0) != 0 {
789                self.count = 0;
790            } else {
791                self.count = self.count.wrapping_add(1);
792            }
793            let mut out = inputs.clone();
794            out.set("data_out", self.count);
795            out
796        }
797    }
798
799    #[test]
800    fn mixed_both_portvalues_match() {
801        let mut sim = Sim::new(counter_hir());
802        let mut abs = CounterAbs { count: 0 };
803        let mut pv = PortValues::default();
804        pv.set("rst", 1);
805        check_mixed_both(&mut sim, &mut abs, pv.clone()).unwrap();
806        pv.set("rst", 0);
807        for _ in 0..4 {
808            check_mixed_both(&mut sim, &mut abs, pv.clone()).unwrap();
809        }
810        assert_eq!(sim.ports().get("data_out"), Some(4));
811    }
812
813    #[test]
814    fn mixed_both_mismatch_fails() {
815        struct WrongAbs;
816        impl AbstractionView for WrongAbs {
817            fn cycle(&mut self, inputs: &PortValues) -> PortValues {
818                let mut out = inputs.clone();
819                out.set("data_out", 99);
820                out
821            }
822        }
823        let mut sim = Sim::new(counter_hir());
824        let mut abs = WrongAbs;
825        let mut pv = PortValues::default();
826        pv.set("rst", 0);
827        let err = check_mixed_both(&mut sim, &mut abs, pv).unwrap_err();
828        assert!(err.iter().any(|m| m.name == "data_out"));
829    }
830
831    #[test]
832    fn no_hir_to_tlm_api() {
833        // FR29: no HIR→TLM *lowering* that replaces cycle-accurate tick.
834        // FR101 may export `systemc_tlm` / `emit_systemc_tlm_lt` as a separate product path
835        // (revised AD-5); that must not appear as `to_tlm` / bare `emit_tlm` on this surface.
836        let src = include_str!("lib.rs");
837        let prod = src.split("#[cfg(test)]").next().unwrap_or(src);
838        assert!(!prod.contains("fn to_tlm") && !prod.contains("pub fn to_tlm"));
839        assert!(!prod.contains("pub fn emit_tlm(") && !prod.contains("fn emit_tlm("));
840        if prod.contains("mod systemc_tlm") || prod.contains("emit_systemc_tlm") {
841            assert!(
842                prod.contains("FR101") || include_str!("systemc_tlm.rs").contains("FR101"),
843                "SystemC TLM surface must be labeled FR101"
844            );
845        }
846    }
847
848    #[test]
849    fn dual_view_equiv_pass() {
850        let mut abs = CounterAbs { count: 0 };
851        let st = check_functional_equiv(counter_hir(), &mut abs, reset_then_run(5));
852        assert!(st.is_pass());
853        assert_eq!(st, EquivStatus::Pass { cycles: 6 });
854    }
855
856    #[test]
857    fn dual_view_equiv_fail_on_deliberate_mismatch() {
858        struct WrongAbs;
859        impl AbstractionView for WrongAbs {
860            fn cycle(&mut self, inputs: &PortValues) -> PortValues {
861                let mut out = inputs.clone();
862                out.set("data_out", 42);
863                out
864            }
865        }
866        let st = check_functional_equiv(counter_hir(), &mut WrongAbs, reset_then_run(2));
867        assert!(!st.is_pass());
868        match st {
869            EquivStatus::Fail { mismatches, .. } => {
870                assert!(mismatches.iter().any(|m| m.name == "data_out"));
871            }
872            EquivStatus::Pass { .. } => panic!("expected fail"),
873        }
874    }
875
876    fn collect_trace(engine: TickEngine, n: usize) -> Vec<Option<u64>> {
877        let mut sim = Sim::with_engine(counter_hir(), engine);
878        let mut pv = PortValues::default();
879        pv.set("rst", 1);
880        sim.set_inputs(pv.clone());
881        sim.tick();
882        pv.set("rst", 0);
883        let mut out = vec![sim.ports().get("data_out")];
884        for _ in 0..n {
885            sim.set_inputs(pv.clone());
886            sim.tick();
887            out.push(sim.ports().get("data_out"));
888        }
889        out
890    }
891
892    #[test]
893    fn interpreter_and_compiled_portvalues_match() {
894        assert_eq!(
895            collect_trace(TickEngine::Interpreter, 6),
896            collect_trace(TickEngine::Compiled, 6)
897        );
898        assert_eq!(
899            TickEngine::from_name("compiled"),
900            Some(TickEngine::Compiled)
901        );
902        let mut a = Sim::with_engine(passthrough_hir(), TickEngine::Interpreter);
903        let mut b = Sim::with_engine(passthrough_hir(), TickEngine::Compiled);
904        let mut pv = PortValues::default();
905        pv.set("data_in", 0x5A);
906        a.set_inputs(pv.clone());
907        b.set_inputs(pv);
908        a.tick();
909        b.tick();
910        assert_eq!(a.ports().get("data_out"), b.ports().get("data_out"));
911        assert_eq!(a.engine().as_str(), "interpreter");
912    }
913
914    #[test]
915    fn coverage_has_hit_and_miss() {
916        let mut sim = Sim::new(counter_hir());
917        let mut pv = PortValues::default();
918        pv.set("rst", 0);
919        sim.set_inputs(pv);
920        sim.tick();
921        sim.tick();
922        let report = sim.coverage_report();
923        assert!(
924            report.starts_with("# bitloom-sim coverage v2")
925                || report.starts_with("# bitloom-sim coverage v3"),
926            "FR105/FR109 coverage header v2 (Mux) or v3 (with FSM)"
927        );
928        let (hits, misses) = parse_report(&report);
929        assert!(
930            hits.iter().any(|h| h == "data_out" || h == "count"),
931            "hits={hits:?}"
932        );
933        assert!(
934            misses.iter().any(|m| m == "data_in" || m == "clk"),
935            "misses={misses:?}"
936        );
937    }
938
939    fn sync_read_mem_hir() -> FrozenHir {
940        let mut s = ElaborateSession::new("t");
941        s.begin_module("Srm", Span::default());
942        s.add_input("clk", GroundType::Clock, Span::default());
943        s.add_input("rst", GroundType::Reset, Span::default());
944        s.add_input("addr", GroundType::UInt { width: 4 }, Span::default());
945        s.add_input("wdata", GroundType::UInt { width: 8 }, Span::default());
946        s.add_input("we", GroundType::Bool, Span::default());
947        s.add_output("rdata", GroundType::UInt { width: 8 }, Span::default());
948        s.declare_sync_read_mem("ram", 16, 8, Span::default());
949        s.declare_reg("q", GroundType::UInt { width: 8 }, Span::default());
950        s.begin_combinational(Span::default());
951        s.assign_net("rdata", "q", Span::default());
952        s.end_process();
953        s.begin_sequential(Span::default());
954        // Always write for this golden; we gate with we in a fuller surface later.
955        s.assign_mem_write("ram", "addr", "wdata", Span::default());
956        s.assign_reg_d_mem_read("q", "ram", "addr", Span::default());
957        s.end_process();
958        s.end_module();
959        s.finish().unwrap()
960    }
961
962    #[test]
963    fn sync_read_mem_read_latency_one() {
964        let mut sim = Sim::new(sync_read_mem_hir());
965        let mut pv = PortValues::default();
966        pv.set("rst", 0);
967        pv.set("addr", 3);
968        pv.set("wdata", 0xAB);
969        pv.set("we", 1);
970        sim.set_inputs(pv.clone());
971        sim.tick(); // write + schedule read of old (0)
972        assert_eq!(sim.ports().get("rdata"), Some(0));
973        sim.tick(); // pending read delivers written value
974        assert_eq!(sim.ports().get("rdata"), Some(0xAB));
975    }
976
977    fn async_reset_hir() -> FrozenHir {
978        let mut s = ElaborateSession::new("t");
979        s.begin_module("AsyncCnt", Span::default());
980        s.add_input("clk", GroundType::Clock, Span::default());
981        s.add_input("rst", GroundType::Reset, Span::default());
982        s.add_output("data_out", GroundType::UInt { width: 8 }, Span::default());
983        s.declare_reg_ex(
984            "count",
985            GroundType::UInt { width: 8 },
986            true,
987            false,
988            Span::default(),
989        );
990        s.begin_combinational(Span::default());
991        s.assign_net("data_out", "count", Span::default());
992        s.end_process();
993        s.begin_sequential(Span::default());
994        s.assign_reg_d_inc("count", Span::default());
995        s.end_process();
996        s.end_module();
997        s.finish().unwrap()
998    }
999
1000    #[test]
1001    fn async_reset_assert_and_release_tick_golden() {
1002        let mut sim = Sim::new(async_reset_hir());
1003        let mut pv = PortValues::default();
1004        pv.set("rst", 0);
1005        sim.set_inputs(pv.clone());
1006        sim.tick();
1007        sim.tick();
1008        assert_eq!(sim.ports().get("data_out"), Some(2));
1009        pv.set("rst", 1);
1010        sim.set_inputs(pv.clone());
1011        sim.tick();
1012        assert_eq!(sim.ports().get("data_out"), Some(0));
1013        pv.set("rst", 0);
1014        sim.set_inputs(pv);
1015        sim.tick();
1016        sim.tick();
1017        assert_eq!(sim.ports().get("data_out"), Some(2));
1018    }
1019
1020    fn enable_hir(has_enable: bool) -> FrozenHir {
1021        let mut s = ElaborateSession::new("t");
1022        s.begin_module("EnCnt", Span::default());
1023        s.add_input("clk", GroundType::Clock, Span::default());
1024        s.add_input("rst", GroundType::Reset, Span::default());
1025        s.add_input("en", GroundType::Bool, Span::default());
1026        s.add_output("data_out", GroundType::UInt { width: 8 }, Span::default());
1027        s.declare_reg_ex(
1028            "count",
1029            GroundType::UInt { width: 8 },
1030            false,
1031            has_enable,
1032            Span::default(),
1033        );
1034        s.begin_combinational(Span::default());
1035        s.assign_net("data_out", "count", Span::default());
1036        s.end_process();
1037        s.begin_sequential(Span::default());
1038        s.assign_reg_d_inc("count", Span::default());
1039        s.end_process();
1040        s.end_module();
1041        s.finish().unwrap()
1042    }
1043
1044    #[test]
1045    fn enable_high_matches_ungated_counter() {
1046        let mut gated = Sim::new(enable_hir(true));
1047        let mut plain = Sim::new(enable_hir(false));
1048        let mut pv = PortValues::default();
1049        pv.set("rst", 0);
1050        pv.set("en", 1);
1051        for _ in 0..4 {
1052            gated.set_inputs(pv.clone());
1053            plain.set_inputs(pv.clone());
1054            gated.tick();
1055            plain.tick();
1056            assert_eq!(gated.ports().get("data_out"), plain.ports().get("data_out"));
1057        }
1058    }
1059
1060    #[test]
1061    fn enable_low_holds_value() {
1062        let mut sim = Sim::new(enable_hir(true));
1063        let mut pv = PortValues::default();
1064        pv.set("rst", 0);
1065        pv.set("en", 1);
1066        sim.set_inputs(pv.clone());
1067        sim.tick();
1068        sim.tick();
1069        assert_eq!(sim.ports().get("data_out"), Some(2));
1070        pv.set("en", 0);
1071        sim.set_inputs(pv);
1072        sim.tick();
1073        sim.tick();
1074        assert_eq!(sim.ports().get("data_out"), Some(2));
1075    }
1076}