Skip to main content

bitloom_sim/
generate.rs

1//! FR47 leg 1: generate a Rust functional-sim crate from FrozenHir (AD-5).
2//! Minimal interpreter-backed AbstractionView — not HLS-quality codegen.
3
4use std::collections::BTreeMap;
5use std::fs;
6use std::io;
7use std::path::{Path, PathBuf};
8
9use bitloom_hir::{AssignExpr, AssignTarget, FrozenHir, GroundType, ProcessKind, Stmt};
10
11use crate::AbstractionView;
12use bitloom_hir::PortValues;
13
14/// Sequential op collected from FrozenHir (order preserved; matches `Sim::tick_sequential`).
15#[derive(Debug, Clone)]
16enum SeqOp {
17    RegD {
18        name: String,
19        expr: AssignExpr,
20        has_en: bool,
21    },
22    MemWrite {
23        mem: String,
24        addr: String,
25        we: Option<String>,
26        expr: AssignExpr,
27    },
28}
29
30/// In-process functional model derived from FrozenHir (FR47 / FR112 / FR159).
31///
32/// **FR112:** SyncReadMem / Mem `MemRead`+`MemWrite` semantics match cycle-accurate
33/// [`crate::Sim::tick`] (latency-1 sync read via `pending_mem_reads`) for the
34/// **in-process** view / `check_generated_bridge`.
35///
36/// **FR159:** [`generate_functional_sim`] emits the same MemRead/MemWrite +
37/// SyncReadMem latency-1 semantics into the standalone functional-sim crate
38/// (no longer stubs `MemRead` as `0`).
39#[derive(Debug, Clone)]
40pub struct GeneratedFunctional {
41    regs: BTreeMap<String, u64>,
42    mems: BTreeMap<String, Vec<u64>>,
43    mem_sync: BTreeMap<String, bool>,
44    pending_mem_reads: BTreeMap<String, u64>,
45    reset_port: String,
46    enable_port: Option<String>,
47    seq: Vec<SeqOp>,
48    /// Combinational Net updates: (net_name, expr).
49    comb: Vec<(String, AssignExpr)>,
50}
51
52impl GeneratedFunctional {
53    /// Build a functional model from the top module of `hir`.
54    pub fn from_hir(hir: &FrozenHir) -> Self {
55        let m = hir
56            .circuit()
57            .modules
58            .first()
59            .expect("FrozenHir has at least one module");
60        let reset_port = m
61            .ports
62            .iter()
63            .find(|p| matches!(p.ty, GroundType::Reset))
64            .map(|p| p.name.clone())
65            .unwrap_or_else(|| "rst".into());
66        let enable_port = m
67            .ports
68            .iter()
69            .find(|p| p.name == "en")
70            .map(|p| p.name.clone());
71
72        let mut regs = BTreeMap::new();
73        let mut reg_has_en = BTreeMap::new();
74        let mut mems = BTreeMap::new();
75        let mut mem_sync = BTreeMap::new();
76        for stmt in &m.body {
77            match stmt {
78                Stmt::RegDecl {
79                    name, has_enable, ..
80                } => {
81                    regs.insert(name.clone(), 0u64);
82                    reg_has_en.insert(name.clone(), *has_enable);
83                }
84                Stmt::MemDecl {
85                    name,
86                    depth,
87                    init,
88                    sync_read,
89                    ..
90                } => {
91                    let words = match init {
92                        Some(v) => v.clone(),
93                        None => vec![0; *depth as usize],
94                    };
95                    mems.insert(name.clone(), words);
96                    mem_sync.insert(name.clone(), *sync_read);
97                }
98                _ => {}
99            }
100        }
101
102        let mut seq = Vec::new();
103        let mut comb = Vec::new();
104        for stmt in &m.body {
105            if let Stmt::Process(p) = stmt {
106                match p.kind {
107                    ProcessKind::Sequential => {
108                        for a in &p.assigns {
109                            match &a.target {
110                                AssignTarget::RegD(name) => {
111                                    let has_en = reg_has_en.get(name).copied().unwrap_or(false);
112                                    seq.push(SeqOp::RegD {
113                                        name: name.clone(),
114                                        expr: a.expr.clone(),
115                                        has_en,
116                                    });
117                                }
118                                AssignTarget::MemWrite { mem, addr, we } => {
119                                    seq.push(SeqOp::MemWrite {
120                                        mem: mem.clone(),
121                                        addr: addr.clone(),
122                                        we: we.clone(),
123                                        expr: a.expr.clone(),
124                                    });
125                                }
126                                _ => {}
127                            }
128                        }
129                    }
130                    ProcessKind::Combinational => {
131                        for a in &p.assigns {
132                            if let AssignTarget::Net(name) = &a.target {
133                                comb.push((name.clone(), a.expr.clone()));
134                            }
135                        }
136                    }
137                }
138            }
139        }
140
141        Self {
142            regs,
143            mems,
144            mem_sync,
145            pending_mem_reads: BTreeMap::new(),
146            reset_port,
147            enable_port,
148            seq,
149            comb,
150        }
151    }
152
153    fn lookup(&self, inputs: &PortValues, name: &str) -> u64 {
154        inputs
155            .get(name)
156            .or_else(|| self.regs.get(name).copied())
157            .unwrap_or(0)
158    }
159
160    fn mem_is_sync(&self, name: &str) -> bool {
161        self.mem_sync.get(name).copied().unwrap_or(false)
162    }
163
164    fn eval_mem_read(&self, inputs: &PortValues, mem: &str, addr: &str) -> u64 {
165        let a = self.lookup(inputs, addr) as usize;
166        self.mems
167            .get(mem)
168            .and_then(|m| m.get(a).copied())
169            .unwrap_or(0)
170    }
171
172    fn eval(&self, inputs: &PortValues, expr: &AssignExpr) -> u64 {
173        match expr {
174            AssignExpr::Ref(n) => self.lookup(inputs, n),
175            AssignExpr::Lit(v) => *v,
176            AssignExpr::Inc(n) => self.lookup(inputs, n).wrapping_add(1),
177            AssignExpr::Add(a, b) => self.lookup(inputs, a).wrapping_add(self.lookup(inputs, b)),
178            AssignExpr::Sub(a, b) => self.lookup(inputs, a).wrapping_sub(self.lookup(inputs, b)),
179            AssignExpr::And(a, b) => self.lookup(inputs, a) & self.lookup(inputs, b),
180            AssignExpr::Or(a, b) => self.lookup(inputs, a) | self.lookup(inputs, b),
181            AssignExpr::Xor(a, b) => self.lookup(inputs, a) ^ self.lookup(inputs, b),
182            AssignExpr::Shl(a, b) => self.lookup(inputs, a) << (self.lookup(inputs, b) & 63),
183            AssignExpr::Shr(a, b) => self.lookup(inputs, a) >> (self.lookup(inputs, b) & 63),
184            AssignExpr::Eq(a, b) => u64::from(self.lookup(inputs, a) == self.lookup(inputs, b)),
185            AssignExpr::Mux { sel, t, f } => {
186                if self.lookup(inputs, sel) != 0 {
187                    self.lookup(inputs, t)
188                } else {
189                    self.lookup(inputs, f)
190                }
191            }
192            AssignExpr::MemRead { mem, addr } => self.eval_mem_read(inputs, mem, addr),
193        }
194    }
195}
196
197impl AbstractionView for GeneratedFunctional {
198    fn cycle(&mut self, inputs: &PortValues) -> PortValues {
199        let reset = inputs.get(&self.reset_port).unwrap_or(0) != 0;
200        let enable = self
201            .enable_port
202            .as_ref()
203            .map(|p| inputs.get(p).unwrap_or(0) != 0)
204            .unwrap_or(true);
205
206        // Apply SyncReadMem pending from previous cycle (latency 1) — matches Sim.
207        let pending = std::mem::take(&mut self.pending_mem_reads);
208        for (name, val) in pending {
209            self.regs.insert(name, if reset { 0 } else { val });
210        }
211
212        let mut next_pending = BTreeMap::new();
213        let mut next_regs: BTreeMap<String, u64> = BTreeMap::new();
214        // Clone ops so MemWrite can mutate `mems` without borrowing `seq`.
215        let ops = self.seq.clone();
216
217        for op in &ops {
218            match op {
219                SeqOp::RegD { name, expr, has_en } => {
220                    if reset {
221                        next_regs.insert(name.clone(), 0);
222                        continue;
223                    }
224                    if *has_en && !enable {
225                        continue;
226                    }
227                    match expr {
228                        AssignExpr::MemRead { mem, addr } if self.mem_is_sync(mem) => {
229                            let val = self.eval_mem_read(inputs, mem, addr);
230                            next_pending.insert(name.clone(), val);
231                        }
232                        _ => {
233                            next_regs.insert(name.clone(), self.eval(inputs, expr));
234                        }
235                    }
236                }
237                SeqOp::MemWrite {
238                    mem,
239                    addr,
240                    we,
241                    expr,
242                } => {
243                    if reset {
244                        continue;
245                    }
246                    if let Some(en) = we {
247                        if self.lookup(inputs, en) == 0 {
248                            continue;
249                        }
250                    }
251                    let a_idx = self.lookup(inputs, addr) as usize;
252                    let data = self.eval(inputs, expr);
253                    if let Some(bank) = self.mems.get_mut(mem) {
254                        if a_idx < bank.len() {
255                            bank[a_idx] = data;
256                        }
257                    }
258                }
259            }
260        }
261        for (k, v) in next_regs {
262            self.regs.insert(k, v);
263        }
264        self.pending_mem_reads = next_pending;
265
266        let mut out = inputs.clone();
267        for (name, expr) in &self.comb {
268            // Prefer updated regs over prior port values (matches Sim::tick_combinational).
269            out.set(name.clone(), self.eval(&out, expr));
270        }
271        out
272    }
273}
274
275/// Alias required by Story 21.2→21.3 product surface naming.
276pub fn emit_functional_crate(hir: &FrozenHir, out_dir: &Path) -> io::Result<PathBuf> {
277    generate_functional_sim(hir, out_dir)
278}
279
280/// Write a standalone Rust functional-sim crate under `out_dir`.
281///
282/// Includes `src/lib.rs` (FunctionalSim + gold test) and `src/main.rs` for `cargo run`.
283pub fn generate_functional_sim(hir: &FrozenHir, out_dir: &Path) -> io::Result<PathBuf> {
284    fs::create_dir_all(out_dir.join("src"))?;
285    let pkg = sanitize_pkg_name(&hir.abi_name);
286    let model = GeneratedFunctional::from_hir(hir);
287    let cargo = render_cargo_toml(&pkg, out_dir)?;
288    let lib = render_lib_rs(&pkg, &model);
289    fs::write(out_dir.join("Cargo.toml"), cargo)?;
290    fs::write(out_dir.join("src/lib.rs"), lib)?;
291    write_functional_main(out_dir)?;
292    Ok(out_dir.to_path_buf())
293}
294
295fn sanitize_pkg_name(abi: &str) -> String {
296    let mut s: String = abi
297        .chars()
298        .map(|c| {
299            if c.is_ascii_alphanumeric() || c == '_' {
300                c.to_ascii_lowercase()
301            } else {
302                '_'
303            }
304        })
305        .collect();
306    if s.is_empty() || s.chars().next().is_some_and(|c| c.is_ascii_digit()) {
307        s = format!("func_{s}");
308    }
309    format!("bitloom_func_{s}")
310}
311
312fn render_cargo_toml(pkg: &str, out_dir: &Path) -> io::Result<String> {
313    let hir_dep = resolve_hir_dep(out_dir);
314    Ok(format!(
315        r#"[package]
316name = "{pkg}"
317version = "0.0.0"
318edition = "2024"
319rust-version = "1.97.1"
320publish = false
321description = "Generated Bitloom functional simulator (FR47). Not SystemC."
322
323# Keep generated crate out of the parent workspace.
324[workspace]
325
326[dependencies]
327{hir_dep}
328
329[[bin]]
330name = "{pkg}"
331path = "src/main.rs"
332"#
333    ))
334}
335
336fn resolve_hir_dep(out_dir: &Path) -> String {
337    // Prefer workspace path when generating inside the monorepo (tests / CLI).
338    let candidates = [
339        out_dir.join("../../crates/bitloom-hir").canonicalize().ok(),
340        std::env::var_os("CARGO_MANIFEST_DIR")
341            .and_then(|m| PathBuf::from(m).join("../bitloom-hir").canonicalize().ok()),
342        PathBuf::from(env!("CARGO_MANIFEST_DIR"))
343            .join("../bitloom-hir")
344            .canonicalize()
345            .ok(),
346    ];
347    for c in candidates.into_iter().flatten() {
348        if c.join("Cargo.toml").is_file() {
349            return format!("bitloom-hir = {{ path = \"{}\" }}", c.display());
350        }
351    }
352    format!("bitloom-hir = \"{}\"", env!("CARGO_PKG_VERSION"))
353}
354
355fn render_lib_rs(pkg: &str, model: &GeneratedFunctional) -> String {
356    let _ = pkg;
357    let reg_inits: String = model
358        .regs
359        .keys()
360        .map(|n| format!("        regs.insert({n:?}.into(), 0u64);\n"))
361        .collect();
362    let mem_inits: String = model
363        .mems
364        .iter()
365        .map(|(n, words)| {
366            let lit = words
367                .iter()
368                .map(|w| w.to_string())
369                .collect::<Vec<_>>()
370                .join(", ");
371            format!("        mems.insert({n:?}.into(), vec![{lit}]);\n")
372        })
373        .collect();
374    let seq_arms: String = model
375        .seq
376        .iter()
377        .map(|op| match op {
378            SeqOp::RegD { name, expr, has_en } => {
379                let body = match expr {
380                    AssignExpr::MemRead { mem, addr } if model.mem_is_sync(mem) => format!(
381                        "                    let val = self.eval_mem_read(inputs, {mem:?}, {addr:?});\n\
382                                            next_pending.insert({name:?}.into(), val);\n"
383                    ),
384                    _ => format!(
385                        "                    next_regs.insert({name:?}.into(), {});\n",
386                        render_expr(expr)
387                    ),
388                };
389                let en_guard = if *has_en {
390                    format!(
391                        "                if reset {{\n\
392                                            next_regs.insert({name:?}.into(), 0);\n\
393                                        }} else if enable {{\n\
394                         {body}                }}\n"
395                    )
396                } else {
397                    format!(
398                        "                if reset {{\n\
399                                            next_regs.insert({name:?}.into(), 0);\n\
400                                        }} else {{\n\
401                         {body}                }}\n"
402                    )
403                };
404                en_guard
405            }
406            SeqOp::MemWrite {
407                mem,
408                addr,
409                we,
410                expr,
411            } => {
412                let we_guard = match we {
413                    Some(w) => format!(
414                        "                if !reset && self.lookup(inputs, {w:?}) != 0 {{\n"
415                    ),
416                    None => "                if !reset {\n".into(),
417                };
418                format!(
419                    "{we_guard}                    let a_idx = self.lookup(inputs, {addr:?}) as usize;\n\
420                                        let data = {};\n\
421                                        if let Some(bank) = self.mems.get_mut({mem:?}) {{\n\
422                                            if a_idx < bank.len() {{\n\
423                                                bank[a_idx] = data;\n\
424                                            }}\n\
425                                        }}\n\
426                                    }}\n",
427                    render_expr(expr)
428                )
429            }
430        })
431        .collect();
432    let comb_arms: String = model
433        .comb
434        .iter()
435        .map(|(name, expr)| {
436            format!(
437                "        out.set({name:?}, {});\n",
438                render_expr_ports_regs(expr)
439            )
440        })
441        .collect();
442    let reset = &model.reset_port;
443    let enable_init = match &model.enable_port {
444        Some(p) => format!("let enable = inputs.get({p:?}).unwrap_or(0) != 0;"),
445        None => "#[allow(unused_variables)] let enable = true;".into(),
446    };
447    let mem_gold = if model.mems.is_empty() {
448        String::new()
449    } else {
450        format!(
451            r#"
452    #[test]
453    fn gold_sync_read_mem_latency1_write_then_read() {{
454        // FR159: emitted SyncReadMem path matches Sim / GeneratedFunctional latency-1.
455        let mut sim = FunctionalSim::new();
456        let mut pv = PortValues::default();
457        pv.set({reset:?}, 0);
458        pv.set("addr", 3);
459        pv.set("wdata", 0xAB);
460        pv.set("we", 1);
461        let c0 = sim.cycle(&pv);
462        if c0.values.contains_key("rdata") {{
463            assert_eq!(c0.get("rdata"), Some(0));
464        }}
465        let c1 = sim.cycle(&pv);
466        if c1.values.contains_key("rdata") {{
467            assert_eq!(c1.get("rdata"), Some(0xAB));
468        }}
469    }}
470"#
471        )
472    };
473
474    format!(
475        r#"//! Generated Bitloom functional simulator (FR47 / FR159 / AD-5).
476//! Not SystemC / TLM-2.0. Do not hand-edit; regenerate via `generate_functional_sim`.
477//! MemRead/MemWrite + SyncReadMem latency-1 match GeneratedFunctional / Sim::tick (FR159).
478
479use std::collections::BTreeMap;
480
481use bitloom_hir::PortValues;
482
483/// Generated functional view (AbstractionView-compatible cycle API).
484#[derive(Debug, Clone)]
485#[allow(dead_code)] // mems/pending unused on designs without MemRead
486pub struct FunctionalSim {{
487    regs: BTreeMap<String, u64>,
488    mems: BTreeMap<String, Vec<u64>>,
489    pending_mem_reads: BTreeMap<String, u64>,
490}}
491
492impl Default for FunctionalSim {{
493    fn default() -> Self {{
494        Self::new()
495    }}
496}}
497
498impl FunctionalSim {{
499    pub fn new() -> Self {{
500        let mut regs = BTreeMap::new();
501{reg_inits}        let mut mems = BTreeMap::new();
502{mem_inits}        Self {{
503            regs,
504            mems,
505            pending_mem_reads: BTreeMap::new(),
506        }}
507    }}
508
509    fn lookup(&self, inputs: &PortValues, name: &str) -> u64 {{
510        inputs
511            .get(name)
512            .or_else(|| self.regs.get(name).copied())
513            .unwrap_or(0)
514    }}
515
516    #[allow(dead_code)]
517    fn eval_mem_read(&self, inputs: &PortValues, mem: &str, addr: &str) -> u64 {{
518        let a = self.lookup(inputs, addr) as usize;
519        self.mems
520            .get(mem)
521            .and_then(|m| m.get(a).copied())
522            .unwrap_or(0)
523    }}
524
525    /// One untimed functional cycle; returns updated `PortValues`.
526    pub fn cycle(&mut self, inputs: &PortValues) -> PortValues {{
527        let reset = inputs.get({reset:?}).unwrap_or(0) != 0;
528        {enable_init}
529        // Apply SyncReadMem pending from previous cycle (latency 1) — matches Sim.
530        let pending = std::mem::take(&mut self.pending_mem_reads);
531        for (name, val) in pending {{
532            self.regs.insert(name, if reset {{ 0 }} else {{ val }});
533        }}
534        let mut next_pending = BTreeMap::new();
535        let mut next_regs: BTreeMap<String, u64> = BTreeMap::new();
536{seq_arms}        for (k, v) in next_regs {{
537            self.regs.insert(k, v);
538        }}
539        self.pending_mem_reads = next_pending;
540        let mut out = inputs.clone();
541{comb_arms}        out
542    }}
543}}
544
545#[cfg(test)]
546mod tests {{
547    use super::*;
548
549    #[test]
550    fn gold_port_values_after_reset_and_three_cycles() {{
551        let mut sim = FunctionalSim::new();
552        let mut pv = PortValues::default();
553        pv.set({reset:?}, 1);
554        let _ = sim.cycle(&pv);
555        pv.set({reset:?}, 0);
556        let mut last = PortValues::default();
557        for _ in 0..3 {{
558            last = sim.cycle(&pv);
559        }}
560        // Counter-style gold: data_out == 3 when HIR has count++ / data_out=count.
561        if last.values.contains_key("data_out") {{
562            assert_eq!(last.get("data_out"), Some(3));
563        }}
564    }}
565{mem_gold}}}
566"#
567    )
568}
569
570fn render_expr(expr: &AssignExpr) -> String {
571    match expr {
572        AssignExpr::Ref(n) => format!("self.lookup(inputs, {n:?})"),
573        AssignExpr::Lit(v) => format!("{v}"),
574        AssignExpr::Inc(n) => format!("self.lookup(inputs, {n:?}).wrapping_add(1)"),
575        AssignExpr::Add(a, b) => {
576            format!("self.lookup(inputs, {a:?}).wrapping_add(self.lookup(inputs, {b:?}))")
577        }
578        AssignExpr::Sub(a, b) => {
579            format!("self.lookup(inputs, {a:?}).wrapping_sub(self.lookup(inputs, {b:?}))")
580        }
581        AssignExpr::And(a, b) => format!("self.lookup(inputs, {a:?}) & self.lookup(inputs, {b:?})"),
582        AssignExpr::Or(a, b) => format!("self.lookup(inputs, {a:?}) | self.lookup(inputs, {b:?})"),
583        AssignExpr::Xor(a, b) => format!("self.lookup(inputs, {a:?}) ^ self.lookup(inputs, {b:?})"),
584        AssignExpr::Shl(a, b) => {
585            format!("self.lookup(inputs, {a:?}) << (self.lookup(inputs, {b:?}) & 63)")
586        }
587        AssignExpr::Shr(a, b) => {
588            format!("self.lookup(inputs, {a:?}) >> (self.lookup(inputs, {b:?}) & 63)")
589        }
590        AssignExpr::Eq(a, b) => {
591            format!("u64::from(self.lookup(inputs, {a:?}) == self.lookup(inputs, {b:?}))")
592        }
593        AssignExpr::Mux { sel, t, f } => format!(
594            "if self.lookup(inputs, {sel:?}) != 0 {{ self.lookup(inputs, {t:?}) }} else {{ self.lookup(inputs, {f:?}) }}"
595        ),
596        AssignExpr::MemRead { mem, addr } => {
597            format!("self.eval_mem_read(inputs, {mem:?}, {addr:?})")
598        }
599    }
600}
601
602fn render_expr_ports_regs(expr: &AssignExpr) -> String {
603    match expr {
604        AssignExpr::Ref(n) => {
605            format!("out.get({n:?}).or_else(|| self.regs.get({n:?}).copied()).unwrap_or(0)")
606        }
607        other => render_expr(other).replace("inputs", "&out"),
608    }
609}
610
611/// Also write a tiny `main.rs` so `cargo run` works (prints one cycle).
612pub fn write_functional_main(out_dir: &Path) -> io::Result<()> {
613    let main = r#"fn main() {
614    use bitloom_func_bin_placeholder::FunctionalSim;
615    use bitloom_hir::PortValues;
616    let mut sim = FunctionalSim::new();
617    let mut pv = PortValues::default();
618    pv.set("rst", 0);
619    let out = sim.cycle(&pv);
620    println!("{out:?}");
621}
622"#;
623    // Fix package import: read Cargo.toml name
624    let toml = fs::read_to_string(out_dir.join("Cargo.toml"))?;
625    let name = toml
626        .lines()
627        .find_map(|l| {
628            l.strip_prefix("name = \"")
629                .and_then(|r| r.strip_suffix('"'))
630                .map(|s| s.replace('-', "_"))
631        })
632        .unwrap_or_else(|| "functional_sim".into());
633    let main = main.replace("bitloom_func_bin_placeholder", &name);
634    fs::write(out_dir.join("src/main.rs"), main)
635}
636
637/// Convenience alias (same as [`generate_functional_sim`]).
638pub fn generate_functional_sim_with_bin(hir: &FrozenHir, out_dir: &Path) -> io::Result<PathBuf> {
639    generate_functional_sim(hir, out_dir)
640}
641
642#[cfg(test)]
643mod tests {
644    use bitloom_builder::{ElaborateSession, GroundType, Span};
645
646    use super::*;
647    use crate::{Sim, check_mixed_both};
648
649    fn counter_hir() -> FrozenHir {
650        let mut s = ElaborateSession::new("t");
651        s.begin_module("Counter", Span::default());
652        s.add_input("clk", GroundType::Clock, Span::default());
653        s.add_input("rst", GroundType::Reset, Span::default());
654        s.add_input("data_in", GroundType::UInt { width: 8 }, Span::default());
655        s.add_output("data_out", GroundType::UInt { width: 8 }, Span::default());
656        s.declare_reg("count", GroundType::UInt { width: 8 }, Span::default());
657        s.begin_combinational(Span::default());
658        s.assign_net("data_out", "count", Span::default());
659        s.end_process();
660        s.begin_sequential(Span::default());
661        s.assign_reg_d_inc("count", Span::default());
662        s.end_process();
663        s.end_module();
664        s.finish().unwrap()
665    }
666
667    #[test]
668    fn generated_functional_matches_tick_port_values() {
669        let hir = counter_hir();
670        let mut sim = Sim::new(hir.clone());
671        let mut abs = GeneratedFunctional::from_hir(&hir);
672        let mut pv = PortValues::default();
673        pv.set("rst", 1);
674        check_mixed_both(&mut sim, &mut abs, pv.clone()).unwrap();
675        pv.set("rst", 0);
676        for _ in 0..3 {
677            check_mixed_both(&mut sim, &mut abs, pv.clone()).unwrap();
678        }
679        assert_eq!(sim.ports().get("data_out"), Some(3));
680    }
681
682    fn sync_read_mem_hir() -> FrozenHir {
683        let mut s = ElaborateSession::new("t");
684        s.begin_module("Srm", Span::default());
685        s.add_input("clk", GroundType::Clock, Span::default());
686        s.add_input("rst", GroundType::Reset, Span::default());
687        s.add_input("addr", GroundType::UInt { width: 4 }, Span::default());
688        s.add_input("wdata", GroundType::UInt { width: 8 }, Span::default());
689        s.add_input("we", GroundType::Bool, Span::default());
690        s.add_output("rdata", GroundType::UInt { width: 8 }, Span::default());
691        s.declare_sync_read_mem("ram", 16, 8, Span::default());
692        s.declare_reg("q", GroundType::UInt { width: 8 }, Span::default());
693        s.begin_combinational(Span::default());
694        s.assign_net("rdata", "q", Span::default());
695        s.end_process();
696        s.begin_sequential(Span::default());
697        s.assign_mem_write("ram", "addr", "wdata", Span::default());
698        s.assign_reg_d_mem_read("q", "ram", "addr", Span::default());
699        s.end_process();
700        s.end_module();
701        s.finish().unwrap()
702    }
703
704    #[test]
705    fn generated_functional_sync_read_mem_matches_tick() {
706        let hir = sync_read_mem_hir();
707        let mut sim = Sim::new(hir.clone());
708        let mut abs = GeneratedFunctional::from_hir(&hir);
709        let mut pv = PortValues::default();
710        pv.set("rst", 0);
711        pv.set("addr", 3);
712        pv.set("wdata", 0xAB);
713        pv.set("we", 1);
714        check_mixed_both(&mut sim, &mut abs, pv.clone()).unwrap();
715        assert_eq!(sim.ports().get("rdata"), Some(0));
716        check_mixed_both(&mut sim, &mut abs, pv).unwrap();
717        assert_eq!(sim.ports().get("rdata"), Some(0xAB));
718    }
719
720    #[test]
721    fn emit_writes_crate_with_gold_test() {
722        let hir = counter_hir();
723        let dir = std::env::temp_dir().join(format!("bitloom-func-gen-{}", std::process::id()));
724        let _ = fs::remove_dir_all(&dir);
725        let out = generate_functional_sim_with_bin(&hir, &dir).unwrap();
726        let lib = fs::read_to_string(out.join("src/lib.rs")).unwrap();
727        assert!(lib.contains("FunctionalSim"));
728        assert!(lib.contains("gold_port_values_after_reset_and_three_cycles"));
729        assert!(!lib.to_lowercase().contains("systemc") || lib.contains("Not SystemC"));
730        let cargo = fs::read_to_string(out.join("Cargo.toml")).unwrap();
731        assert!(cargo.contains("bitloom-hir"));
732        assert!(out.join("src/main.rs").is_file());
733    }
734
735    #[test]
736    fn emit_sync_read_mem_not_stubbed_and_cargo_tests() {
737        let hir = sync_read_mem_hir();
738        let dir = std::env::temp_dir().join(format!("bitloom-func-mem-{}", std::process::id()));
739        let _ = fs::remove_dir_all(&dir);
740        let out = generate_functional_sim(&hir, &dir).unwrap();
741        let lib = fs::read_to_string(out.join("src/lib.rs")).unwrap();
742        assert!(
743            lib.contains("eval_mem_read") && lib.contains("pending_mem_reads"),
744            "FR159 emit must include real MemRead / SyncReadMem latency-1"
745        );
746        assert!(
747            lib.contains("gold_sync_read_mem_latency1_write_then_read"),
748            "emitted crate must include SyncReadMem gold"
749        );
750        // Must not be the pre-FR159 constant stub for MemRead targets.
751        assert!(
752            lib.contains("next_pending.insert(\"q\"")
753                || lib.contains("next_pending.insert(\"q\".into()"),
754            "sync MemRead into q must queue pending, not assign 0"
755        );
756        let status = std::process::Command::new("cargo")
757            .arg("+1.97.1")
758            .arg("test")
759            .arg("--manifest-path")
760            .arg(out.join("Cargo.toml"))
761            .arg("--quiet")
762            .status()
763            .expect("spawn cargo test");
764        assert!(
765            status.success(),
766            "emitted SyncReadMem functional crate must cargo test"
767        );
768    }
769}