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