celox_runtime/backend.rs
1use std::{any::Any, sync::Arc};
2
3use num_bigint::BigUint;
4
5pub use crate::SimulatorErrorCode;
6use crate::{AbsoluteAddr, MemoryLayout, RuntimeEventBuffer, SignalRef};
7
8/// Marker trait for backend-specific event handles.
9///
10/// An event handle is an opaque reference to a compiled clock or
11/// async-reset trigger. It is resolved once via
12/// [`SimBackend::resolve_event`] and then passed to tick/eval methods
13/// for zero-cost dispatch.
14pub trait EventHandle: Copy + std::fmt::Debug {
15 /// Numeric event identifier used for scheduling.
16 fn id(&self) -> usize;
17
18 /// The absolute address of the signal this event is bound to.
19 fn addr(&self) -> AbsoluteAddr;
20}
21
22/// Abstraction over different simulation backends (JIT, WASM, etc.).
23///
24/// `Simulator<B>` is generic over this trait so that the same high-level
25/// API works with any backend. `JitBackend` is the default.
26pub trait SimBackend {
27 /// The event handle type produced by this backend.
28 type Event: EventHandle;
29
30 // ── evaluation ──────────────────────────────────────────────
31 fn eval_comb(&mut self) -> Result<(), SimulatorErrorCode>;
32
33 /// Evaluate and apply a flip-flop domain for the given event.
34 fn eval_apply_ff_at(&mut self, event: Self::Event) -> Result<(), SimulatorErrorCode>;
35
36 /// Evaluate combinational logic and then evaluate/apply one flip-flop
37 /// domain. Backends may override this to compile the two phases as one
38 /// function; the default preserves the same ordering with two calls.
39 fn eval_comb_apply_ff_at(&mut self, event: Self::Event) -> Result<(), SimulatorErrorCode> {
40 self.eval_comb()?;
41 self.eval_apply_ff_at(event)
42 }
43
44 /// Execute up to `count` identical fused ticks. The returned count is the
45 /// number of iterations completed before a runtime event or error forced a
46 /// return to the host. Backends without an in-function loop execute one
47 /// iteration so the caller can preserve per-tick observation semantics.
48 fn eval_comb_apply_ff_many_at(
49 &mut self,
50 event: Self::Event,
51 count: u64,
52 ) -> (u64, Result<(), SimulatorErrorCode>) {
53 if count == 0 {
54 return (0, Ok(()));
55 }
56 (1, self.eval_comb_apply_ff_at(event))
57 }
58
59 /// Evaluate FF domain without applying (for cascaded clocks).
60 fn eval_only_ff_at(&mut self, event: Self::Event) -> Result<(), SimulatorErrorCode>;
61
62 /// Apply (commit) an already-evaluated FF domain.
63 fn apply_ff_at(&mut self, event: Self::Event) -> Result<(), SimulatorErrorCode>;
64
65 // ── signal access ───────────────────────────────────────────
66 fn resolve_signal(&self, addr: &AbsoluteAddr) -> SignalRef;
67 fn resolve_event(&self, addr: &AbsoluteAddr) -> Self::Event;
68 fn resolve_event_opt(&self, addr: &AbsoluteAddr) -> Option<Self::Event>;
69 fn resolve_eval_only_event(&self, addr: &AbsoluteAddr) -> Option<Self::Event>;
70 fn resolve_apply_event(&self, addr: &AbsoluteAddr) -> Option<Self::Event>;
71
72 // ── get / set ───────────────────────────────────────────────
73 fn set<T: Copy>(&mut self, signal: SignalRef, val: T);
74 fn set_wide(&mut self, signal: SignalRef, val: BigUint);
75 fn set_four_state(&mut self, signal: SignalRef, val: BigUint, mask: BigUint);
76 fn get(&self, signal: SignalRef) -> BigUint;
77 fn get_as<T: Default + Copy>(&self, signal: SignalRef) -> T;
78 fn get_four_state(&self, signal: SignalRef) -> (BigUint, BigUint);
79
80 // ── memory / layout ─────────────────────────────────────────
81 fn memory_as_ptr(&self) -> (*const u8, usize);
82 /// Exposing a retained writable view must permanently disable sparse VCD
83 /// tracking in backend-owned state outside the returned memory range.
84 fn memory_as_mut_ptr(&mut self) -> (*mut u8, usize);
85 /// Return an opaque owner that keeps the memory allocation alive.
86 ///
87 /// Host integrations that expose the raw memory outside Rust can retain
88 /// this value until their external view is finalized. Backends without a
89 /// separately owned stable allocation may keep the default `None`.
90 /// When returning `Some`, the allocation and address reported by
91 /// [`Self::memory_as_ptr`] and [`Self::memory_as_mut_ptr`] must remain valid
92 /// until every clone of the owner has been dropped.
93 fn memory_owner(&self) -> Option<Arc<dyn Any + Send + Sync>> {
94 None
95 }
96 fn runtime_event_buffer_as_ptr(&self) -> (*const u8, usize);
97 fn runtime_event_buffer(&self) -> Option<Arc<RuntimeEventBuffer>> {
98 None
99 }
100 fn set_comb_capture_event_enabled(&mut self, _active_sites: &[bool]) {}
101 fn stable_region_size(&self) -> usize;
102 fn layout(&self) -> &MemoryLayout;
103
104 // ── event enumeration ───────────────────────────────────────
105 fn id_to_addr_slice(&self) -> &[AbsoluteAddr];
106 fn id_to_event_slice(&self) -> &[Self::Event];
107 fn num_events(&self) -> usize;
108
109 // ── trigger bits (for Simulation edge detection) ────────────
110 fn clear_triggered_bits(&mut self);
111 fn mark_triggered_bit(&mut self, id: usize);
112 fn get_triggered_bits(&self) -> bit_set::BitSet;
113
114 /// Opt into generated write notifications only while no raw mutable view
115 /// has escaped. The default keeps other backend implementations correct.
116 fn vcd_tracking_enabled(&self) -> bool {
117 false
118 }
119
120 /// Consume waveform activity independently of clock trigger processing.
121 /// Returns false when full scanning is required (including raw host views).
122 fn take_vcd_activity(&mut self, groups: &mut Vec<usize>) -> bool {
123 groups.clear();
124 if !self.vcd_tracking_enabled() {
125 return false;
126 }
127 let Some(trace) = self.layout().trace.as_ref() else {
128 return false;
129 };
130 let (ptr, size) = self.memory_as_ptr();
131 // SAFETY: &mut self excludes execution/host access during consumption;
132 // the backend memory contract exposes this same writable state image.
133 let memory = unsafe { std::slice::from_raw_parts_mut(ptr.cast_mut(), size) };
134 trace.take(memory, groups);
135 true
136 }
137
138 /// Called by host setters, which share the generated-store notification ABI.
139 fn mark_vcd_signal(&mut self, signal: SignalRef) {
140 if let Some(trace) = self.layout().trace.as_ref() {
141 let len = signal.array_layout.map_or_else(
142 || celox_state_layout::get_byte_size(signal.width),
143 |array| array.plane_size,
144 );
145 let (ptr, _) = self.memory_as_ptr();
146 // SAFETY: layout homes and metadata fit in the writable state image.
147 unsafe {
148 trace.mark_range(ptr.cast_mut(), signal.offset, len);
149 }
150 }
151 }
152}