Skip to main content

veryl_simulator/
simulator.rs

1use crate::backend::CompiledWhole;
2use crate::ir::write_log::{
3    WriteLogBuffer, clear_event_write_log, ff_commit_from_log, set_event_write_log,
4};
5use crate::ir::{
6    Event, Ir, ModuleVariables, Statement, Value, VarId, VarPath, dispatch_stmt_fast,
7    read_native_value, write_native_value,
8};
9use crate::wave_dumper::{DumpVar, WaveDumper};
10use std::str::FromStr;
11use veryl_analyzer::value::MaskCache;
12
13#[cfg(feature = "profile")]
14#[derive(Default, Debug)]
15pub struct SimProfile {
16    pub step_count: u64,
17    pub settle_comb_count: u64,
18    pub comb_eval_count: u64,
19    pub extra_pass_count: u64,
20    pub converged_first_try: u64,
21    pub settle_comb_ns: u64,
22    pub event_eval_ns: u64,
23    pub ff_swap_ns: u64,
24    pub eval_comb_full_ns: u64,
25}
26
27#[cfg(not(feature = "profile"))]
28#[derive(Default, Debug)]
29pub struct SimProfile;
30
31pub struct Simulator {
32    pub ir: Ir,
33    pub time: u64,
34    pub dump: Option<WaveDumper>,
35    dump_vars: Vec<DumpVar>,
36    pub mask_cache: MaskCache,
37    comb_dirty: bool,
38    pub profile: SimProfile,
39    last_event: Option<Event>,
40    last_event_stmts: *const Vec<Statement>,
41    /// Env-gated `VERYL_WRITE_LOG_DIAG=1` diagnostics for the write-log
42    /// commit path.  Accumulated across the run; `dump` is invoked
43    /// automatically when the cycle counter crosses a logarithmic
44    /// checkpoint (doubling cadence, capped at 1 M cycles).
45    pub write_log_diag: WriteLogDiag,
46}
47
48#[derive(Default)]
49pub struct WriteLogDiag {
50    pub enabled: bool,
51    pub total_cycles: u64,
52    pub total_entries: u64,
53    pub max_entries_per_cycle: u32,
54    pub cycles_with_entries: u64,
55    next_print_cycle: u64,
56}
57
58impl WriteLogDiag {
59    fn maybe_print(&mut self) {
60        if !self.enabled {
61            return;
62        }
63        if self.total_cycles >= self.next_print_cycle {
64            self.next_print_cycle = self.next_print_cycle.saturating_mul(2).max(1_000_000);
65            self.dump();
66        }
67    }
68
69    pub fn dump(&self) {
70        let avg = if self.cycles_with_entries > 0 {
71            self.total_entries as f64 / self.cycles_with_entries as f64
72        } else {
73            0.0
74        };
75        eprintln!(
76            "[write_log_diag] cycles={} cycles_with_entries={} total_entries={} max_per_cycle={} avg_per_active_cycle={:.2}",
77            self.total_cycles,
78            self.cycles_with_entries,
79            self.total_entries,
80            self.max_entries_per_cycle,
81            avg,
82        );
83    }
84}
85
86impl Simulator {
87    pub fn new(ir: Ir, dump: Option<WaveDumper>) -> Self {
88        let mut ret = Self {
89            ir,
90            time: 0,
91            dump: None,
92            dump_vars: Vec::new(),
93            mask_cache: MaskCache::default(),
94            comb_dirty: true,
95            profile: Default::default(),
96            last_event: None,
97            last_event_stmts: std::ptr::null(),
98            write_log_diag: WriteLogDiag {
99                enabled: std::env::var("VERYL_WRITE_LOG_DIAG").as_deref() == Ok("1"),
100                next_print_cycle: 1_000_000,
101                ..Default::default()
102            },
103        };
104
105        if let Some(dumper) = dump {
106            ret.setup_dump(dumper);
107        }
108
109        ret
110    }
111
112    fn do_settle_comb(&mut self) {
113        self.ir.settle_comb(&mut self.mask_cache, &mut self.profile);
114    }
115
116    pub fn set(&mut self, port: &str, value: Value) {
117        let port = VarPath::from_str(port).unwrap();
118
119        if let Some(id) = self.ir.ports.get(&port)
120            && let Some(x) = self.ir.module_variables.variables.get_mut(id)
121        {
122            let mut value = value;
123            value.trunc(x.width);
124            unsafe {
125                write_native_value(
126                    x.current_values[0],
127                    x.native_bytes,
128                    self.ir.use_4state,
129                    &value,
130                );
131            }
132            self.comb_dirty = true;
133        }
134    }
135
136    pub fn get(&mut self, port: &str) -> Option<Value> {
137        self.ensure_comb_updated();
138
139        let port = VarPath::from_str(port).unwrap();
140
141        if let Some(id) = self.ir.ports.get(&port)
142            && let Some(x) = self.ir.module_variables.variables.get(id)
143        {
144            let value = unsafe {
145                read_native_value(
146                    x.current_values[0],
147                    x.native_bytes,
148                    self.ir.use_4state,
149                    x.width as u32,
150                    false,
151                )
152            };
153            Some(value)
154        } else {
155            None
156        }
157    }
158
159    /// Get a variable value by hierarchical path (e.g., "dut.cnt").
160    /// Searches all module variables including children.
161    pub fn get_var(&mut self, path: &str) -> Option<Value> {
162        self.ensure_comb_updated();
163
164        let target = VarPath::from_str(path).unwrap();
165        Self::find_var_in_module(&self.ir.module_variables, &target, self.ir.use_4state)
166    }
167
168    fn find_var_in_module(
169        module: &ModuleVariables,
170        target: &VarPath,
171        use_4state: bool,
172    ) -> Option<Value> {
173        // If target has multiple segments, try matching child module by name first
174        if target.0.len() > 1 {
175            for child in &module.children {
176                if child.name == target.0[0] {
177                    let sub = VarPath::from_slice(&target.0[1..]);
178                    if let Some(v) = Self::find_var_in_module(child, &sub, use_4state) {
179                        return Some(v);
180                    }
181                }
182            }
183        }
184
185        // Look for a variable whose path matches exactly
186        for var in module.variables.values() {
187            if var.path == *target {
188                let value = unsafe {
189                    read_native_value(
190                        var.current_values[0],
191                        var.native_bytes,
192                        use_4state,
193                        var.width as u32,
194                        false,
195                    )
196                };
197                return Some(value);
198            }
199        }
200        None
201    }
202
203    pub fn ensure_comb_updated(&mut self) {
204        if self.comb_dirty {
205            #[cfg(feature = "profile")]
206            let start = std::time::Instant::now();
207
208            self.do_settle_comb();
209            self.comb_dirty = false;
210
211            #[cfg(feature = "profile")]
212            {
213                self.profile.settle_comb_ns += start.elapsed().as_nanos() as u64;
214            }
215        }
216    }
217
218    pub fn mark_comb_dirty(&mut self) {
219        self.comb_dirty = true;
220    }
221
222    pub fn get_clock(&self, port: &str) -> Option<Event> {
223        let port = VarPath::from_str(port).unwrap();
224        self.ir.ports.get(&port).map(|id| Event::Clock(*id))
225    }
226
227    pub fn get_reset(&self, port: &str) -> Option<Event> {
228        let port = VarPath::from_str(port).unwrap();
229        self.ir.ports.get(&port).map(|id| Event::Reset(*id))
230    }
231
232    pub fn step(&mut self, event: &Event) {
233        #[cfg(feature = "profile")]
234        {
235            self.profile.step_count += 1;
236        }
237
238        // Install the per-Ir WriteLogBuffer before settle_comb so that
239        // comb-scope FF writes (which appear under `--disable-ff-opt`'s
240        // force_all_ff path and never go through the event scope) also
241        // emit log entries and get committed alongside event-scope writes
242        // at cycle end.  Without this install, settle_comb's FF stores
243        // hit `event_write_log_push_static`'s "no active log" branch and
244        // turn into no-ops, leaving the FF current slot stale.
245        // SAFETY: the buffer outlives the call to dispatch_stmt_fast and
246        // is cleared before this stack frame returns.
247        unsafe {
248            set_event_write_log(&mut self.ir.write_log_buffer);
249        }
250
251        if self.comb_dirty {
252            #[cfg(feature = "profile")]
253            let start = std::time::Instant::now();
254
255            self.do_settle_comb();
256            self.comb_dirty = false;
257
258            #[cfg(feature = "profile")]
259            {
260                self.profile.settle_comb_ns += start.elapsed().as_nanos() as u64;
261            }
262        }
263
264        #[cfg(feature = "profile")]
265        let event_start = std::time::Instant::now();
266
267        let stmts_ptr = if self.last_event.as_ref() == Some(event) {
268            self.last_event_stmts
269        } else {
270            let ptr: *const Vec<Statement> = match self.ir.event_statements.get(event) {
271                Some(v) => v as *const _,
272                None => std::ptr::null(),
273            };
274            self.last_event = Some(event.clone());
275            self.last_event_stmts = ptr;
276            ptr
277        };
278
279        // Whole-event backend (today: AOT-C): if a backend committed to
280        // a one-function compile for this event, invoke it in place of
281        // the per-stmt Cranelift dispatch.  The function reads ff/comb
282        // current values and pushes WriteLogEntries into the buffer
283        // (3rd arg), exactly as the Cranelift event JIT does;
284        // `ff_commit_from_log` below applies them.
285        use crate::backend::DispatchOutcome;
286        let whole_event = self.ir.whole_events.get(event).cloned();
287        let dispatched = if let Some(whole) = whole_event {
288            let ff_ptr = self.ir.ff_values.as_ptr();
289            let comb_ptr = self.ir.comb_values.as_ptr() as *mut u8;
290            let log_ptr = (&*self.ir.write_log_buffer) as *const _ as *mut u8;
291
292            // VERYL_AOT_C_VALIDATE=1: dual-run paths and diff.  Default-off.
293            let validate = self.ir.aot_c_validate;
294
295            if !validate {
296                matches!(
297                    whole.try_dispatch(ff_ptr, comb_ptr, log_ptr),
298                    DispatchOutcome::Done,
299                )
300            } else {
301                // For validate, the wrapper compares the whole-event
302                // dispatch against the per-stmt Cranelift path and panics
303                // on divergence.  The whole-event backend only exists on
304                // native (BackendRegistry stays empty on wasm), so this
305                // branch is effectively native-only at runtime.
306                self.validate_event_aot(whole.as_ref(), stmts_ptr);
307                true
308            }
309        } else {
310            false
311        };
312
313        if !dispatched && !stmts_ptr.is_null() {
314            // SAFETY: event_statements is never mutated after Ir construction.
315            let statements: &Vec<Statement> = unsafe { &*stmts_ptr };
316            for x in statements {
317                dispatch_stmt_fast(x, &mut self.mask_cache);
318            }
319        }
320
321        #[cfg(feature = "profile")]
322        {
323            self.profile.event_eval_ns += event_start.elapsed().as_nanos() as u64;
324        }
325
326        #[cfg(feature = "profile")]
327        let ff_start = std::time::Instant::now();
328
329        ff_commit_from_log(&mut self.ir.ff_values, &self.ir.write_log_buffer);
330
331        clear_event_write_log();
332        if self.write_log_diag.enabled {
333            let n = self.ir.write_log_buffer.count();
334            self.write_log_diag.total_cycles += 1;
335            if n > 0 {
336                self.write_log_diag.total_entries += n as u64;
337                self.write_log_diag.cycles_with_entries += 1;
338                if n > self.write_log_diag.max_entries_per_cycle {
339                    self.write_log_diag.max_entries_per_cycle = n;
340                }
341            }
342            self.write_log_diag.maybe_print();
343        }
344        self.ir.write_log_buffer.reset();
345
346        #[cfg(feature = "profile")]
347        {
348            self.profile.ff_swap_ns += ff_start.elapsed().as_nanos() as u64;
349        }
350
351        self.comb_dirty = true;
352
353        self.dump_variables();
354    }
355
356    /// VERYL_AOT_C_VALIDATE event-path check: run the AOT-C event function and
357    /// the Cranelift per-stmt dispatch on identical inputs, compare the
358    /// WriteLogEntries they push plus any direct ff/comb writes, and panic on
359    /// first divergence.  Leaves the Cranelift result live (ground truth).
360    /// Slow (clones ff/comb each event) — diagnostics only.  Unreachable on
361    /// wasm since no whole-event backend ever registers there.
362    fn validate_event_aot(&mut self, whole: &dyn CompiledWhole, stmts_ptr: *const Vec<Statement>) {
363        let ff_ptr = self.ir.ff_values.as_ptr();
364        let comb_ptr = self.ir.comb_values.as_ptr() as *mut u8;
365        let log_ptr = (&*self.ir.write_log_buffer) as *const _ as *mut u8;
366
367        let ff_snap = self.ir.ff_values.to_vec();
368        let comb_snap = self.ir.comb_values.to_vec();
369        let count_before = self.ir.write_log_buffer.narrow_count as usize;
370
371        // Whole-event backend, then capture its pushed entries + ff/comb.
372        let _ = whole.try_dispatch(ff_ptr, comb_ptr, log_ptr);
373        // The committed FF effect is `ff_commit_from_log`'s last-write-wins per
374        // offset, so compare offset -> (width_class, last payload) maps, not the
375        // raw entry order or the pre-commit ff_values (the dual-slot "next slot"
376        // direct writes are vestigial — ff_commit applies the *log* to the
377        // current slots, so those transient writes don't affect correctness).
378        let lww_map = |buf: &WriteLogBuffer, lo: usize, hi: usize| {
379            let mut m: std::collections::HashMap<u32, (u16, u64)> = Default::default();
380            for e in &buf.narrow_entries_slice()[lo..hi] {
381                m.insert(e.offset, (e.width_class, e.payload));
382            }
383            m
384        };
385        let aot_count = self.ir.write_log_buffer.narrow_count as usize;
386        let aot_map = lww_map(&self.ir.write_log_buffer, count_before, aot_count);
387
388        // Restore inputs + log count, then run the Cranelift event.
389        unsafe {
390            std::ptr::copy_nonoverlapping(
391                ff_snap.as_ptr(),
392                self.ir.ff_values.as_ptr() as *mut u8,
393                ff_snap.len(),
394            );
395            std::ptr::copy_nonoverlapping(
396                comb_snap.as_ptr(),
397                self.ir.comb_values.as_ptr() as *mut u8,
398                comb_snap.len(),
399            );
400        }
401        self.ir.write_log_buffer.narrow_count = count_before as u32;
402        if !stmts_ptr.is_null() {
403            let statements: &Vec<Statement> = unsafe { &*stmts_ptr };
404            for x in statements {
405                dispatch_stmt_fast(x, &mut self.mask_cache);
406            }
407        }
408        let cr_count = self.ir.write_log_buffer.narrow_count as usize;
409        let cr_map = lww_map(&self.ir.write_log_buffer, count_before, cr_count);
410
411        if aot_map != cr_map {
412            eprintln!(
413                "[aot_event_validate] DIVERGENCE event={:?}: committed-FF maps differ (aot {} offsets, cranelift {} offsets)",
414                self.last_event,
415                aot_map.len(),
416                cr_map.len(),
417            );
418            // Offsets present in only one side, or with differing value.
419            for (off, av) in &aot_map {
420                match cr_map.get(off) {
421                    None => eprintln!("  off={off:#x}: aot={av:?} cranelift=<absent>"),
422                    Some(cv) if cv != av => {
423                        eprintln!("  off={off:#x}: aot={av:?} cranelift={cv:?}")
424                    }
425                    _ => {}
426                }
427            }
428            for off in cr_map.keys() {
429                if !aot_map.contains_key(off) {
430                    eprintln!("  off={off:#x}: aot=<absent> cranelift={:?}", cr_map[off]);
431                }
432            }
433            panic!("AOT-C event validate divergence (see above)");
434        }
435    }
436
437    /// Set a variable value by VarId. Used to write clock/reset signal values
438    /// into the variable storage so they appear in wave dumps.
439    pub fn set_var_by_id(&mut self, var_id: &VarId, val: Value) {
440        if let Some(x) = self.ir.module_variables.variables.get_mut(var_id) {
441            let mut val = val;
442            val.trunc(x.width);
443            unsafe {
444                write_native_value(
445                    x.current_values[0],
446                    x.native_bytes,
447                    self.ir.use_4state,
448                    &val,
449                );
450            }
451            self.comb_dirty = true;
452        }
453    }
454
455    pub fn dump_start(&mut self) {
456        if let Some(dump) = &mut self.dump {
457            dump.begin_dumpvars();
458            dump.dump_all_vars(&self.dump_vars, self.ir.use_4state);
459            dump.end_dumpvars();
460        }
461    }
462
463    pub fn dump_variables(&mut self) {
464        if self.dump.is_some() {
465            if self.comb_dirty {
466                self.do_settle_comb();
467                self.comb_dirty = false;
468            }
469            let dump = self.dump.as_mut().unwrap();
470            dump.timestamp(self.time);
471            dump.dump_all_vars(&self.dump_vars, self.ir.use_4state);
472        }
473    }
474
475    fn setup_dump(&mut self, mut dumper: WaveDumper) {
476        dumper.timescale();
477        dumper.setup_module(&self.ir.module_variables, &mut self.dump_vars);
478        dumper.finish_header();
479        self.dump = Some(dumper);
480    }
481}