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