1use 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#[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#[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 comb: Vec<(String, AssignExpr)>,
47}
48
49impl GeneratedFunctional {
50 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 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 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 out.set(name.clone(), self.eval(&out, expr));
267 }
268 out
269 }
270}
271
272pub fn emit_functional_crate(hir: &FrozenHir, out_dir: &Path) -> io::Result<PathBuf> {
274 generate_functional_sim(hir, out_dir)
275}
276
277pub 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 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
509pub 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 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
535pub 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}