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 /// Run process kernel `index` until it suspends or ends. The kernel
66 /// reports how it returned in its control slots. Backends without
67 /// process kernels reject the call.
68 fn run_process(&mut self, index: usize) -> Result<(), SimulatorErrorCode> {
69 Err(SimulatorErrorCode::Runtime {
70 message: format!("backend cannot run process kernel {index}"),
71 signals: Vec::new(),
72 })
73 }
74
75 // ── signal access ───────────────────────────────────────────
76 fn resolve_signal(&self, addr: &AbsoluteAddr) -> SignalRef;
77 fn resolve_event(&self, addr: &AbsoluteAddr) -> Self::Event;
78 fn resolve_event_opt(&self, addr: &AbsoluteAddr) -> Option<Self::Event>;
79 fn resolve_eval_only_event(&self, addr: &AbsoluteAddr) -> Option<Self::Event>;
80 fn resolve_apply_event(&self, addr: &AbsoluteAddr) -> Option<Self::Event>;
81
82 // ── get / set ───────────────────────────────────────────────
83 fn set<T: Copy>(&mut self, signal: SignalRef, val: T);
84 fn set_wide(&mut self, signal: SignalRef, val: BigUint);
85 fn set_four_state(&mut self, signal: SignalRef, val: BigUint, mask: BigUint);
86 fn get(&self, signal: SignalRef) -> BigUint;
87 fn get_as<T: Default + Copy>(&self, signal: SignalRef) -> T;
88 fn get_four_state(&self, signal: SignalRef) -> (BigUint, BigUint);
89
90 // ── memory / layout ─────────────────────────────────────────
91 fn memory_as_ptr(&self) -> (*const u8, usize);
92 /// Exposing a retained writable view must permanently disable sparse VCD
93 /// tracking in backend-owned state outside the returned memory range.
94 fn memory_as_mut_ptr(&mut self) -> (*mut u8, usize);
95 /// Overwrite `bytes.len()` bytes of the memory image at `offset`.
96 ///
97 /// Unlike [`Self::memory_as_mut_ptr`], this keeps VCD change tracking
98 /// enabled. The write marks no VCD activity, so the caller must have the
99 /// next dump scan every signal.
100 fn write_memory(&mut self, offset: usize, bytes: &[u8]) {
101 let (ptr, len) = self.memory_as_mut_ptr();
102 assert!(offset + bytes.len() <= len, "write past the memory image");
103 // Safety: the backend exposes `len` writable bytes at `ptr`.
104 unsafe { std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr.add(offset), bytes.len()) };
105 }
106
107 /// Return an opaque owner that keeps the memory allocation alive.
108 ///
109 /// Host integrations that expose the raw memory outside Rust can retain
110 /// this value until their external view is finalized. Backends without a
111 /// separately owned stable allocation may keep the default `None`.
112 /// When returning `Some`, the allocation and address reported by
113 /// [`Self::memory_as_ptr`] and [`Self::memory_as_mut_ptr`] must remain valid
114 /// until every clone of the owner has been dropped.
115 fn memory_owner(&self) -> Option<Arc<dyn Any + Send + Sync>> {
116 None
117 }
118 fn runtime_event_buffer_as_ptr(&self) -> (*const u8, usize);
119 fn runtime_event_buffer(&self) -> Option<Arc<RuntimeEventBuffer>> {
120 None
121 }
122 fn set_comb_capture_event_enabled(&mut self, _active_sites: &[bool]) {}
123 fn stable_region_size(&self) -> usize;
124 fn layout(&self) -> &MemoryLayout;
125
126 // ── event enumeration ───────────────────────────────────────
127 fn id_to_addr_slice(&self) -> &[AbsoluteAddr];
128 fn id_to_event_slice(&self) -> &[Self::Event];
129 fn num_events(&self) -> usize;
130
131 // ── trigger bits (for Simulation edge detection) ────────────
132 fn clear_triggered_bits(&mut self);
133 fn mark_triggered_bit(&mut self, id: usize);
134 fn get_triggered_bits(&self) -> bit_set::BitSet;
135
136 /// Opt into generated write notifications only while no raw mutable view
137 /// has escaped. The default keeps other backend implementations correct.
138 fn vcd_tracking_enabled(&self) -> bool {
139 false
140 }
141
142 /// Consume waveform activity independently of clock trigger processing.
143 /// Returns false when full scanning is required (including raw host views).
144 fn take_vcd_activity(&mut self, groups: &mut Vec<usize>) -> bool {
145 groups.clear();
146 if !self.vcd_tracking_enabled() {
147 return false;
148 }
149 let Some(trace) = self.layout().trace.as_ref() else {
150 return false;
151 };
152 let (ptr, size) = self.memory_as_ptr();
153 // SAFETY: &mut self excludes execution/host access during consumption;
154 // the backend memory contract exposes this same writable state image.
155 let memory = unsafe { std::slice::from_raw_parts_mut(ptr.cast_mut(), size) };
156 trace.take(memory, groups);
157 true
158 }
159
160 /// Called by host setters, which share the generated-store notification ABI.
161 fn mark_vcd_signal(&mut self, signal: SignalRef) {
162 if let Some(trace) = self.layout().trace.as_ref() {
163 let len = signal.array_layout.map_or_else(
164 || celox_state_layout::get_byte_size(signal.width),
165 |array| array.plane_size,
166 );
167 let (ptr, _) = self.memory_as_ptr();
168 // SAFETY: layout homes and metadata fit in the writable state image.
169 unsafe {
170 trace.mark_range(ptr.cast_mut(), signal.offset, len);
171 }
172 }
173 }
174}