Skip to main content

celox/
simulator.rs

1mod builder;
2mod error;
3
4#[cfg(all(
5    feature = "host-runtime",
6    any(
7        target_arch = "x86_64",
8        feature = "arm64-codegen",
9        target_arch = "aarch64"
10    )
11))]
12pub use builder::NativeCompilation;
13#[cfg(feature = "host-runtime")]
14pub use builder::{DeadStorePolicy, SimulatorBuilder, SimulatorOptions, TierPromotion};
15pub use builder::{compile_frontend_to_sir, compile_to_sir};
16#[cfg(feature = "systemverilog")]
17pub use builder::{compile_mixed_to_sir, compile_sv_to_sir};
18pub use error::render_diagnostic;
19pub use error::{CodegenError, CompilationWarning, SimulatorError, SimulatorErrorKind};
20
21#[cfg(feature = "host-runtime")]
22mod host {
23    use super::*;
24    use std::sync::Arc;
25    use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
26
27    use crate::backend::RuntimeEventBuffer;
28    #[cfg(any(
29        target_arch = "x86_64",
30        feature = "arm64-codegen",
31        target_arch = "aarch64"
32    ))]
33    use crate::backend::native::{NativeBackend, SharedNativeCode};
34    use crate::{
35        IOContext, RuntimeErrorCode,
36        backend::{JitBackend, MemoryLayout, SharedJitCode, SimBackend},
37        ir::{
38            InitialMemoryData, InitialMemoryWriteRun, InstancePath, RuntimeEventKind,
39            RuntimeEventSite, RuntimeProgram, SignalRef, VariableInfo,
40        },
41    };
42    use celox_testbench::{DisplayFormatArg, format_display_arg};
43    use num_bigint::BigUint;
44
45    /// Hierarchical instance tree with resolved signals.
46    #[derive(Debug, Clone)]
47    pub struct InstanceHierarchy {
48        pub module_name: String,
49        pub signals: Vec<NamedSignal>,
50        pub children: Vec<(String, Vec<InstanceHierarchy>)>,
51    }
52
53    /// A named signal with its resolved memory reference and metadata.
54    #[derive(Debug, Clone)]
55    pub struct NamedSignal {
56        pub name: String,
57        pub signal: SignalRef,
58        pub info: VariableInfo,
59        /// For reset signals, the name of the associated clock (from FfDeclaration).
60        pub associated_clock: Option<String>,
61    }
62
63    /// A named event with its resolved ID and event reference.
64    #[derive(Debug, Clone)]
65    pub struct NamedEvent<B: SimBackend = crate::DefaultBackend> {
66        pub name: String,
67        pub id: usize,
68        pub event_ref: B::Event,
69    }
70
71    /// The core logic evaluation engine.
72    ///
73    /// Encapsulates the backend, the original SIR program,
74    /// and an optional VCD writer. Provides low-level, event-driven control.
75    ///
76    /// The default type parameter `B = DefaultBackend` means that bare `Simulator`
77    /// uses the custom native backend on a matching host and Cranelift for
78    /// cross-codegen builds or unsupported hosts.
79    pub struct Simulator<B: SimBackend = crate::DefaultBackend> {
80        pub(crate) backend: B,
81        pub(crate) program: RuntimeProgram,
82        pub(crate) vcd_writer: Option<crate::VcdWriter>,
83        pub(crate) dirty: bool,
84        pub(crate) warnings: Vec<CompilationWarning>,
85        pub(crate) components: crate::component::ComponentRuntime,
86        pub(crate) component_simulation: Option<celox_runtime::SimulationState<B>>,
87        runtime_event_read_seq: Arc<AtomicU64>,
88        runtime_event_drain_active: Arc<AtomicBool>,
89        comb_observer_snapshots: Vec<Vec<(BigUint, BigUint)>>,
90        comb_observer_initial_eval: bool,
91        pub(crate) diagnostics: crate::RuntimeDiagnostics,
92        tick_timing_ticks: u64,
93        tick_timing_eval_apply_ns: u64,
94        tick_timing_eval_comb_ns: u64,
95    }
96
97    #[derive(Debug, Clone, PartialEq, Eq)]
98    pub enum RuntimeEvent {
99        Display { message: String },
100        Write { message: String },
101        AssertContinue { message: String },
102        AssertFatal { message: String },
103        Missed { count: u64 },
104    }
105
106    #[derive(Debug, Clone, Copy, Default)]
107    pub struct RuntimeFormatContext<'a> {
108        pub tb_time: Option<u64>,
109        pub scope: Option<&'a str>,
110    }
111
112    pub struct RuntimeEventDrain {
113        buffer: Arc<RuntimeEventBuffer>,
114        layout: MemoryLayout,
115        sites: Vec<RuntimeEventSite>,
116        read_seq: u64,
117        shared_read_seq: Arc<AtomicU64>,
118        active: Arc<AtomicBool>,
119    }
120
121    impl RuntimeEventDrain {
122        pub fn drain(&mut self) -> Vec<RuntimeEvent> {
123            self.drain_with_context(RuntimeFormatContext::default())
124        }
125
126        pub fn drain_with_context(&mut self, ctx: RuntimeFormatContext<'_>) -> Vec<RuntimeEvent> {
127            let events = drain_raw_runtime_events_from_buffer(
128                &self.buffer,
129                &self.layout,
130                &self.sites,
131                &mut self.read_seq,
132            );
133            self.shared_read_seq.store(self.read_seq, Ordering::Release);
134            events
135                .into_iter()
136                .filter_map(|raw| render_raw_runtime_event(raw, &self.sites, ctx))
137                .collect()
138        }
139    }
140
141    impl Drop for RuntimeEventDrain {
142        fn drop(&mut self) {
143            self.shared_read_seq.store(self.read_seq, Ordering::Release);
144            self.active.store(false, Ordering::Release);
145        }
146    }
147
148    impl<B: SimBackend> std::fmt::Debug for Simulator<B> {
149        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150            f.debug_struct("Simulator").finish()
151        }
152    }
153
154    #[derive(Clone)]
155    struct RuntimeEventArgValue {
156        values: Vec<u64>,
157        masks: Vec<u64>,
158        width: usize,
159        signed: bool,
160        is_string: bool,
161    }
162
163    #[derive(Clone)]
164    enum RawRuntimeEvent {
165        Event {
166            site_id: usize,
167            args: Vec<RuntimeEventArgValue>,
168        },
169        Missed {
170            count: u64,
171        },
172    }
173
174    fn runtime_event_words_to_biguint(words: &[u64], width: usize) -> BigUint {
175        let mut value = BigUint::from(0u8);
176        for (idx, word) in words.iter().enumerate() {
177            value |= BigUint::from(*word) << (idx * 64);
178        }
179        if width > 0 {
180            value & ((BigUint::from(1u8) << width) - BigUint::from(1u8))
181        } else {
182            BigUint::from(0u8)
183        }
184    }
185
186    fn mask_width(value: BigUint, width: usize) -> BigUint {
187        if width == 0 {
188            BigUint::from(0u8)
189        } else {
190            value & ((BigUint::from(1u8) << width) - BigUint::from(1u8))
191        }
192    }
193
194    fn slice_biguint(value: &BigUint, lsb: usize, msb: usize) -> BigUint {
195        if msb < lsb {
196            return BigUint::from(0u8);
197        }
198        mask_width(value >> lsb, msb - lsb + 1)
199    }
200
201    fn write_bits_to_memory(mem: &mut [u8], dst_bit_offset: usize, bit_width: usize, src: &[u8]) {
202        write_bits_to_memory_from(mem, dst_bit_offset, bit_width, src, 0);
203    }
204
205    fn write_bits_to_memory_from(
206        mem: &mut [u8],
207        dst_bit_offset: usize,
208        bit_width: usize,
209        src: &[u8],
210        src_bit_offset: usize,
211    ) {
212        for bit in 0..bit_width {
213            let src_bit_index = src_bit_offset + bit;
214            let src_bit = (src[src_bit_index / 8] >> (src_bit_index % 8)) & 1;
215            let dst_idx = (dst_bit_offset + bit) / 8;
216            let dst_mask = 1u8 << ((dst_bit_offset + bit) % 8);
217            if src_bit == 0 {
218                mem[dst_idx] &= !dst_mask;
219            } else {
220                mem[dst_idx] |= dst_mask;
221            }
222        }
223    }
224
225    fn write_initial_run_to_plane(
226        mem: &mut [u8],
227        signal: SignalRef,
228        mask_plane: bool,
229        run: &InitialMemoryWriteRun,
230        src: &[u8],
231    ) {
232        let Some(array) = signal.array_layout else {
233            let plane_size = signal.width.div_ceil(8);
234            let plane_bit_offset =
235                (signal.offset + usize::from(mask_plane) * plane_size) * 8 + run.bit_offset;
236            write_bits_to_memory(mem, plane_bit_offset, run.bit_width, src);
237            return;
238        };
239
240        let plane_offset = signal.offset + usize::from(mask_plane) * array.plane_size;
241        let mut consumed = 0usize;
242        while consumed < run.bit_width {
243            let logical_offset = run.bit_offset + consumed;
244            let element = logical_offset / array.element_width;
245            let intra_element = logical_offset % array.element_width;
246            let part_width = (run.bit_width - consumed).min(array.element_width - intra_element);
247            let destination_bit_offset =
248                (plane_offset + element * array.element_stride) * 8 + intra_element;
249
250            if consumed.is_multiple_of(8)
251                && destination_bit_offset.is_multiple_of(8)
252                && part_width.is_multiple_of(8)
253            {
254                let src_byte = consumed / 8;
255                let dst_byte = destination_bit_offset / 8;
256                let byte_width = part_width / 8;
257                mem[dst_byte..dst_byte + byte_width]
258                    .copy_from_slice(&src[src_byte..src_byte + byte_width]);
259            } else {
260                write_bits_to_memory_from(mem, destination_bit_offset, part_width, src, consumed);
261            }
262            consumed += part_width;
263        }
264    }
265
266    fn runtime_event_format_arg(arg: &RuntimeEventArgValue, spec: Option<char>) -> String {
267        let value = runtime_event_words_to_biguint(&arg.values, arg.width);
268        let mask = runtime_event_words_to_biguint(&arg.masks, arg.width);
269        format_display_arg(
270            &DisplayFormatArg {
271                value: &value,
272                mask: Some(&mask),
273                width: arg.width,
274                signed: arg.signed,
275                is_string: arg.is_string,
276            },
277            spec,
278        )
279    }
280
281    fn render_runtime_event_message(
282        site: &RuntimeEventSite,
283        args: &[RuntimeEventArgValue],
284        ctx: RuntimeFormatContext<'_>,
285    ) -> String {
286        let Some(template) = site.template.as_deref() else {
287            let default_spec = match site.kind {
288                RuntimeEventKind::Display | RuntimeEventKind::Write => 'd',
289                RuntimeEventKind::AssertContinue | RuntimeEventKind::AssertFatal => {
290                    if args.is_empty() {
291                        return "assertion failed".to_string();
292                    }
293                    'x'
294                }
295            };
296            return args
297                .iter()
298                .map(|arg| runtime_event_format_arg(arg, Some(default_spec)))
299                .collect::<Vec<_>>()
300                .join(" ");
301        };
302        let mut out = String::new();
303        let mut arg_idx = 0usize;
304        let mut chars = template.chars().peekable();
305        while let Some(ch) = chars.next() {
306            if ch != '%' {
307                out.push(ch);
308                continue;
309            }
310            if matches!(chars.peek(), Some('%')) {
311                chars.next();
312                out.push('%');
313                continue;
314            }
315            while matches!(chars.peek(), Some('0'..='9')) {
316                chars.next();
317            }
318            let spec = chars.next().unwrap_or('d');
319            match spec {
320                'x' | 'h' | 'X' | 'H' | 'b' | 'B' | 'o' | 'O' | 'c' | 'C' | 's' | 'S' => {
321                    let Some(arg) = args.get(arg_idx) else {
322                        arg_idx += 1;
323                        continue;
324                    };
325                    out.push_str(&runtime_event_format_arg(arg, Some(spec)));
326                    arg_idx += 1;
327                }
328                'd' | 'D' | 'i' | 'I' => {
329                    let Some(arg) = args.get(arg_idx) else {
330                        arg_idx += 1;
331                        continue;
332                    };
333                    out.push_str(&runtime_event_format_arg(arg, Some(spec)));
334                    arg_idx += 1;
335                }
336                't' | 'T' => out.push_str(&ctx.tb_time.unwrap_or(0).to_string()),
337                'm' | 'M' => {
338                    out.push_str(ctx.scope.or(site.scope.as_deref()).unwrap_or("<hierarchy>"))
339                }
340                other => {
341                    out.push('%');
342                    out.push(other);
343                }
344            }
345        }
346        out
347    }
348
349    fn render_raw_runtime_event(
350        raw: RawRuntimeEvent,
351        sites: &[RuntimeEventSite],
352        ctx: RuntimeFormatContext<'_>,
353    ) -> Option<RuntimeEvent> {
354        match raw {
355            RawRuntimeEvent::Missed { count } => Some(RuntimeEvent::Missed { count }),
356            RawRuntimeEvent::Event { site_id, args } => {
357                let site = sites.get(site_id)?;
358                let message = render_runtime_event_message(site, &args, ctx);
359                Some(match site.kind {
360                    RuntimeEventKind::Display => RuntimeEvent::Display { message },
361                    RuntimeEventKind::Write => RuntimeEvent::Write { message },
362                    RuntimeEventKind::AssertContinue => RuntimeEvent::AssertContinue { message },
363                    RuntimeEventKind::AssertFatal => RuntimeEvent::AssertFatal { message },
364                })
365            }
366        }
367    }
368
369    fn collect_runtime_events(
370        layout: &MemoryLayout,
371        sites: &[RuntimeEventSite],
372        read_seq: &mut u64,
373        buffer_size: usize,
374        mut read_payload_u64: impl FnMut(usize) -> u64,
375        mut read_seq_u64: impl FnMut(usize) -> u64,
376    ) -> Vec<RawRuntimeEvent> {
377        use crate::backend::memory_layout::{
378            RUNTIME_EVENT_HEADER_SIZE, RUNTIME_EVENT_SLOT_ARG_COUNT_OFFSET,
379            RUNTIME_EVENT_SLOT_PAYLOAD_OFFSET, RUNTIME_EVENT_SLOT_SEQ_OFFSET,
380            RUNTIME_EVENT_SLOT_SITE_OFFSET, RUNTIME_EVENT_WRITING,
381        };
382
383        if RUNTIME_EVENT_HEADER_SIZE > buffer_size || layout.runtime_event_capacity == 0 {
384            return Vec::new();
385        }
386
387        // Ring-buffer synchronization protocol:
388        // - writers store event payload/site/arg fields normally;
389        // - writers publish slot/global sequence words with release semantics;
390        // - readers acquire-load sequence words, then read payload normally;
391        // - readers re-check the slot sequence after reading payload to reject races.
392        let write_seq = read_seq_u64(0);
393        let mut events = Vec::new();
394        let capacity = layout.runtime_event_capacity as u64;
395        if *read_seq + capacity < write_seq {
396            let new_read = write_seq - capacity;
397            events.push(RawRuntimeEvent::Missed {
398                count: new_read - *read_seq,
399            });
400            *read_seq = new_read;
401        }
402
403        while *read_seq < write_seq {
404            let seq = *read_seq;
405            let slot = (seq as usize) & (layout.runtime_event_capacity - 1);
406            let slot_base = RUNTIME_EVENT_HEADER_SIZE + slot * layout.runtime_event_slot_size;
407            let published = read_seq_u64(slot_base + RUNTIME_EVENT_SLOT_SEQ_OFFSET);
408            if published == RUNTIME_EVENT_WRITING || published != seq {
409                break;
410            }
411
412            let site_id = read_payload_u64(slot_base + RUNTIME_EVENT_SLOT_SITE_OFFSET) as usize;
413            let site = sites.get(site_id);
414            let site_layout = layout.runtime_event_site_layouts.get(site_id);
415            let arg_count =
416                read_payload_u64(slot_base + RUNTIME_EVENT_SLOT_ARG_COUNT_OFFSET) as usize;
417            let arg_count = site
418                .map(|site| arg_count.min(site.arg_widths.len()))
419                .unwrap_or(0);
420            let mut args = Vec::with_capacity(arg_count);
421            if let Some(site_layout) = site_layout {
422                for idx in 0..arg_count {
423                    let Some(arg_layout) = site_layout.args.get(idx) else {
424                        break;
425                    };
426                    let mut values = Vec::with_capacity(arg_layout.word_count);
427                    let mut masks = Vec::with_capacity(arg_layout.word_count);
428                    for word_idx in 0..arg_layout.word_count {
429                        values.push(read_payload_u64(
430                            slot_base
431                                + RUNTIME_EVENT_SLOT_PAYLOAD_OFFSET
432                                + (arg_layout.value_word_offset + word_idx) * 8,
433                        ));
434                        masks.push(read_payload_u64(
435                            slot_base
436                                + RUNTIME_EVENT_SLOT_PAYLOAD_OFFSET
437                                + (arg_layout.mask_word_offset + word_idx) * 8,
438                        ));
439                    }
440                    args.push(RuntimeEventArgValue {
441                        values,
442                        masks,
443                        width: site
444                            .and_then(|site| site.arg_widths.get(idx).copied())
445                            .unwrap_or(64),
446                        signed: site
447                            .and_then(|site| site.arg_signed.get(idx).copied())
448                            .unwrap_or(false),
449                        is_string: site
450                            .and_then(|site| site.arg_is_string.get(idx).copied())
451                            .unwrap_or(false),
452                    });
453                }
454            }
455
456            let published_after = read_seq_u64(slot_base + RUNTIME_EVENT_SLOT_SEQ_OFFSET);
457            if published_after == RUNTIME_EVENT_WRITING || published_after != seq {
458                break;
459            }
460
461            if site.is_some() {
462                events.push(RawRuntimeEvent::Event { site_id, args });
463            }
464            *read_seq += 1;
465        }
466
467        events
468    }
469
470    fn drain_raw_runtime_events_from_buffer(
471        buffer: &RuntimeEventBuffer,
472        layout: &MemoryLayout,
473        sites: &[RuntimeEventSite],
474        read_seq: &mut u64,
475    ) -> Vec<RawRuntimeEvent> {
476        use std::sync::atomic::Ordering;
477
478        collect_runtime_events(
479            layout,
480            sites,
481            read_seq,
482            buffer.byte_size(),
483            |offset| buffer.read_u64(offset),
484            |offset| buffer.load_atomic_u64(offset, Ordering::Acquire),
485        )
486    }
487
488    #[cfg(any(
489        target_arch = "x86_64",
490        feature = "arm64-codegen",
491        target_arch = "aarch64"
492    ))]
493    pub(crate) fn runtime_event_write_seq_for_backend<B: SimBackend>(backend: &B) -> u64 {
494        if let Some(buffer) = backend.runtime_event_buffer() {
495            buffer.load_atomic_u64(0, std::sync::atomic::Ordering::Acquire)
496        } else {
497            let (pointer, size) = backend.runtime_event_buffer_as_ptr();
498            if size < std::mem::size_of::<u64>() {
499                return 0;
500            }
501            // Safety: the backend contract exposes `size` readable bytes.
502            unsafe { std::ptr::read_volatile(pointer.cast::<u64>()) }
503        }
504    }
505
506    #[cfg(any(
507        target_arch = "x86_64",
508        feature = "arm64-codegen",
509        target_arch = "aarch64"
510    ))]
511    pub(crate) fn collect_runtime_events_for_backend<B: SimBackend>(
512        backend: &B,
513        sites: &[RuntimeEventSite],
514        read_seq: &mut u64,
515        context: RuntimeFormatContext<'_>,
516    ) -> Vec<RuntimeEvent> {
517        let layout = backend.layout();
518        let raw = if let Some(buffer) = backend.runtime_event_buffer() {
519            collect_runtime_events(
520                layout,
521                sites,
522                read_seq,
523                buffer.byte_size(),
524                |offset| buffer.read_u64(offset),
525                |offset| buffer.load_atomic_u64(offset, std::sync::atomic::Ordering::Acquire),
526            )
527        } else {
528            let (pointer, size) = backend.runtime_event_buffer_as_ptr();
529            let read_u64 = |offset: usize| -> u64 {
530                // Safety: `collect_runtime_events` bounds every access by `size`.
531                unsafe { std::ptr::read_volatile(pointer.add(offset).cast::<u64>()) }
532            };
533            collect_runtime_events(layout, sites, read_seq, size, read_u64, read_u64)
534        };
535        raw.into_iter()
536            .filter_map(|event| render_raw_runtime_event(event, sites, context))
537            .collect()
538    }
539
540    // ── Generic methods available for any backend ────────────────────────
541    impl<B: SimBackend> Simulator<B> {
542        fn decorate_runtime_error(&self, err: RuntimeErrorCode) -> RuntimeErrorCode {
543            match err {
544                RuntimeErrorCode::DetectedTrueLoopCode(code) => {
545                    let Some(info) = self.program.runtime_schema.runtime_errors.get(&code) else {
546                        return RuntimeErrorCode::DetectedTrueLoop;
547                    };
548                    let signals = info
549                        .signals
550                        .iter()
551                        .map(|addr| self.program.get_path(addr))
552                        .collect::<Vec<_>>();
553                    if info.message == "Detected True Loop" {
554                        RuntimeErrorCode::DetectedTrueLoopAt { signals }
555                    } else {
556                        RuntimeErrorCode::Runtime {
557                            message: info.message.clone(),
558                            signals,
559                        }
560                    }
561                }
562                other => other,
563            }
564        }
565
566        pub fn with_backend_and_program(
567            backend: B,
568            program: RuntimeProgram,
569            warnings: Vec<CompilationWarning>,
570        ) -> Self {
571            let mut sim = Self {
572                backend,
573                program,
574                vcd_writer: None,
575                dirty: false,
576                warnings,
577                components: Default::default(),
578                component_simulation: None,
579                runtime_event_read_seq: Arc::new(AtomicU64::new(0)),
580                runtime_event_drain_active: Arc::new(AtomicBool::new(false)),
581                comb_observer_snapshots: Vec::new(),
582                comb_observer_initial_eval: true,
583                diagnostics: crate::RuntimeDiagnostics::default(),
584                tick_timing_ticks: 0,
585                tick_timing_eval_apply_ns: 0,
586                tick_timing_eval_comb_ns: 0,
587            };
588            sim.comb_observer_snapshots = sim.snapshot_all_comb_observers();
589            sim
590        }
591
592        fn record_tick_timing(&mut self, eval_apply_ns: u64, eval_comb_ns: u64) {
593            let Some(every) = self.diagnostics.tick_timing_every else {
594                return;
595            };
596            if every == 0 {
597                return;
598            }
599            self.tick_timing_ticks = self.tick_timing_ticks.saturating_add(1);
600            self.tick_timing_eval_apply_ns =
601                self.tick_timing_eval_apply_ns.saturating_add(eval_apply_ns);
602            self.tick_timing_eval_comb_ns =
603                self.tick_timing_eval_comb_ns.saturating_add(eval_comb_ns);
604            if self.tick_timing_ticks.is_multiple_of(every) {
605                tracing::debug!(
606                    "[tick-timing] ticks={} eval_apply_ms={:.3} eval_comb_ms={:.3} avg_apply_us={:.3} avg_comb_us={:.3}",
607                    self.tick_timing_ticks,
608                    self.tick_timing_eval_apply_ns as f64 / 1_000_000.0,
609                    self.tick_timing_eval_comb_ns as f64 / 1_000_000.0,
610                    self.tick_timing_eval_apply_ns as f64 / self.tick_timing_ticks as f64 / 1_000.0,
611                    self.tick_timing_eval_comb_ns as f64 / self.tick_timing_ticks as f64 / 1_000.0,
612                );
613            }
614        }
615
616        pub(crate) fn apply_initial_values(&mut self) {
617            let mut applied = false;
618            let initial_memory_values = self.program.design.take_initial_state();
619            for init in &initial_memory_values {
620                applied = true;
621                let signal = self.backend.resolve_signal(&init.address);
622                match &init.data {
623                    InitialMemoryData::Packed {
624                        value,
625                        mask,
626                        written_mask,
627                    } => {
628                        let width_mask = if signal.width == 0 {
629                            BigUint::default()
630                        } else {
631                            (BigUint::from(1u8) << signal.width) - BigUint::from(1u8)
632                        };
633                        let preserve_mask = &width_mask ^ (written_mask & &width_mask);
634                        let (current_value, current_mask) = self.backend.get_four_state(signal);
635                        let value = (current_value & &preserve_mask) | (value & written_mask);
636                        let mask = (current_mask & &preserve_mask) | (mask & written_mask);
637                        if self.backend.layout().four_state && signal.is_4state {
638                            self.backend.set_four_state(signal, value, mask);
639                        } else {
640                            let known_mask = &width_mask ^ (&mask & &width_mask);
641                            self.backend.set_wide(signal, value & known_mask);
642                        }
643                    }
644                    InitialMemoryData::Writes(runs) => {
645                        self.apply_initial_memory_writes(signal, runs);
646                    }
647                }
648            }
649            if applied {
650                self.dirty = true;
651            }
652            self.program
653                .design
654                .restore_initial_state(initial_memory_values);
655        }
656
657        fn apply_initial_memory_writes(
658            &mut self,
659            signal: SignalRef,
660            runs: &[InitialMemoryWriteRun],
661        ) {
662            let value_byte_size = signal.width.div_ceil(8);
663            let write_mask = self.backend.layout().four_state && signal.is_4state;
664            // Initial writes precede the first full dump; no mutable view escapes.
665            let (ptr, mem_len) = self.backend.memory_as_ptr();
666            let ptr = ptr.cast_mut();
667            let mem = unsafe { std::slice::from_raw_parts_mut(ptr, mem_len) };
668
669            for run in runs {
670                if run.bit_width == 0 {
671                    continue;
672                }
673                if signal.array_layout.is_some() {
674                    write_initial_run_to_plane(mem, signal, false, run, &run.value_bytes);
675                    if write_mask {
676                        write_initial_run_to_plane(mem, signal, true, run, &run.mask_bytes);
677                    }
678                    continue;
679                }
680                if run.bit_offset % 8 == 0 && run.bit_width % 8 == 0 {
681                    let byte_offset = run.bit_offset / 8;
682                    let byte_width = run.bit_width / 8;
683                    let value_offset = signal.offset + byte_offset;
684                    mem[value_offset..value_offset + byte_width]
685                        .copy_from_slice(&run.value_bytes[..byte_width]);
686                    if write_mask {
687                        let mask_offset = signal.offset + value_byte_size + byte_offset;
688                        mem[mask_offset..mask_offset + byte_width]
689                            .copy_from_slice(&run.mask_bytes[..byte_width]);
690                    }
691                    continue;
692                }
693
694                write_bits_to_memory(
695                    mem,
696                    signal.offset * 8 + run.bit_offset,
697                    run.bit_width,
698                    &run.value_bytes,
699                );
700                if write_mask {
701                    write_bits_to_memory(
702                        mem,
703                        (signal.offset + value_byte_size) * 8 + run.bit_offset,
704                        run.bit_width,
705                        &run.mask_bytes,
706                    );
707                }
708            }
709        }
710
711        pub fn drain_runtime_events(&mut self) -> Vec<RuntimeEvent> {
712            self.drain_runtime_events_with_context(RuntimeFormatContext::default())
713        }
714
715        pub fn drain_runtime_events_with_context(
716            &mut self,
717            ctx: RuntimeFormatContext<'_>,
718        ) -> Vec<RuntimeEvent> {
719            assert!(
720                !self.runtime_event_drain_active.load(Ordering::Acquire),
721                "cannot use Simulator::drain_runtime_events while a RuntimeEventDrain is active",
722            );
723            if self.dirty {
724                self.eval_comb_checked().unwrap();
725                self.dirty = false;
726            }
727            self.collect_formatted_runtime_events(ctx)
728        }
729
730        pub(crate) fn drain_runtime_events_deferred_with_context(
731            &mut self,
732            ctx: RuntimeFormatContext<'_>,
733        ) -> Vec<RuntimeEvent> {
734            assert!(
735                !self.runtime_event_drain_active.load(Ordering::Acquire),
736                "cannot use Simulator::drain_runtime_events while a RuntimeEventDrain is active",
737            );
738            if !self.program.runtime_schema.comb_observers.is_empty() && self.dirty {
739                self.eval_comb_checked().unwrap();
740                self.dirty = false;
741            }
742            self.collect_formatted_runtime_events(ctx)
743        }
744
745        fn collect_formatted_runtime_events(
746            &mut self,
747            ctx: RuntimeFormatContext<'_>,
748        ) -> Vec<RuntimeEvent> {
749            if self.runtime_event_read_seq.load(Ordering::Acquire) == self.runtime_event_write_seq()
750            {
751                return Vec::new();
752            }
753            self.collect_backend_runtime_events()
754                .into_iter()
755                .filter_map(|raw| {
756                    render_raw_runtime_event(
757                        raw,
758                        &self.program.runtime_schema.runtime_event_sites,
759                        ctx,
760                    )
761                })
762                .collect()
763        }
764
765        fn collect_backend_runtime_events(&mut self) -> Vec<RawRuntimeEvent> {
766            let layout = self.backend.layout();
767            let mut read_seq = self.runtime_event_read_seq.load(Ordering::Acquire);
768            if let Some(buffer) = self.backend.runtime_event_buffer() {
769                let events = collect_runtime_events(
770                    layout,
771                    &self.program.runtime_schema.runtime_event_sites,
772                    &mut read_seq,
773                    buffer.byte_size(),
774                    |offset| buffer.read_u64(offset),
775                    |offset| buffer.load_atomic_u64(offset, std::sync::atomic::Ordering::Acquire),
776                );
777                self.runtime_event_read_seq
778                    .store(read_seq, Ordering::Release);
779                events
780            } else {
781                let (ptr, size) = self.backend.runtime_event_buffer_as_ptr();
782                let read_u64 = |offset: usize| -> u64 {
783                    unsafe { std::ptr::read_volatile(ptr.add(offset) as *const u64) }
784                };
785                let events = collect_runtime_events(
786                    layout,
787                    &self.program.runtime_schema.runtime_event_sites,
788                    &mut read_seq,
789                    size,
790                    read_u64,
791                    read_u64,
792                );
793                self.runtime_event_read_seq
794                    .store(read_seq, Ordering::Release);
795                events
796            }
797        }
798
799        fn runtime_event_write_seq(&self) -> u64 {
800            if let Some(buffer) = self.backend.runtime_event_buffer() {
801                buffer.load_atomic_u64(0, std::sync::atomic::Ordering::Acquire)
802            } else {
803                let (ptr, _size) = self.backend.runtime_event_buffer_as_ptr();
804                unsafe { std::ptr::read_volatile(ptr as *const u64) }
805            }
806        }
807
808        fn peek_backend_runtime_events_from(&self, read_seq: u64) -> Vec<RawRuntimeEvent> {
809            let mut read_seq = read_seq;
810            let layout = self.backend.layout();
811            if let Some(buffer) = self.backend.runtime_event_buffer() {
812                collect_runtime_events(
813                    layout,
814                    &self.program.runtime_schema.runtime_event_sites,
815                    &mut read_seq,
816                    buffer.byte_size(),
817                    |offset| buffer.read_u64(offset),
818                    |offset| buffer.load_atomic_u64(offset, std::sync::atomic::Ordering::Acquire),
819                )
820            } else {
821                let (ptr, size) = self.backend.runtime_event_buffer_as_ptr();
822                let read_u64 = |offset: usize| -> u64 {
823                    unsafe { std::ptr::read_volatile(ptr.add(offset) as *const u64) }
824                };
825                collect_runtime_events(
826                    layout,
827                    &self.program.runtime_schema.runtime_event_sites,
828                    &mut read_seq,
829                    size,
830                    read_u64,
831                    read_u64,
832                )
833            }
834        }
835
836        pub fn runtime_event_drain(&mut self) -> Option<RuntimeEventDrain> {
837            let buffer = self.backend.runtime_event_buffer()?;
838            self.runtime_event_drain_active
839                .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
840                .ok()?;
841            Some(RuntimeEventDrain {
842                buffer,
843                layout: self.backend.layout().clone(),
844                sites: self.program.runtime_schema.runtime_event_sites.clone(),
845                read_seq: self.runtime_event_read_seq.load(Ordering::Acquire),
846                shared_read_seq: Arc::clone(&self.runtime_event_read_seq),
847                active: Arc::clone(&self.runtime_event_drain_active),
848            })
849        }
850
851        /// Returns a reference to the compiled SIR program.
852        pub fn program(&self) -> &RuntimeProgram {
853            &self.program
854        }
855
856        /// Returns a reference to the backend (for signal/event resolution).
857        pub fn backend_ref(&self) -> &B {
858            &self.backend
859        }
860
861        /// Returns warnings emitted during compilation.
862        pub fn warnings(&self) -> &[CompilationWarning] {
863            &self.warnings
864        }
865
866        /// Captures the current state of all signals and writes them to the VCD file.
867        pub fn dump(&mut self, timestamp: u64) {
868            if self.dirty {
869                self.eval_comb_checked().unwrap();
870                self.dirty = false;
871            }
872            let component_traces = self.components.trace_values();
873            if let Some(ref mut writer) = self.vcd_writer {
874                writer
875                    .dump_backend(timestamp, &mut self.backend, &component_traces)
876                    .unwrap();
877            }
878        }
879
880        /// Make buffered waveform output visible and report write errors.
881        pub fn flush_vcd(&mut self) -> std::io::Result<()> {
882            if let Some(writer) = self.vcd_writer.as_mut() {
883                writer.flush()?;
884            }
885            Ok(())
886        }
887
888        pub fn vcd_statistics(&self) -> Option<celox_runtime::VcdStatistics> {
889            self.vcd_writer.as_ref().map(crate::VcdWriter::statistics)
890        }
891
892        /// Sets a signal value and marks combinational logic as dirty.
893        pub fn set<T: Copy>(&mut self, signal: SignalRef, val: T) {
894            self.backend.set(signal, val);
895            self.dirty = true;
896            self.settle_dirty_for_runtime_event_drain();
897        }
898
899        /// Sets a wide signal value and marks combinational logic as dirty.
900        pub fn set_wide(&mut self, signal: SignalRef, val: BigUint) {
901            self.backend.set_wide(signal, val);
902            self.dirty = true;
903            self.settle_dirty_for_runtime_event_drain();
904        }
905
906        /// Sets a four-state signal value and marks combinational logic as dirty.
907        pub fn set_four_state(&mut self, signal: SignalRef, val: BigUint, mask: BigUint) {
908            self.backend.set_four_state(signal, val, mask);
909            self.dirty = true;
910            self.settle_dirty_for_runtime_event_drain();
911        }
912
913        /// Modifies internal state via a callback and marks combinational logic as dirty.
914        pub fn modify<F>(&mut self, f: F) -> Result<(), RuntimeErrorCode>
915        where
916            F: FnOnce(&mut IOContext<B>),
917        {
918            let mut ctx = IOContext {
919                backend: &mut self.backend,
920            };
921            f(&mut ctx);
922            self.dirty = true;
923            if self.runtime_event_drain_active.load(Ordering::Acquire) {
924                self.eval_comb_checked()?;
925                self.dirty = false;
926            }
927            Ok(())
928        }
929
930        fn settle_dirty_for_runtime_event_drain(&mut self) {
931            if self.runtime_event_drain_active.load(Ordering::Acquire) {
932                self.eval_comb_checked().unwrap();
933                self.dirty = false;
934            }
935        }
936
937        pub(crate) fn eval_comb_checked(&mut self) -> Result<(), RuntimeErrorCode> {
938            if self.program.runtime_schema.runtime_event_sites.is_empty() {
939                return self
940                    .backend
941                    .eval_comb()
942                    .map_err(|e| self.decorate_runtime_error(e));
943            }
944            if self.program.runtime_schema.comb_observers.is_empty() {
945                let runtime_event_start_seq = self.runtime_event_write_seq();
946                let eval_result = self
947                    .backend
948                    .eval_comb()
949                    .map_err(|e| self.decorate_runtime_error(e));
950                let runtime_events = self.peek_backend_runtime_events_from(runtime_event_start_seq);
951                if let Some(err) = self.fatal_comb_capture_error(&runtime_events) {
952                    return Err(err);
953                }
954                return eval_result;
955            }
956
957            let before = self.snapshot_all_comb_observers();
958            let active_before: Vec<bool> = before
959                .iter()
960                .zip(&self.comb_observer_snapshots)
961                .map(|(now, prev)| now != prev)
962                .collect();
963            let mut active_sites =
964                vec![false; self.program.runtime_schema.runtime_event_sites.len()];
965            for (observer, is_active) in self
966                .program
967                .runtime_schema
968                .comb_observers
969                .iter()
970                .zip(active_before.iter().copied())
971            {
972                if is_active || self.comb_observer_initial_eval {
973                    let group = observer.activation_group;
974                    for group_observer in &self.program.runtime_schema.comb_observers {
975                        if group_observer.activation_group == group {
976                            active_sites[group_observer.site_id as usize] = true;
977                        }
978                    }
979                }
980            }
981            self.backend.set_comb_capture_event_enabled(&active_sites);
982            let runtime_event_start_seq = self.runtime_event_write_seq();
983            let eval_result = self
984                .backend
985                .eval_comb()
986                .map_err(|e| self.decorate_runtime_error(e));
987            let after = self.snapshot_all_comb_observers();
988            let runtime_events = self.peek_backend_runtime_events_from(runtime_event_start_seq);
989            let fatal_error = self.fatal_comb_capture_error(&runtime_events);
990            self.backend.set_comb_capture_event_enabled(&vec![
991                false;
992                self.program
993                    .runtime_schema
994                    .runtime_event_sites
995                    .len()
996            ]);
997            self.comb_observer_snapshots = after;
998            self.comb_observer_initial_eval = false;
999            if let Some(err) = fatal_error {
1000                return Err(err);
1001            }
1002            eval_result
1003        }
1004
1005        fn snapshot_all_comb_observers(&self) -> Vec<Vec<(BigUint, BigUint)>> {
1006            self.program
1007                .runtime_schema
1008                .comb_observers
1009                .iter()
1010                .map(|observer| {
1011                    observer
1012                        .sensitivity
1013                        .iter()
1014                        .map(|atom| {
1015                            let signal = self.backend.resolve_signal(&atom.id);
1016                            let (value, mask) = if signal.is_4state {
1017                                self.backend.get_four_state(signal)
1018                            } else {
1019                                (self.backend.get(signal), BigUint::default())
1020                            };
1021                            (
1022                                slice_biguint(&value, atom.access.lsb, atom.access.msb),
1023                                slice_biguint(&mask, atom.access.lsb, atom.access.msb),
1024                            )
1025                        })
1026                        .collect()
1027                })
1028                .collect()
1029        }
1030
1031        fn fatal_comb_capture_error(&self, events: &[RawRuntimeEvent]) -> Option<RuntimeErrorCode> {
1032            events.iter().find_map(|raw| {
1033                let RawRuntimeEvent::Event { site_id, args } = raw else {
1034                    return None;
1035                };
1036                let site = self
1037                    .program
1038                    .runtime_schema
1039                    .runtime_event_sites
1040                    .get(*site_id)?;
1041                if !matches!(site.kind, RuntimeEventKind::AssertFatal) {
1042                    return None;
1043                }
1044                Some(RuntimeErrorCode::Runtime {
1045                    message: render_runtime_event_message(
1046                        site,
1047                        args,
1048                        RuntimeFormatContext::default(),
1049                    ),
1050                    signals: Vec::new(),
1051                })
1052            })
1053        }
1054
1055        pub(crate) fn eval_apply_ff_at_checked(
1056            &mut self,
1057            event: B::Event,
1058        ) -> Result<(), RuntimeErrorCode> {
1059            self.backend
1060                .eval_apply_ff_at(event)
1061                .map_err(|e| self.decorate_runtime_error(e))
1062        }
1063
1064        pub(crate) fn eval_comb_apply_ff_at_checked(
1065            &mut self,
1066            event: B::Event,
1067        ) -> Result<(), RuntimeErrorCode> {
1068            self.backend
1069                .eval_comb_apply_ff_at(event)
1070                .map_err(|e| self.decorate_runtime_error(e))
1071        }
1072
1073        pub(crate) fn eval_only_ff_at_checked(
1074            &mut self,
1075            event: B::Event,
1076        ) -> Result<(), RuntimeErrorCode> {
1077            self.backend
1078                .eval_only_ff_at(event)
1079                .map_err(|e| self.decorate_runtime_error(e))
1080        }
1081
1082        pub(crate) fn apply_ff_at_checked(
1083            &mut self,
1084            event: B::Event,
1085        ) -> Result<(), RuntimeErrorCode> {
1086            self.backend
1087                .apply_ff_at(event)
1088                .map_err(|e| self.decorate_runtime_error(e))
1089        }
1090
1091        /// Manually triggers a clock or event to process sequential logic.
1092        pub fn tick(&mut self, event: B::Event) -> Result<(), RuntimeErrorCode> {
1093            let timing_enabled = self.diagnostics.tick_timing_every.is_some();
1094            let mut eval_comb_ns = 0u64;
1095            let mut eval_apply_ns = 0u64;
1096            if self.dirty {
1097                if timing_enabled {
1098                    let start = crate::timing::now();
1099                    self.eval_comb_checked()?;
1100                    eval_comb_ns = eval_comb_ns.saturating_add(start.elapsed().as_nanos() as u64);
1101                    let start = crate::timing::now();
1102                    self.eval_apply_ff_at_checked(event)?;
1103                    eval_apply_ns = eval_apply_ns.saturating_add(start.elapsed().as_nanos() as u64);
1104                } else {
1105                    self.eval_comb_checked()?;
1106                    self.eval_apply_ff_at_checked(event)?;
1107                }
1108                self.dirty = false;
1109            } else {
1110                if timing_enabled {
1111                    let start = crate::timing::now();
1112                    self.eval_apply_ff_at_checked(event)?;
1113                    eval_apply_ns = eval_apply_ns.saturating_add(start.elapsed().as_nanos() as u64);
1114                } else {
1115                    self.eval_apply_ff_at_checked(event)?;
1116                }
1117            }
1118            if timing_enabled {
1119                let start = crate::timing::now();
1120                self.eval_comb_checked()?;
1121                eval_comb_ns = eval_comb_ns.saturating_add(start.elapsed().as_nanos() as u64);
1122                self.record_tick_timing(eval_apply_ns, eval_comb_ns);
1123            } else {
1124                self.eval_comb_checked()?;
1125            }
1126            self.dirty = false;
1127            Ok(())
1128        }
1129
1130        /// Advance one event while deferring the post-edge combinational settle.
1131        /// Native testbenches use this between observable expressions so the
1132        /// preceding comb phase and this FF phase can share one compiled function.
1133        pub(crate) fn tick_deferred_comb(
1134            &mut self,
1135            event: B::Event,
1136        ) -> Result<(), RuntimeErrorCode> {
1137            if !self.program.runtime_schema.comb_observers.is_empty() {
1138                return self.tick(event);
1139            }
1140            if self.dirty {
1141                self.eval_comb_apply_ff_at_checked(event)?;
1142            } else {
1143                self.eval_apply_ff_at_checked(event)?;
1144            }
1145            self.dirty = true;
1146            Ok(())
1147        }
1148
1149        /// Advance a run of identical deferred-comb ticks. Native code may keep
1150        /// the loop inside the generated function, but must return after publishing
1151        /// a runtime event so host-side observation remains tick-accurate.
1152        pub(crate) fn tick_deferred_comb_many(
1153            &mut self,
1154            event: B::Event,
1155            count: u64,
1156        ) -> (u64, Result<(), RuntimeErrorCode>) {
1157            if count == 0 {
1158                return (0, Ok(()));
1159            }
1160            if !self.program.runtime_schema.comb_observers.is_empty() || !self.dirty {
1161                return (1, self.tick_deferred_comb(event));
1162            }
1163            let (completed, result) = self.backend.eval_comb_apply_ff_many_at(event, count);
1164            self.dirty = true;
1165            (
1166                completed,
1167                result.map_err(|error| self.decorate_runtime_error(error)),
1168            )
1169        }
1170
1171        /// Resolves a signal path into a performance-optimized [`SignalRef`].
1172        /// This handle allows for direct memory access without `HashMap` lookups.
1173        pub fn signal(&self, path: &str) -> SignalRef {
1174            let addr = self.program.get_addr(&[], &[path]).unwrap();
1175            self.backend.resolve_signal(&addr)
1176        }
1177
1178        /// Resolve a port name to an event handle.
1179        pub fn event(&self, port: &str) -> B::Event {
1180            let addr = self.program.get_addr(&[], &[port]).unwrap();
1181            self.backend.resolve_event(&addr)
1182        }
1183
1184        /// Try to resolve a signal path. Returns `Err` if the path is not found or ambiguous.
1185        pub fn try_signal(&self, path: &str) -> Result<SignalRef, crate::ir::AddrLookupError> {
1186            let addr = self.program.get_addr(&[], &[path])?;
1187            Ok(self.backend.resolve_signal(&addr))
1188        }
1189
1190        /// Try to resolve a port name to an event handle.
1191        pub fn try_event(&self, port: &str) -> Result<B::Event, crate::ir::AddrLookupError> {
1192            let addr = self.program.get_addr(&[], &[port])?;
1193            Ok(self.backend.resolve_event(&addr))
1194        }
1195
1196        /// Retrieves the current value as a fixed-size type without `BigUint` allocation.
1197        /// Lazily evaluates combinational logic if the state is dirty.
1198        pub fn get_as<T: Default + Copy>(&mut self, signal: SignalRef) -> T {
1199            if self.dirty {
1200                self.eval_comb_checked().unwrap();
1201                self.dirty = false;
1202            }
1203            self.backend.get_as(signal)
1204        }
1205
1206        /// Retrieves the current value of a variable using a pre-resolved [`SignalRef`] handle.
1207        /// Lazily evaluates combinational logic if the state is dirty.
1208        pub fn get(&mut self, signal: SignalRef) -> BigUint {
1209            if self.dirty {
1210                self.eval_comb_checked().unwrap();
1211                self.dirty = false;
1212            }
1213            self.backend.get(signal)
1214        }
1215
1216        /// Retrieves the current 4-state value (value, mask) of a variable using a [`SignalRef`] handle.
1217        /// Lazily evaluates combinational logic if the state is dirty.
1218        pub fn get_four_state(&mut self, signal: SignalRef) -> (BigUint, BigUint) {
1219            if self.dirty {
1220                self.eval_comb_checked().unwrap();
1221                self.dirty = false;
1222            }
1223            self.backend.get_four_state(signal)
1224        }
1225
1226        /// Directly execute combinational logic evaluation.
1227        pub fn eval_comb(&mut self) -> Result<(), RuntimeErrorCode> {
1228            self.eval_comb_checked()?;
1229            self.dirty = false;
1230            Ok(())
1231        }
1232
1233        /// Returns a raw pointer to the backend memory and its total size in bytes.
1234        pub fn memory_as_ptr(&self) -> (*const u8, usize) {
1235            self.backend.memory_as_ptr()
1236        }
1237
1238        /// Returns a mutable raw pointer to the backend memory and its total size in bytes.
1239        pub fn memory_as_mut_ptr(&mut self) -> (*mut u8, usize) {
1240            self.backend.memory_as_mut_ptr()
1241        }
1242
1243        /// Returns an opaque owner that keeps the backend memory allocation alive.
1244        pub fn memory_owner(&self) -> Option<Arc<dyn std::any::Any + Send + Sync>> {
1245            self.backend.memory_owner()
1246        }
1247
1248        /// Returns the stable region size in bytes.
1249        pub fn stable_region_size(&self) -> usize {
1250            self.backend.stable_region_size()
1251        }
1252
1253        /// Returns a reference to the memory layout.
1254        pub fn layout(&self) -> &MemoryLayout {
1255            self.backend.layout()
1256        }
1257
1258        /// Take ownership of the VCD writer, if one was configured at build time.
1259        ///
1260        /// Builders create the writer themselves so that layout selection can
1261        /// account for VCD recording; callers building handles around a backend
1262        /// use this to keep tracing alive without rebuilding descriptors.
1263        pub fn take_vcd_writer(&mut self) -> Option<crate::VcdWriter> {
1264            self.vcd_writer.take()
1265        }
1266
1267        /// Build VCD signal descriptors for all instances.
1268        ///
1269        /// The returned descriptors are self-contained (no IR references) and can
1270        /// be cached alongside [`SharedJitCode`] so that VCD works on cache-hit
1271        /// paths without the original [`RuntimeProgram`].
1272        pub fn build_vcd_descs(&self, four_state_mode: bool) -> Vec<crate::VcdSignalDesc> {
1273            let mut descs = Vec::new();
1274            let mut sorted_instances = self.program.design.instances().collect::<Vec<_>>();
1275            sorted_instances.sort_by_key(|instance| instance.id);
1276
1277            for instance in sorted_instances {
1278                let scope = format!("{}", instance.id);
1279
1280                let mut sorted_vars: Vec<_> = instance
1281                    .state_addresses()
1282                    .iter()
1283                    .filter_map(|address| self.program.design.variable(address))
1284                    .filter(|variable| instance.resolves_path_to(&variable.path, variable.address))
1285                    .collect();
1286                sorted_vars.sort_by(|a, b| a.path.cmp(&b.path));
1287
1288                for variable in sorted_vars {
1289                    let name = variable.path.join(".");
1290                    let signal = self.backend.resolve_signal(&variable.address);
1291
1292                    descs.push(crate::VcdSignalDesc {
1293                        scope: scope.clone(),
1294                        name,
1295                        offset: signal.offset,
1296                        width: signal.width,
1297                        is_4state: four_state_mode && signal.is_4state,
1298                    });
1299                }
1300            }
1301            descs
1302        }
1303
1304        /// Returns all ports of the top-level module with their resolved signal references.
1305        pub fn named_signals(&self) -> Vec<NamedSignal> {
1306            let top_instance = self
1307                .program
1308                .design
1309                .root_instance()
1310                .expect("top-level instance not found");
1311            self.build_signals_for_instance(top_instance.id)
1312        }
1313
1314        /// Returns all signals for the instance at the given hierarchical path.
1315        ///
1316        /// The path is specified as a slice of `(instance_name, index)` pairs.
1317        /// Returns an empty `Vec` if the path does not exist.
1318        pub fn instance_signals(&self, instance_path: &[(&str, usize)]) -> Vec<NamedSignal> {
1319            let path: Vec<_> = instance_path
1320                .iter()
1321                .map(|(name, idx)| ((*name).to_string(), *idx))
1322                .collect();
1323            match self.program.design.instance_at_path(&InstancePath(path)) {
1324                Some(instance) => self.build_signals_for_instance(instance.id),
1325                None => Vec::new(),
1326            }
1327        }
1328
1329        /// Builds the list of named signals for a given instance.
1330        ///
1331        /// Variables with ambiguous VarPaths (multiple scoped locals sharing the
1332        /// same name) are excluded — they cannot be addressed by path and would
1333        /// cause silent overwrites in name-keyed maps such as the layout JSON.
1334        fn build_signals_for_instance(
1335            &self,
1336            instance_id: crate::ir::InstanceId,
1337        ) -> Vec<NamedSignal> {
1338            let instance = self.program.design.instance(instance_id).unwrap();
1339
1340            let mut result = Vec::new();
1341            for address in instance.state_addresses() {
1342                let variable = self.program.design.variable(address).unwrap();
1343                // Skip variables whose VarPath is ambiguous (None in the path index).
1344                if !instance.resolves_path_to(&variable.path, *address) {
1345                    continue;
1346                }
1347                let name = variable.path.join(".");
1348                let signal = self.backend.resolve_signal(address);
1349
1350                // Resolve associated clock for reset signals
1351                let associated_clock = self
1352                    .program
1353                    .design
1354                    .events
1355                    .reset_clocks
1356                    .get(address)
1357                    .map(|clock_addr| self.program.get_path(clock_addr));
1358
1359                result.push(NamedSignal {
1360                    name,
1361                    signal,
1362                    info: self.program.design.variable_info(address).unwrap(),
1363                    associated_clock,
1364                });
1365            }
1366            result
1367        }
1368
1369        /// Returns all events (clock/reset signals) with their IDs and event references.
1370        pub fn named_events(&self) -> Vec<NamedEvent<B>> {
1371            let mut result = Vec::new();
1372            for (id, addr) in self.backend.id_to_addr_slice().iter().enumerate() {
1373                let name = self.program.get_path(addr);
1374                if let Some(ev) = self.backend.resolve_event_opt(addr) {
1375                    result.push(NamedEvent {
1376                        name,
1377                        id,
1378                        event_ref: ev,
1379                    });
1380                }
1381            }
1382            result
1383        }
1384
1385        /// Triggers a clock/event by its numeric ID.
1386        ///
1387        /// # Errors
1388        ///
1389        /// Returns [`RuntimeErrorCode::NotAnEvent`] if `event_id` is not a
1390        /// known event ID, or a runtime error if evaluating the event fails.
1391        pub fn tick_by_id(&mut self, event_id: usize) -> Result<(), RuntimeErrorCode> {
1392            let event = self
1393                .backend
1394                .id_to_event_slice()
1395                .get(event_id)
1396                .copied()
1397                .ok_or_else(|| RuntimeErrorCode::NotAnEvent(format!("event_id={event_id}")))?;
1398            self.tick(event)
1399        }
1400
1401        /// Triggers a clock/event N times by its numeric ID.
1402        /// Avoids repeated cross-boundary calls when used from FFI.
1403        /// The event ID is validated even when `count` is zero.
1404        ///
1405        /// # Errors
1406        ///
1407        /// Returns [`RuntimeErrorCode::NotAnEvent`] if `event_id` is not a
1408        /// known event ID, or a runtime error if evaluating an event fails.
1409        pub fn tick_by_id_n(
1410            &mut self,
1411            event_id: usize,
1412            count: u32,
1413        ) -> Result<(), RuntimeErrorCode> {
1414            let event = self
1415                .backend
1416                .id_to_event_slice()
1417                .get(event_id)
1418                .copied()
1419                .ok_or_else(|| RuntimeErrorCode::NotAnEvent(format!("event_id={event_id}")))?;
1420            for _ in 0..count {
1421                self.tick(event)?;
1422            }
1423            Ok(())
1424        }
1425
1426        /// Resolves a signal inside a child instance.
1427        pub fn child_signal(&self, instance_path: &[(&str, usize)], var: &str) -> SignalRef {
1428            let addr = self.program.get_addr(instance_path, &[var]).unwrap();
1429            self.backend.resolve_signal(&addr)
1430        }
1431
1432        /// Try to resolve a signal inside a child instance.
1433        pub fn try_child_signal(
1434            &self,
1435            instance_path: &[(&str, usize)],
1436            var: &str,
1437        ) -> Result<SignalRef, crate::ir::AddrLookupError> {
1438            let addr = self.program.get_addr(instance_path, &[var])?;
1439            Ok(self.backend.resolve_signal(&addr))
1440        }
1441
1442        /// Returns the full instance hierarchy starting from the top module.
1443        pub fn named_hierarchy(&self) -> InstanceHierarchy {
1444            self.build_hierarchy(&[])
1445        }
1446
1447        fn build_hierarchy(&self, current_path: &[(String, usize)]) -> InstanceHierarchy {
1448            let instance = self
1449                .program
1450                .design
1451                .instance_at_path(&InstancePath(current_path.to_vec()))
1452                .expect("instance not found");
1453            let module_name = instance.module_name.clone();
1454
1455            let signals = self.build_signals_for_instance(instance.id);
1456
1457            // Find direct children: instance paths that extend current by exactly 1 segment
1458            let current_len = current_path.len();
1459            let mut children_map: crate::HashMap<String, Vec<(usize, InstanceHierarchy)>> =
1460                crate::HashMap::default();
1461
1462            for child in self.program.design.instances() {
1463                if child.path.0.len() == current_len + 1 && child.path.0.starts_with(current_path) {
1464                    let (child_name, child_index) = &child.path.0[current_len];
1465                    let child_hierarchy = self.build_hierarchy(&child.path.0);
1466                    children_map
1467                        .entry(child_name.clone())
1468                        .or_default()
1469                        .push((*child_index, child_hierarchy));
1470                }
1471            }
1472
1473            // Sort children by index within each group
1474            let mut children: Vec<(String, Vec<InstanceHierarchy>)> = children_map
1475                .into_iter()
1476                .map(|(name, mut instances)| {
1477                    instances.sort_by_key(|(idx, _)| *idx);
1478                    let sorted = instances.into_iter().map(|(_, h)| h).collect();
1479                    (name, sorted)
1480                })
1481                .collect();
1482            children.sort_by(|(a, _), (b, _)| a.cmp(b));
1483
1484            InstanceHierarchy {
1485                module_name,
1486                signals,
1487                children,
1488            }
1489        }
1490    }
1491
1492    // ── JitBackend-specific methods ──────────────────────────────────────
1493    impl Simulator {
1494        pub fn builder<'a>(code: &'a str, top: &'a str) -> SimulatorBuilder<'a, Simulator> {
1495            SimulatorBuilder::<Simulator>::new(code, top)
1496        }
1497
1498        pub fn from_sources<'a>(
1499            sources: Vec<(&'a str, &'a std::path::Path)>,
1500            top: &'a str,
1501        ) -> SimulatorBuilder<'a, Simulator> {
1502            SimulatorBuilder::<Simulator>::from_sources(sources, top)
1503        }
1504
1505        /// Low-level adapter hook for an elaborated external frontend artifact.
1506        ///
1507        /// Frontend crates should wrap this with a constructor named for their
1508        /// own artifact type instead of exposing
1509        /// [`celox_frontend_sdk::FrontendArtifact`] to applications.
1510        pub fn from_frontend(
1511            artifact: celox_frontend_sdk::FrontendArtifact,
1512        ) -> SimulatorBuilder<'static, Simulator> {
1513            SimulatorBuilder::<Simulator>::from_frontend(artifact)
1514        }
1515
1516        /// Low-level adapter hook for a Veryl testbench around an external module.
1517        pub fn from_frontend_with_testbench<'a>(
1518            artifact: celox_frontend_sdk::FrontendArtifact,
1519            sources: Vec<(&'a str, &'a std::path::Path)>,
1520            top: &'a str,
1521        ) -> SimulatorBuilder<'a, Simulator> {
1522            SimulatorBuilder::<Simulator>::from_frontend_with_testbench(artifact, sources, top)
1523        }
1524
1525        /// Build a simulator directly from SystemVerilog sources.
1526        #[cfg(feature = "systemverilog")]
1527        pub fn from_sv_sources<'a>(
1528            sources: Vec<(&'a str, &'a std::path::Path)>,
1529            top: &'a str,
1530        ) -> SimulatorBuilder<'a, Simulator> {
1531            SimulatorBuilder::<Simulator>::from_sv_sources(sources, top)
1532        }
1533
1534        /// Build a simulator from a Veryl hierarchy with SystemVerilog children.
1535        #[cfg(feature = "systemverilog")]
1536        pub fn from_mixed_sources<'a>(
1537            sources: Vec<(&'a str, &'a std::path::Path)>,
1538            sv_sources: Vec<(&'a str, &'a std::path::Path)>,
1539            top: &'a str,
1540        ) -> SimulatorBuilder<'a, Simulator> {
1541            SimulatorBuilder::<Simulator>::from_mixed_sources(sources, sv_sources, top)
1542        }
1543    }
1544
1545    impl Simulator<JitBackend> {
1546        /// Returns the shared compiled JIT code, allowing it to be reused
1547        /// for creating additional simulator instances without recompilation.
1548        pub fn shared_code(&self) -> Arc<SharedJitCode> {
1549            self.backend.shared_code()
1550        }
1551
1552        /// Consume the simulator and return the inner JIT backend.
1553        pub fn into_backend(self) -> JitBackend {
1554            self.backend
1555        }
1556    }
1557
1558    impl Simulator<crate::backend::wasm_runtime::WasmBackend> {
1559        /// Consume the simulator and return the inner Wasmtime backend.
1560        pub fn into_backend(self) -> crate::backend::wasm_runtime::WasmBackend {
1561            self.backend
1562        }
1563    }
1564
1565    impl Simulator<crate::backend::TieredBackend> {
1566        /// Whether this tiered simulation has adopted its compiled tier.
1567        ///
1568        /// False while background compilation is still running or after it
1569        /// failed (the interpreter then remains the permanent tier).
1570        pub fn is_compiled(&self) -> bool {
1571            self.backend.is_compiled()
1572        }
1573
1574        /// Return deterministic tier lifecycle and evaluation measurements.
1575        ///
1576        /// The snapshot contains counts rather than ad-hoc wall-clock samples;
1577        /// use a statistical benchmark harness to measure elapsed time.
1578        pub fn tiered_execution_stats(&self) -> crate::TieredExecutionStats {
1579            self.backend.execution_stats()
1580        }
1581
1582        /// Start opt-in wall-clock measurement from workload execution to
1583        /// promotion. Ordinary tiered simulation does not read the host clock.
1584        pub fn start_tiered_execution_timing(&mut self) {
1585            self.backend.start_execution_timing();
1586        }
1587
1588        /// Stop tiered execution timing and return the promotion interval.
1589        pub fn finish_tiered_execution_timing(&mut self) -> Option<crate::TieredExecutionTiming> {
1590            self.backend.finish_execution_timing()
1591        }
1592
1593        /// Why promotion has not happened yet, for diagnostics.
1594        pub fn promotion_error(&self) -> Option<&SimulatorError> {
1595            self.backend.promotion_error()
1596        }
1597
1598        /// Request cancellation of background compilation.
1599        ///
1600        /// The simulation stays on the interpreter permanently and
1601        /// [`Simulator::promotion_error`] reports the cancellation. Returns
1602        /// whether a background compilation was still pending.
1603        pub fn cancel_background_compilation(&mut self) -> bool {
1604            self.backend.cancel_background_compilation()
1605        }
1606
1607        /// Consume the simulator and return the inner tiered backend.
1608        pub fn into_backend(self) -> crate::backend::TieredBackend {
1609            self.backend
1610        }
1611    }
1612
1613    #[cfg(any(
1614        target_arch = "x86_64",
1615        feature = "arm64-codegen",
1616        target_arch = "aarch64"
1617    ))]
1618    impl Simulator<NativeBackend> {
1619        /// Returns the shared compiled native code, allowing it to be reused
1620        /// for creating additional simulator instances without recompilation.
1621        pub fn shared_code(&self) -> Arc<SharedNativeCode> {
1622            self.backend.shared_code()
1623        }
1624
1625        /// Start measuring time spent inside generated native simulator functions.
1626        ///
1627        /// The measurement is disabled by default, so ordinary simulation does not
1628        /// read the host clock. Native tick loops are measured once per host-side
1629        /// batch rather than once per simulated tick.
1630        pub fn start_native_execution_timing(&mut self) {
1631            self.backend.start_execution_timing();
1632        }
1633
1634        /// Stop native execution timing and return the accumulated measurement.
1635        pub fn finish_native_execution_timing(
1636            &mut self,
1637        ) -> Option<crate::native_backend::NativeExecutionTiming> {
1638            self.backend.finish_execution_timing()
1639        }
1640
1641        /// Create a simulator from pre-compiled shared native code.
1642        pub fn from_shared(
1643            shared: Arc<SharedNativeCode>,
1644            program: crate::ir::OptimizedSir,
1645        ) -> Self {
1646            let backend = NativeBackend::from_shared(shared);
1647            let mut sim = Self::with_backend_and_program(backend, program.into_runtime(), vec![]);
1648            sim.apply_initial_values();
1649            sim
1650        }
1651
1652        /// Consume the simulator and return the inner native backend.
1653        pub fn into_backend(self) -> NativeBackend {
1654            self.backend
1655        }
1656    }
1657}
1658
1659#[cfg(feature = "host-runtime")]
1660pub use host::*;