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)]
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 comb: Vec<(String, AssignExpr)>,
50}
51
52impl GeneratedFunctional {
53 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 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 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 out.set(name.clone(), self.eval(&out, expr));
270 }
271 out
272 }
273}
274
275pub fn emit_functional_crate(hir: &FrozenHir, out_dir: &Path) -> io::Result<PathBuf> {
277 generate_functional_sim(hir, out_dir)
278}
279
280pub 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 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
611pub 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 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
637pub 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 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}