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