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