Skip to main content

bitloom_sim/
cycle.rs

1//! FR47 leg 2: cycle-accurate tick-wrapper artifact + bridge/compare.
2
3use 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
14/// Thin FrozenHir → `Sim::tick` wrapper (cycle-accurate product surface).
15pub 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
35/// Bridge/compare: generated functional model vs cycle-accurate `tick` (FR47 leg 2).
36pub 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
44/// Same bridge entry with an arbitrary functional view (for deliberate-mismatch tests).
45pub 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
53/// Alias for product naming symmetry with `emit_functional_crate`.
54pub fn emit_cycle_accurate_crate(hir: &FrozenHir, out_dir: &Path) -> io::Result<PathBuf> {
55    generate_cycle_accurate_sim(hir, out_dir)
56}
57
58/// Write a cycle-accurate tick-wrapper crate (FrozenHir rebuild + `Sim::tick`).
59pub 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}