1use std::fs;
4use std::io;
5use std::path::{Path, PathBuf};
6
7use bitloom_hir::{
8 AssignExpr, AssignTarget, FrozenHir, GroundType, PortDirection, ProcessKind, Stmt,
9};
10
11use crate::generate::GeneratedFunctional;
12use crate::{AbstractionView, EquivStatus, PortValues, Sim, check_functional_equiv};
13
14pub struct CycleAccurateSim {
16 sim: Sim,
17}
18
19impl CycleAccurateSim {
20 pub fn from_hir(hir: FrozenHir) -> Self {
21 Self { sim: Sim::new(hir) }
22 }
23
24 pub fn tick_with(&mut self, inputs: PortValues) -> &PortValues {
25 self.sim.set_inputs(inputs);
26 self.sim.tick();
27 self.sim.ports()
28 }
29
30 pub fn ports(&self) -> &PortValues {
31 self.sim.ports()
32 }
33}
34
35pub fn check_generated_bridge(
37 hir: FrozenHir,
38 stimuli: impl IntoIterator<Item = PortValues>,
39) -> EquivStatus {
40 let mut abs = GeneratedFunctional::from_hir(&hir);
41 check_functional_equiv(hir, &mut abs, stimuli)
42}
43
44pub fn check_generated_bridge_with<A: AbstractionView>(
46 hir: FrozenHir,
47 abs: &mut A,
48 stimuli: impl IntoIterator<Item = PortValues>,
49) -> EquivStatus {
50 check_functional_equiv(hir, abs, stimuli)
51}
52
53pub fn emit_cycle_accurate_crate(hir: &FrozenHir, out_dir: &Path) -> io::Result<PathBuf> {
55 generate_cycle_accurate_sim(hir, out_dir)
56}
57
58pub fn generate_cycle_accurate_sim(hir: &FrozenHir, out_dir: &Path) -> io::Result<PathBuf> {
60 fs::create_dir_all(out_dir.join("src"))?;
61 let pkg = sanitize_pkg_name(&hir.abi_name);
62 let cargo = render_cargo_toml(&pkg, out_dir)?;
63 let hir_src =
64 emit_hir_builder(hir).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
65 let lib = render_lib_rs(&pkg, &hir_src);
66 fs::write(out_dir.join("Cargo.toml"), cargo)?;
67 fs::write(out_dir.join("src/lib.rs"), lib)?;
68 let main = format!(
69 r#"fn main() {{
70 use {crate_name}::CycleAccurate;
71 use bitloom_hir::PortValues;
72 let mut sim = CycleAccurate::new();
73 let mut pv = PortValues::default();
74 pv.set("rst", 0);
75 let out = sim.cycle(&pv);
76 println!("{{out:?}}");
77}}
78"#,
79 crate_name = pkg.replace('-', "_"),
80 );
81 fs::write(out_dir.join("src/main.rs"), main)?;
82 Ok(out_dir.to_path_buf())
83}
84
85fn sanitize_pkg_name(abi: &str) -> String {
86 let mut s: String = abi
87 .chars()
88 .map(|c| {
89 if c.is_ascii_alphanumeric() || c == '_' {
90 c.to_ascii_lowercase()
91 } else {
92 '_'
93 }
94 })
95 .collect();
96 if s.is_empty() || s.chars().next().is_some_and(|c| c.is_ascii_digit()) {
97 s = format!("cycle_{s}");
98 }
99 format!("bitloom_cycle_{s}")
100}
101
102fn resolve_dep(crate_name: &str, out_dir: &Path) -> String {
103 let candidates = [
104 out_dir
105 .join(format!("../../crates/{crate_name}"))
106 .canonicalize()
107 .ok(),
108 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
109 .join(format!("../{crate_name}"))
110 .canonicalize()
111 .ok(),
112 ];
113 for c in candidates.into_iter().flatten() {
114 if c.join("Cargo.toml").is_file() {
115 return format!("{crate_name} = {{ path = \"{}\" }}", c.display());
116 }
117 }
118 format!("{crate_name} = \"{}\"", env!("CARGO_PKG_VERSION"))
119}
120
121fn render_cargo_toml(pkg: &str, out_dir: &Path) -> io::Result<String> {
122 let hir = resolve_dep("bitloom-hir", out_dir);
123 let sim = resolve_dep("bitloom-sim", out_dir);
124 let builder = resolve_dep("bitloom-builder", out_dir);
125 Ok(format!(
126 r#"[package]
127name = "{pkg}"
128version = "0.0.0"
129edition = "2024"
130rust-version = "1.97.1"
131publish = false
132description = "Generated Bitloom cycle-accurate tick wrapper (FR47). Not SystemC."
133
134[workspace]
135
136[dependencies]
137{hir}
138{sim}
139{builder}
140
141[[bin]]
142name = "{pkg}"
143path = "src/main.rs"
144"#
145 ))
146}
147
148fn render_ground_type(ty: &GroundType) -> String {
149 match ty {
150 GroundType::UInt { width } => format!("GroundType::UInt {{ width: {width} }}"),
151 GroundType::SInt { width } => format!("GroundType::SInt {{ width: {width} }}"),
152 GroundType::Clock => "GroundType::Clock".into(),
153 GroundType::Reset => "GroundType::Reset".into(),
154 GroundType::Bool => "GroundType::Bool".into(),
155 GroundType::Analog => "GroundType::Analog".into(),
156 }
157}
158
159fn emit_hir_builder(hir: &FrozenHir) -> Result<String, String> {
160 let circuit = hir.circuit();
161 if circuit.modules.len() != 1 {
162 return Err("cycle-accurate emit supports a single top module".into());
163 }
164 let m = &circuit.modules[0];
165 let mut body = String::new();
166 body.push_str(&format!(
167 " let mut s = ElaborateSession::new({:?});\n",
168 circuit.name
169 ));
170 body.push_str(&format!(
171 " s.begin_module({:?}, Span::default());\n",
172 m.name
173 ));
174 for p in &m.ports {
175 let ty = render_ground_type(&p.ty);
176 match p.direction {
177 PortDirection::Input => {
178 body.push_str(&format!(
179 " s.add_input({:?}, {ty}, Span::default());\n",
180 p.name
181 ));
182 }
183 PortDirection::Output => {
184 body.push_str(&format!(
185 " s.add_output({:?}, {ty}, Span::default());\n",
186 p.name
187 ));
188 }
189 PortDirection::InOut => {
190 return Err("InOut ports not supported in cycle-accurate emit".into());
191 }
192 }
193 }
194 for stmt in &m.body {
195 match stmt {
196 Stmt::RegDecl { name, ty, .. } => {
197 body.push_str(&format!(
198 " s.declare_reg({:?}, {}, Span::default());\n",
199 name,
200 render_ground_type(ty)
201 ));
202 }
203 Stmt::WireDecl { name, ty, .. } => {
204 body.push_str(&format!(
205 " s.declare_wire({:?}, {}, Span::default());\n",
206 name,
207 render_ground_type(ty)
208 ));
209 }
210 Stmt::Process(p) => {
211 match p.kind {
212 ProcessKind::Combinational => {
213 body.push_str(" s.begin_combinational(Span::default());\n");
214 }
215 ProcessKind::Sequential => {
216 body.push_str(" s.begin_sequential(Span::default());\n");
217 }
218 }
219 for a in &p.assigns {
220 body.push_str(&emit_assign(a)?);
221 }
222 body.push_str(" s.end_process();\n");
223 }
224 Stmt::Instance(_) | Stmt::MemDecl { .. } => {
225 return Err(
226 "instances/memories not supported in minimal cycle-accurate emit".into(),
227 );
228 }
229 }
230 }
231 body.push_str(" s.end_module();\n");
232 body.push_str(" s.finish().expect(\"rebuild FrozenHir\")\n");
233 Ok(body)
234}
235
236fn emit_assign(a: &bitloom_hir::Assign) -> Result<String, String> {
237 match (&a.target, &a.expr) {
238 (AssignTarget::Net(name), AssignExpr::Ref(from)) => Ok(format!(
239 " s.assign_net({name:?}, {from:?}, Span::default());\n"
240 )),
241 (AssignTarget::Net(name), AssignExpr::Lit(v)) => Ok(format!(
242 " s.assign_lit({name:?}, {v}, Span::default());\n"
243 )),
244 (AssignTarget::RegD(name), AssignExpr::Inc(_)) => Ok(format!(
245 " s.assign_reg_d_inc({name:?}, Span::default());\n"
246 )),
247 (AssignTarget::RegD(name), AssignExpr::Ref(from)) => Ok(format!(
248 " s.assign_reg_d_from({name:?}, {from:?}, Span::default());\n"
249 )),
250 (AssignTarget::Net(name), AssignExpr::Add(a, b)) => Ok(format!(
251 " s.assign_add({name:?}, {a:?}, {b:?}, Span::default());\n"
252 )),
253 (AssignTarget::Net(name), AssignExpr::Sub(a, b)) => Ok(format!(
254 " s.assign_sub({name:?}, {a:?}, {b:?}, Span::default());\n"
255 )),
256 (AssignTarget::Net(name), AssignExpr::And(a, b)) => Ok(format!(
257 " s.assign_and({name:?}, {a:?}, {b:?}, Span::default());\n"
258 )),
259 (AssignTarget::Net(name), AssignExpr::Or(a, b)) => Ok(format!(
260 " s.assign_or({name:?}, {a:?}, {b:?}, Span::default());\n"
261 )),
262 (AssignTarget::Net(name), AssignExpr::Xor(a, b)) => Ok(format!(
263 " s.assign_xor({name:?}, {a:?}, {b:?}, Span::default());\n"
264 )),
265 (AssignTarget::Net(name), AssignExpr::Eq(a, b)) => Ok(format!(
266 " s.assign_eq({name:?}, {a:?}, {b:?}, Span::default());\n"
267 )),
268 (AssignTarget::Net(name), AssignExpr::Mux { sel, t, f }) => Ok(format!(
269 " s.assign_mux({name:?}, {sel:?}, {t:?}, {f:?}, Span::default());\n"
270 )),
271 _ => Err(format!(
272 "unsupported assign in cycle-accurate emit: {:?} <- {:?}",
273 a.target, a.expr
274 )),
275 }
276}
277
278fn render_lib_rs(pkg: &str, hir_body: &str) -> String {
279 let _ = pkg;
280 format!(
281 r#"//! Generated Bitloom cycle-accurate simulator (FR47 / AD-5).
282//! FrozenHir → `bitloom_sim::Sim::tick` wrapper. Not SystemC / TLM-2.0.
283
284use bitloom_builder::{{ElaborateSession, GroundType, Span}};
285use bitloom_hir::{{FrozenHir, PortValues}};
286use bitloom_sim::Sim;
287
288fn frozen_hir() -> FrozenHir {{
289{hir_body}}}
290
291/// Cycle-accurate tick wrapper over reconstructed FrozenHir.
292pub struct CycleAccurate {{
293 sim: Sim,
294}}
295
296impl Default for CycleAccurate {{
297 fn default() -> Self {{
298 Self::new()
299 }}
300}}
301
302impl CycleAccurate {{
303 pub fn new() -> Self {{
304 Self {{
305 sim: Sim::new(frozen_hir()),
306 }}
307 }}
308
309 pub fn cycle(&mut self, inputs: &PortValues) -> PortValues {{
310 self.sim.set_inputs(inputs.clone());
311 self.sim.tick();
312 self.sim.ports().clone()
313 }}
314}}
315
316#[cfg(test)]
317mod tests {{
318 use super::*;
319 use bitloom_sim::{{GeneratedFunctional, check_generated_bridge, reset_then_run}};
320
321 #[test]
322 fn bridge_matches_functional_gold() {{
323 let hir = frozen_hir();
324 let status = check_generated_bridge(hir, reset_then_run(3));
325 assert!(status.is_pass(), "{{status:?}}");
326 }}
327
328 #[test]
329 fn tick_port_values_gold() {{
330 let mut ca = CycleAccurate::new();
331 let mut pv = PortValues::default();
332 pv.set("rst", 1);
333 let _ = ca.cycle(&pv);
334 pv.set("rst", 0);
335 let mut last = PortValues::default();
336 for _ in 0..3 {{
337 last = ca.cycle(&pv);
338 }}
339 if last.values.contains_key("data_out") {{
340 assert_eq!(last.get("data_out"), Some(3));
341 }}
342 }}
343
344 #[test]
345 fn deliberate_mismatch_fails() {{
346 struct Wrong;
347 impl bitloom_sim::AbstractionView for Wrong {{
348 fn cycle(&mut self, inputs: &PortValues) -> PortValues {{
349 let mut o = inputs.clone();
350 o.set("data_out", 99);
351 o
352 }}
353 }}
354 let hir = frozen_hir();
355 let mut w = Wrong;
356 let status = bitloom_sim::check_generated_bridge_with(hir, &mut w, reset_then_run(1));
357 assert!(!status.is_pass());
358 }}
359
360 #[test]
361 fn functional_in_process_still_aligned() {{
362 use bitloom_sim::AbstractionView;
363 let hir = frozen_hir();
364 let mut abs = GeneratedFunctional::from_hir(&hir);
365 let mut ca = CycleAccurate::new();
366 let mut pv = PortValues::default();
367 pv.set("rst", 0);
368 let f = abs.cycle(&pv);
369 let c = ca.cycle(&pv);
370 assert_eq!(f.get("data_out"), c.get("data_out"));
371 }}
372}}
373"#
374 )
375}
376
377#[cfg(test)]
378mod tests {
379 use bitloom_builder::{ElaborateSession, GroundType, Span};
380
381 use super::*;
382 use crate::reset_then_run;
383
384 fn counter_hir() -> FrozenHir {
385 let mut s = ElaborateSession::new("t");
386 s.begin_module("Counter", Span::default());
387 s.add_input("clk", GroundType::Clock, Span::default());
388 s.add_input("rst", GroundType::Reset, Span::default());
389 s.add_input("data_in", GroundType::UInt { width: 8 }, Span::default());
390 s.add_output("data_out", GroundType::UInt { width: 8 }, Span::default());
391 s.declare_reg("count", GroundType::UInt { width: 8 }, Span::default());
392 s.begin_combinational(Span::default());
393 s.assign_net("data_out", "count", Span::default());
394 s.end_process();
395 s.begin_sequential(Span::default());
396 s.assign_reg_d_inc("count", Span::default());
397 s.end_process();
398 s.end_module();
399 s.finish().unwrap()
400 }
401
402 #[test]
403 fn bridge_pass_and_mismatch_fail() {
404 let hir = counter_hir();
405 assert!(check_generated_bridge(hir.clone(), reset_then_run(3)).is_pass());
406
407 struct Wrong;
408 impl AbstractionView for Wrong {
409 fn cycle(&mut self, inputs: &PortValues) -> PortValues {
410 let mut o = inputs.clone();
411 o.set("data_out", 7);
412 o
413 }
414 }
415 let mut w = Wrong;
416 assert!(!check_generated_bridge_with(hir, &mut w, reset_then_run(1)).is_pass());
417 }
418
419 #[test]
420 fn emit_cycle_crate_compiles() {
421 let hir = counter_hir();
422 let dir = std::env::temp_dir().join(format!("bitloom-cycle-gen-{}", std::process::id()));
423 let _ = fs::remove_dir_all(&dir);
424 let out = generate_cycle_accurate_sim(&hir, &dir).unwrap();
425 let lib = fs::read_to_string(out.join("src/lib.rs")).unwrap();
426 assert!(
427 lib.contains("CycleAccurate") && lib.contains("Sim::tick") || lib.contains("sim.tick")
428 );
429 assert!(lib.contains("check_generated_bridge"));
430 }
431}