vj_sim/sched/kernel_adapter.rs
1//! Task S4 (`docs/superpowers/plans/2026-09-26-wave-A2-slice.md`): the ONE
2//! place in this tree a frozen-ABI compiled kernel (`vj_abi::ProcessFn`)
3//! is bridged to `sched::CompiledProcess` -- ledger PR-100, PR-101, PR-102,
4//! PR-103, PR-108, PR-109, PR-110, PR-112, PR-121, CG-083, CG-087, CG-088,
5//! CG-089, CG-106, CG-107, CG-116, CG-164. See `docs/design/process.md` §4
6//! and ARCHITECTURE.md §5/§6.4.
7//!
8//! ## `SV_KERNEL_ABI_RECONCILIATION` (CG-084/CG-111..CG-116 supersession)
9//!
10//! `docs/design/codegen.md` §9's literal `SvKernelTable`/`SvProcessDesc`/
11//! `SvWakeSpec`/`SvResumeStatus` draft is superseded by ARCHITECTURE.md §5's
12//! simpler, frozen ABI (`SimHandle`/`ProcessFn`/`EvalResult`/`Checkpoint`) --
13//! the same reconciliation `docs/ledger.json`'s own `PR-037`/`PR-107` rows
14//! already record for the process-model side. This module closes CG-084 and
15//! CG-111 through CG-116 by implementing the frozen ABI shape below, not the
16//! literal old struct names: [`CompiledKernelProcess`] IS what
17//! `SvKernelTable`'s "resume a compiled process" ask reduces to once
18//! `resume_pc` is a plain function argument instead of a state-arena-resident
19//! discriminant slot (CG-088/CG-089's own reconciliation, applied at the
20//! `vj-codegen::process_lower::lower_kernel_fn` call site that emits the
21//! LLVM side of this same boundary).
22//!
23//! ## PR-121: `RescheduleReq`'s opaque bytes, given real meaning
24//!
25//! [`build_eval_result`]/[`decode_reschedule`] are the only two functions in
26//! this tree that ever read or write `vj_abi::eval::RescheduleReq::opaque`
27//! -- everywhere else treats it as an inert 32-byte blob. They reuse (never
28//! reinvent) `super::reschedule_req_proposal`'s already-tested 24-byte tagged
29//! union, packed into the frozen 32-byte `opaque` array alongside 4 more bytes
30//! (`next_resume_pc`, little-endian) and 4 reserved zero bytes -- the "8 of
31//! the 32 reserved bytes free for a future field" headroom that module's own
32//! doc comment already sized in for.
33//!
34//! ## Known A2-slice scope limit -- CLOSED (A2-part2 review-fix r1)
35//!
36//! The frozen `ProcessFn = extern "C" fn(SimHandle, *mut u8, u32) ->
37//! EvalResult` signature carries no `ProcessId`/`ProcessInstanceId` parameter
38//! at all -- there is nowhere in this ABI boundary for a compiled kernel to
39//! report "which process instance is this" to `nba_enqueue`'s own `source:
40//! u32` parameter, so every `nba_enqueue` call this wave's codegen emits
41//! still passes a constant `0` for `source` (`vj-codegen::process_lower`'s
42//! own doc comment on its NBA lowering repeats this same note at the
43//! call-emission site) -- an ABI-breaking 4th `ProcessFn` parameter remains
44//! out of this task's authority (AB-050 is a cross-track-negotiated
45//! contract). What this review fix closes is the OTHER half this section
46//! used to flag as unbuilt: the runtime-side lookup keyed off "current
47//! process" ambient state. [`CURRENT_PROCESS`]/[`with_current_process`]
48//! below are exactly that -- `sched::resume::ResumeDriver::resume_for`/
49//! `start` (the two places SCHED already knows the real `ProcessId` it is
50//! about to resume) install it for the duration of the `resume()` call, and
51//! [`real_nba_enqueue`] prefers it over the always-`0` raw ABI value. A test
52//! exercising two concurrently-nonblocking-writing process instances in one
53//! time step now exists (`vj-harness`'s
54//! `two_process_nba_same_var_matches_iverilog_bit_for_bit`, against a real
55//! Icarus run) -- see that fixture's own doc comment for why it uses a
56//! causal (not same-event-race) dependency between the two processes: the
57//! LRM leaves same-event multi-process activation order unspecified, and
58//! `commit_nba`'s own hazard resolution (`sched::notify::commit_nba`)
59//! already, deliberately, ties break by push/insertion order rather than by
60//! `ProcessId` (see that function's own doc comment) -- this fix's real
61//! contribution is that `source` is no longer silently dishonest about which
62//! process staged a write, not a change to that already-correct resolution
63//! algorithm.
64//!
65//! **r2 addendum (review fix, A2-part2 r2, finding 3):** the harness test
66//! named above proves `commit_nba`'s push-order resolution, not that
67//! `CURRENT_PROCESS` itself is wired correctly -- `commit_nba` never reads
68//! `source` at all, so that differential test cannot distinguish "wired
69//! correctly" from "absent/always-0". [`tests::
70//! real_nba_enqueue_prefers_current_process_over_the_raw_abi_source`] closes
71//! that gap directly: it calls [`real_nba_enqueue`] with and without
72//! [`with_current_process`] installed and asserts the recorded `source`
73//! differs accordingly, independent of `commit_nba`/push order entirely.
74//!
75//! ## Frame-storage deviation from the plan's literal 3-field struct
76//!
77//! The plan's own struct sketch is `{ kernel, activation: Box<ActivationRecord>,
78//! resume_pc: u32 }`. `ActivationRecord::frame` is (correctly -- see that
79//! field's own doc comment) only a non-owning [`crate::process::ArenaRange`]
80//! *descriptor* into a separate, STATE-owned per-process frame arena; no such
81//! frame-arena allocator exists anywhere in this tree yet (`state::instance::
82//! StateEngine::allocate_instance` allocates the *signal* arena, a different
83//! one, and no other committed code in this wave or an earlier one builds a
84//! frame-word allocator). Since the frozen ABI's `local: *mut u8` parameter
85//! must point at real, live, correctly-sized byte storage for `resume()` to
86//! be callable at all, [`CompiledKernelProcess`] owns that byte buffer
87//! directly (the `frame` field below) until a real frame-arena allocator
88//! lands in a later wave and this adapter is updated to resolve
89//! `activation.frame` against it instead.
90use std::cell::{Cell, RefCell};
91use std::ffi::CStr;
92
93use vj_abi::eval::{EvalOutcome, EvalResult, RescheduleReq};
94use vj_abi::{ProcessFn, SimHandle};
95
96use super::reschedule_req_proposal::{
97 KIND_BY_REQUEST, KIND_DELAY, KIND_DONE, KIND_EDGE, KIND_EDGE_LIST, KIND_FORK_JOIN,
98 KIND_REPEAT_EDGE, KIND_WAIT_FORK, RescheduleReqLayoutProposal,
99};
100use super::{
101 CompiledProcess, EventTerm, EventWaitSet, JoinKind, ProcessId, ProcessRequest, ProcessState,
102 ResumeArg, SchedulerOps, SpawnSpec, Suspend,
103};
104use crate::dpi_task::{CallPoll, ExportRequest, TaskCall};
105use crate::process::{ActivationRecord, ArenaRange, PredicateFn, WaitSpecStorage};
106use crate::state::{EdgeKind, FourStateValue, InstanceHandle, StateCellId};
107
108/// Byte offset, within [`RescheduleReq::opaque`], where this module stashes
109/// the raw `next resume_pc` a caller must pass on its following call --
110/// immediately after the 24-byte [`RescheduleReqLayoutProposal`] (PR-121's
111/// "8 of the 32 reserved bytes free for a future field" headroom).
112const NEXT_RESUME_PC_OFFSET: usize = 24;
113/// Byte offset of the flags word in the `opaque` bytes' reserved tail.
114const FLAGS_OFFSET: usize = 28;
115/// Flag bit: the wait is a `wait(expr)` -- see [`EventWaitSet::rechecks_on_wake`].
116const FLAG_RECHECKS_ON_WAKE: u8 = 1;
117const PROPOSAL_SIZE: usize = 24;
118
119/// The compile-time-resolved suspend shape [`build_eval_result`] encodes.
120/// Deliberately **not** `vj_sim::process::cfg::SuspendPoint` (an
121/// `sv_lang`-carrying, `frontend`-gated type): this module -- and the whole
122/// `sched` module tree -- must build with `--no-default-features` (D4), so
123/// every type crossing this boundary is `sv_lang`-free. `vj-codegen`
124/// resolves its own `SuspendPoint` (a constant delay tick count, or an edge
125/// signal's already-assigned `SlotId`) down to this shape before calling in.
126#[derive(Clone, Copy, Debug)]
127pub enum ResolvedSuspend {
128 Delay(u64),
129 /// A delay computed at run time (`#d`, `#(expr)`): the kernel stores
130 /// the tick count with [`real_sv_rt_set_delay`] just before it returns.
131 DynamicDelay,
132 Edge {
133 cell: StateCellId,
134 edge: EdgeKind,
135 },
136 Done,
137 /// **Task 6 (CG-081/CG-171):** a single-cell, single-edge REPEAT wait --
138 /// `count` occurrences of `edge` on `cell` must fire before the process
139 /// resumes. Closes `SuspendPoint::CycleDelay` (`##N`, always reachable
140 /// via a `Timed` statement -- FULLY resolved by this task) and would
141 /// close `RepeatedEventControl` (`repeat(n) @(...)`, CG-081's own name
142 /// for this kind) too, were that AST shape ever reachable from a `Timed`
143 /// statement at all -- see `vj-codegen::process_lower::
144 /// resolve_suspend_point`'s `CycleDelay` arm and `vj-sim::process::
145 /// cfg::SuspendPoint`'s own doc comment for the full reasoning on both.
146 /// `count` is the RAW, un-multiplied cycle count -- `edge`s of `cell`
147 /// are counted LIVE at scheduler runtime (`EventTerm::Repeat`,
148 /// SC-052/SC-053, already built and tested), never pre-multiplied
149 /// against any period at codegen time (ARCHITECTURE §6.5).
150 RepeatEdge {
151 count: u64,
152 cell: StateCellId,
153 edge: EdgeKind,
154 },
155 /// **Task 7 (AB-096/CG-082):** a single-cell, single-edge wait carrying a
156 /// compiled re-check guard -- the `wait(expr)`/`iff`-qualified shape.
157 /// See [`RescheduleReqLayoutProposal::from_single_edge_with_guard`]'s
158 /// own doc comment for the full ABI-budget reasoning behind reusing
159 /// `a`/`b` instead of a ninth struct field, and for why this is NOT
160 /// extended to `RepeatEdge` in this pass.
161 EdgeWithGuard {
162 cell: StateCellId,
163 edge: EdgeKind,
164 guard_fn: extern "C" fn(SimHandle) -> bool,
165 },
166 /// **R-run Task 17 (PR-047/PR-061/PR-085):** a `fork ... join`
167 /// (`JoinKind::All` only -- this task's own scope) suspend. Unlike
168 /// every other variant above, a fork/join site's real payload (one
169 /// [`super::SpawnSpec`] per branch) is NOT self-contained here -- it is
170 /// compile-time-known (branches are static source, not a per-instance
171 /// runtime count) but too large to fit `RescheduleReqLayoutProposal`'s
172 /// frozen 24-byte budget for more than ~2 branches, so `vj-codegen`
173 /// instead materializes a real, compile-time-constant
174 /// `Vec<SpawnSpec>` fork table (one per compiled program, growing as
175 /// each fork/join site is lowered) and this variant carries only
176 /// `base`/`count`, an [`crate::process::ArenaRange`]-shaped reference
177 /// into it -- exactly [`RescheduleReqLayoutProposal::from_fork_join`]'s
178 /// own, already-specified `KIND_FORK_JOIN` wire shape (`docs/CONTRACTS.md`
179 /// open question 8 / D1a's own "no allocator, no STATE-owned storage"
180 /// concern does not apply here: this table is compiled INTO the
181 /// program as an ordinary Rust `Vec` a `CompiledKernelProcess` is handed
182 /// at construction time -- see [`decode_reschedule_with_fork_table`]).
183 ForkJoin {
184 join_kind: JoinKind,
185 base: u32,
186 count: u32,
187 },
188 /// `wait fork;` (SC-125) -- see `reschedule_req_proposal::KIND_WAIT_FORK`.
189 WaitFork,
190 /// `p.await()` / `p.suspend()` (classes plan Task 13) -- see
191 /// `reschedule_req_proposal::KIND_BY_REQUEST`.
192 ByRequest,
193 /// A wait on several terms (`@*`, `@(a or posedge b)`, `wait(expr)` on a
194 /// wide or multi-signal condition, `@(a)` any-change): the terms live in
195 /// the program's wait table (`[base, base+count)`, see [`WaitTableEntry`]),
196 /// encoded as `KIND_EDGE_LIST`.
197 EventList {
198 base: u32,
199 count: u32,
200 },
201}
202
203/// One term of a multi-term wait, in the program's wait table: the waited-on
204/// signal's TEMPLATE slot (mapped to the waiting instance's cell at run time)
205/// and its edge -- `None` = any value change of any bit.
206#[derive(Clone, Copy, Debug, PartialEq, Eq)]
207pub struct WaitTableEntry {
208 pub slot: u32,
209 pub edge: Option<EdgeKind>,
210 /// `Some((lsb, width))`: the term watches only bits `[lsb, lsb + width)`
211 /// of the signal -- a constant bit/part select (`@(posedge v[1])`, LRM
212 /// 9.4.2) -- and an edge is taken on the select's least significant bit.
213 pub select: Option<(u32, u32)>,
214}
215
216impl WaitTableEntry {
217 /// A term on the whole signal.
218 pub fn whole(slot: u32, edge: Option<EdgeKind>) -> Self {
219 Self {
220 slot,
221 edge,
222 select: None,
223 }
224 }
225}
226
227/// PR-121: builds a real `vj_abi::eval::EvalResult` from a
228/// [`ResolvedSuspend`] and the raw `next_resume_pc` a caller must pass on its
229/// next call for this same process (a plain `u32`, `vj-codegen`'s
230/// `lower_kernel_fn` supplies the already-known target `SegId` here at
231/// codegen time; see [`decode_reschedule`] for the inverse this module gives
232/// the ABI's opaque bytes real meaning through, layered on top of
233/// `RescheduleReqLayoutProposal`'s already-tested 24-byte tagged union rather
234/// than inventing a second encoding).
235pub fn build_eval_result(resolved: ResolvedSuspend, next_resume_pc: u32) -> EvalResult {
236 let (outcome, proposal) = match resolved {
237 ResolvedSuspend::Delay(ticks) => (
238 EvalOutcome::SuspendedOnDelay,
239 RescheduleReqLayoutProposal::from_delay(ticks),
240 ),
241 ResolvedSuspend::DynamicDelay => (
242 EvalOutcome::SuspendedOnDelay,
243 RescheduleReqLayoutProposal::from_delay(DYNAMIC_DELAY_MARK),
244 ),
245 ResolvedSuspend::Edge { cell, edge } => (
246 EvalOutcome::SuspendedOnEvent,
247 RescheduleReqLayoutProposal::from_single_edge(edge, cell),
248 ),
249 ResolvedSuspend::Done => (EvalOutcome::Completed, RescheduleReqLayoutProposal::done()),
250 ResolvedSuspend::RepeatEdge { count, cell, edge } => (
251 EvalOutcome::SuspendedOnEvent,
252 RescheduleReqLayoutProposal::from_repeat_edge(count, edge, cell),
253 ),
254 // Task 7 (AB-096/CG-082).
255 ResolvedSuspend::EdgeWithGuard {
256 cell,
257 edge,
258 guard_fn,
259 } => (
260 EvalOutcome::SuspendedOnEvent,
261 RescheduleReqLayoutProposal::from_single_edge_with_guard(edge, cell, guard_fn),
262 ),
263 // R-run Task 17: `EvalOutcome` is the frozen `vj-abi` 5-variant
264 // enum (Completed/SuspendedOnEvent/SuspendedOnDelay/
265 // MoreWorkInRegion/RuntimeError) -- no dedicated fork/join
266 // discriminant exists in it (out of this task's authority to add
267 // one, ARCHITECTURE §7 item 3), so this reuses `SuspendedOnEvent`,
268 // the same "process suspended waiting on something" outcome
269 // `KIND_EDGE`/`KIND_EDGE_LIST` above already use; `CompiledKernelProcess
270 // ::resume` never reads `outcome` at all (only `decode_reschedule`'s
271 // decode of `reschedule.opaque` matters for real dispatch), so this
272 // choice is a documentation nicety, not load-bearing.
273 ResolvedSuspend::ForkJoin {
274 join_kind,
275 base,
276 count,
277 } => (
278 EvalOutcome::SuspendedOnEvent,
279 RescheduleReqLayoutProposal::from_fork_join(join_kind, ArenaRange { base, count }),
280 ),
281 ResolvedSuspend::WaitFork => (
282 EvalOutcome::SuspendedOnEvent,
283 RescheduleReqLayoutProposal::wait_fork(),
284 ),
285 ResolvedSuspend::ByRequest => (
286 EvalOutcome::SuspendedOnEvent,
287 RescheduleReqLayoutProposal::by_request(),
288 ),
289 ResolvedSuspend::EventList { base, count } => (
290 EvalOutcome::SuspendedOnEvent,
291 RescheduleReqLayoutProposal::from_edge_list(ArenaRange { base, count }),
292 ),
293 };
294
295 let mut opaque = [0u8; vj_abi::eval::RESCHEDULE_REQ_SIZE];
296 // Task 11 (A2-part2; found, not assigned): this used to be an `unsafe {
297 // core::mem::transmute(proposal) }` of the `#[repr(C)]`
298 // `RescheduleReqLayoutProposal` struct directly to `[u8; PROPOSAL_SIZE]`
299 // -- which reads that struct's own compiler-inserted alignment padding
300 // (between `b: u32` and `edge_signal: u64`) as real bytes, and a plain
301 // struct literal never pins padding to a known value, only its named
302 // fields. `RescheduleReqLayoutProposal::to_bytes` (its own doc comment
303 // has the full story, including why a `mem::zeroed()`-based struct
304 // literal fix was tried first and does not work) writes all 24 bytes
305 // itself instead, explicit `0` for the padding's own offsets -- no
306 // `unsafe`, no transmute, no indeterminate byte survives into this
307 // `EvalResult`, which `vj-codegen::process_lower` freezes directly
308 // into a compile-time LLVM IR constant.
309 let proposal_bytes: [u8; PROPOSAL_SIZE] = proposal.to_bytes();
310 opaque[..PROPOSAL_SIZE].copy_from_slice(&proposal_bytes);
311 opaque[NEXT_RESUME_PC_OFFSET..NEXT_RESUME_PC_OFFSET + 4]
312 .copy_from_slice(&next_resume_pc.to_le_bytes());
313 // opaque[28..32] is the flags word (see `mark_rechecks_on_wake`): explicit
314 // zero here, never left as poison.
315
316 EvalResult {
317 outcome,
318 reschedule: RescheduleReq { opaque },
319 }
320}
321
322/// Marks `er` (a `wait(expr)` suspend) as one whose resumed kernel re-checks
323/// its condition, so the scheduler may replay a wake parked while the process
324/// was suspended ([`EventWaitSet::rechecks_on_wake`]). Only the codegen of a
325/// level-sensitive wait calls this.
326pub fn mark_rechecks_on_wake(mut er: EvalResult) -> EvalResult {
327 er.reschedule.opaque[FLAGS_OFFSET] |= FLAG_RECHECKS_ON_WAKE;
328 er
329}
330
331/// Whether [`mark_rechecks_on_wake`] was applied to `er`.
332fn rechecks_on_wake(er: &EvalResult) -> bool {
333 er.reschedule.opaque[FLAGS_OFFSET] & FLAG_RECHECKS_ON_WAKE != 0
334}
335
336/// Review fix (A2-part2 r2, binding rule 8): the typed failure
337/// [`decode_reschedule`] returns instead of panicking for `KIND_EDGE_LIST`/
338/// `KIND_FORK_JOIN`/any out-of-range kind byte.
339///
340/// **Why this stops at a typed error rather than a full decode into
341/// `Suspend::WaitEvents`'s multi-term shape / `Suspend::ForkJoin`, for these
342/// two kinds specifically:** `RescheduleReqLayoutProposal::from_edge_list`/
343/// `from_fork_join`'s own doc comments are explicit that their payload is
344/// only an [`ArenaRange`] *reference* into, respectively, "a static,
345/// elaboration-time-built edge-term table" and "the engine's dynamic
346/// fork-child table" -- never the concrete `StateCellId`s/`SpawnSpec`s
347/// themselves. Per `docs/CONTRACTS.md` open question 8 and resolved decision
348/// D1a, **neither table is materialized by any wave-A track**: no allocator
349/// and no STATE-owned storage exists yet to resolve `base`/`count` against.
350/// Fabricating a `Suspend::WaitEvents`/`Suspend::ForkJoin` from just a range
351/// with no real backing data would be a silently-wrong `Suspend` (exactly
352/// what this decoder's own prior doc comment already refused to do) --
353/// strictly worse than an honest, typed "cannot resolve this yet" error.
354/// This variant carries the range/`join_kind` bytes through unchanged, so
355/// the future task that finally builds those tables has everything it needs
356/// without inventing a second wire format.
357#[derive(Clone, Copy, PartialEq, Eq, Debug)]
358pub enum DecodeRescheduleError {
359 /// `kind == KIND_EDGE_LIST`: a multi-term `Suspend::WaitEvents` whose
360 /// term data lives in the (not-yet-materialized) edge-term table this
361 /// `ArenaRange` would index into.
362 UnresolvedEdgeList { base: u32, count: u32 },
363 /// `kind == KIND_FORK_JOIN`: a `Suspend::ForkJoin` whose `SpawnSpec`s
364 /// live in the (not-yet-materialized) dynamic fork-child table this
365 /// `ArenaRange` would index into. `join_kind` is the raw
366 /// `JoinKind`-discriminant byte (`RescheduleReqLayoutProposal::
367 /// fork_join_kind`), carried through undecoded since decoding it further
368 /// buys nothing without the range it pairs with also being resolvable.
369 UnresolvedForkJoin {
370 join_kind: u8,
371 base: u32,
372 count: u32,
373 },
374 /// Any byte outside `KIND_DELAY..=KIND_DONE` -- an ABI/version mismatch;
375 /// `build_eval_result` (the only producer in this tree) never emits one.
376 InvalidKind(u8),
377}
378
379/// The inverse of [`build_eval_result`]'s suspend-shape half: decodes
380/// `r.reschedule`'s opaque bytes back into the scheduler-facing [`Suspend`].
381/// Every kind byte [`RescheduleReqLayoutProposal`] can hold is named and
382/// handled -- `KIND_EDGE_LIST`/`KIND_FORK_JOIN` and any out-of-range byte
383/// return a typed [`DecodeRescheduleError`] (never a panic: review fix,
384/// A2-part2 r2, binding rule 8) instead of a silently-wrong `Suspend`. See
385/// [`DecodeRescheduleError`]'s own doc comment for why those two kinds stop
386/// at a typed error rather than a full decode.
387pub fn decode_reschedule(r: EvalResult) -> Result<Suspend, DecodeRescheduleError> {
388 let mut proposal_bytes = [0u8; PROPOSAL_SIZE];
389 proposal_bytes.copy_from_slice(&r.reschedule.opaque[..PROPOSAL_SIZE]);
390 // SAFETY: inverse of `build_eval_result`'s transmute above -- every
391 // `EvalResult` this function is ever called with in this tree was itself
392 // produced by that function, so `proposal_bytes` is a valid
393 // `RescheduleReqLayoutProposal` bit pattern (a plain, `#[repr(C)]`,
394 // all-fields-`Copy` struct: no bit pattern of the right size is invalid).
395 let proposal: RescheduleReqLayoutProposal = unsafe { core::mem::transmute(proposal_bytes) };
396 match proposal.kind {
397 KIND_DELAY => {
398 let ticks = u64::from(proposal.a) | (u64::from(proposal.b) << 32);
399 if ticks == DYNAMIC_DELAY_MARK {
400 return Ok(Suspend::Delay(DYNAMIC_DELAY.with(Cell::get)));
401 }
402 Ok(Suspend::Delay(ticks))
403 }
404 // Task 7 (AB-096): `proposal.guard_fn()` is `Some` iff this
405 // `KIND_EDGE` payload was built via `from_single_edge_with_guard`
406 // (a real `wait(expr)`/`iff`-qualified edge) -- see
407 // `sc068_decode_reschedule_can_never_produce_a_some_iff_edge_term`'s
408 // own doc comment for the "documented flip condition" this arm now
409 // satisfies for that specific shape, while a plain `ResolvedSuspend
410 // ::Edge` (a == b == 0) still decodes `iff: None` exactly as before.
411 KIND_EDGE => Ok(Suspend::WaitEvents(EventWaitSet {
412 terms: vec![EventTerm::Edge {
413 cell: StateCellId(proposal.edge_signal),
414 edge: decode_edge_kind_u8(proposal.edge_kind),
415 iff: proposal.guard_fn().map(PredicateFn),
416 }],
417 rechecks_on_wake: rechecks_on_wake(&r),
418 })),
419 KIND_DONE => Ok(Suspend::Done),
420 KIND_WAIT_FORK => Ok(Suspend::WaitFork),
421 KIND_BY_REQUEST => Ok(Suspend::ByRequest),
422 // Task 6 (CG-081/CG-171): a single-cell, single-edge REPEAT wait --
423 // decodes directly into the ALREADY-tested `EventTerm::Repeat`-over-
424 // `Edge` machinery (SC-052/SC-053), unlike `KIND_EDGE_LIST`/
425 // `KIND_FORK_JOIN` below: this kind's whole payload (`edge_kind`/
426 // `edge_signal`/`a`/`b`) is self-contained inline, no out-of-line
427 // table to resolve.
428 KIND_REPEAT_EDGE => Ok(Suspend::WaitEvents(EventWaitSet {
429 terms: vec![EventTerm::Repeat {
430 count: u64::from(proposal.a) | (u64::from(proposal.b) << 32),
431 inner: Box::new(EventTerm::Edge {
432 cell: StateCellId(proposal.edge_signal),
433 edge: decode_edge_kind_u8(proposal.edge_kind),
434 iff: None,
435 }),
436 }],
437 ..Default::default()
438 })),
439 KIND_EDGE_LIST => Err(DecodeRescheduleError::UnresolvedEdgeList {
440 base: proposal.a,
441 count: proposal.b,
442 }),
443 KIND_FORK_JOIN => Err(DecodeRescheduleError::UnresolvedForkJoin {
444 join_kind: proposal.fork_join_kind,
445 base: proposal.a,
446 count: proposal.b,
447 }),
448 other => Err(DecodeRescheduleError::InvalidKind(other)),
449 }
450}
451
452/// R-run Task 17 (PR-047/PR-061/PR-085): the real, resolvable sibling of
453/// [`decode_reschedule`] for a caller that DOES have a materialized fork
454/// table to resolve `KIND_FORK_JOIN`'s `(base, count)` `ArenaRange` against
455/// -- see [`ResolvedSuspend::ForkJoin`]'s own doc comment for why that table
456/// is a plain, compile-time-constant `Vec<SpawnSpec>` (never a runtime
457/// allocator), and [`CompiledKernelProcess`]'s own `fork_table` field for
458/// where a real caller gets one. Every OTHER kind byte delegates verbatim to
459/// [`decode_reschedule`] (never a second, independently-maintained copy of
460/// its decode logic) -- this function only ever adds behavior for the one
461/// kind that function still cannot resolve on its own.
462///
463/// `fork_table[base..base+count]` out of bounds (an internal-invariant
464/// violation -- `vj-codegen` never constructs a `(base, count)` pair
465/// that does not index its own, matching fork table) re-returns the
466/// original typed [`DecodeRescheduleError::UnresolvedForkJoin`] rather than
467/// panicking, so a caller that mismatches a kernel with the wrong fork
468/// table gets a loud, typed failure, never an out-of-bounds panic.
469pub fn decode_reschedule_with_fork_table(
470 r: EvalResult,
471 fork_table: &[SpawnSpec],
472) -> Result<Suspend, DecodeRescheduleError> {
473 decode_reschedule_with_tables(r, fork_table, &[])
474}
475
476/// [`decode_reschedule_with_fork_table`] plus the program's wait table, so a
477/// `KIND_EDGE_LIST` (multi-term wait, [`ResolvedSuspend::EventList`]) also
478/// resolves: each entry becomes an `Edge` term (edged) or an `ImplicitAny`
479/// term (any change), cells still TEMPLATE slots (the kernel process maps
480/// them to its instance).
481pub fn decode_reschedule_with_tables(
482 r: EvalResult,
483 fork_table: &[SpawnSpec],
484 wait_table: &[WaitTableEntry],
485) -> Result<Suspend, DecodeRescheduleError> {
486 match decode_reschedule(r) {
487 Err(DecodeRescheduleError::UnresolvedEdgeList { base, count }) => {
488 let start = base as usize;
489 let end = start.saturating_add(count as usize);
490 let entries = wait_table
491 .get(start..end)
492 .ok_or(DecodeRescheduleError::UnresolvedEdgeList { base, count })?;
493 Ok(Suspend::WaitEvents(EventWaitSet {
494 terms: entries
495 .iter()
496 .map(|e| match (e.select, e.edge) {
497 (Some((lsb, width)), edge) => EventTerm::Select {
498 cell: StateCellId(u64::from(e.slot)),
499 edge: edge.unwrap_or(EdgeKind::None),
500 lsb,
501 width,
502 },
503 (None, Some(edge)) => EventTerm::Edge {
504 cell: StateCellId(u64::from(e.slot)),
505 edge,
506 iff: None,
507 },
508 (None, None) => {
509 EventTerm::ImplicitAny(vec![StateCellId(u64::from(e.slot))])
510 }
511 })
512 .collect(),
513 rechecks_on_wake: rechecks_on_wake(&r),
514 }))
515 }
516 other => decode_fork_join(other, fork_table),
517 }
518}
519
520fn decode_fork_join(
521 decoded: Result<Suspend, DecodeRescheduleError>,
522 fork_table: &[SpawnSpec],
523) -> Result<Suspend, DecodeRescheduleError> {
524 match decoded {
525 Err(DecodeRescheduleError::UnresolvedForkJoin {
526 join_kind,
527 base,
528 count,
529 }) => {
530 let start = base as usize;
531 let end = start.saturating_add(count as usize);
532 let spawns =
533 fork_table
534 .get(start..end)
535 .ok_or(DecodeRescheduleError::UnresolvedForkJoin {
536 join_kind,
537 base,
538 count,
539 })?;
540 Ok(Suspend::ForkJoin(
541 decode_join_kind_u8(join_kind),
542 spawns.to_vec(),
543 ))
544 }
545 other => other,
546 }
547}
548
549/// The inverse of `JoinKind as u8` (used by
550/// [`RescheduleReqLayoutProposal::from_fork_join`]) -- an out-of-range byte
551/// (an ABI/version mismatch this tree never actually produces, since
552/// `JoinKind` has exactly 3 fieldless variants) falls back to `JoinKind::
553/// None`, mirroring [`decode_edge_kind_u8`]'s own identical "out-of-range
554/// falls back to the safest/most conservative variant" convention just
555/// below, rather than panicking on a byte this function cannot itself prove
556/// is unreachable.
557fn decode_join_kind_u8(b: u8) -> JoinKind {
558 if b == JoinKind::All as u8 {
559 JoinKind::All
560 } else if b == JoinKind::Any as u8 {
561 JoinKind::Any
562 } else {
563 JoinKind::None
564 }
565}
566
567fn decode_edge_kind_u8(b: u8) -> EdgeKind {
568 if b == EdgeKind::Posedge as u8 {
569 EdgeKind::Posedge
570 } else if b == EdgeKind::Negedge as u8 {
571 EdgeKind::Negedge
572 } else if b == EdgeKind::BothEdges as u8 {
573 EdgeKind::BothEdges
574 } else {
575 EdgeKind::None
576 }
577}
578
579/// PR-100/PR-101/PR-102/PR-108/PR-109/PR-110: the ONE adapter bridging a
580/// JIT'd/AOT'd `vj_abi::ProcessFn` into `sched::CompiledProcess` -- see
581/// this module's own doc comment for why it also owns its instance's frame
582/// bytes directly (a documented deviation from the plan's literal 3-field
583/// sketch) and for the `source: 0` NBA scope limit.
584///
585/// **Task S7 finding (the `SimHandle` this struct's own `resume()` calls the
586/// kernel with is `self.sim_handle`, NEVER `resume()`'s own `state`
587/// parameter):** verified against the real `Engine` (`sched::control.rs`),
588/// which passes `SimHandle(std::ptr::null_mut())` to every `CompiledProcess::
589/// resume()` call unconditionally (`sched` is deliberately STATE-agnostic --
590/// it dispatches by `ProcessId` against a generic `Box<dyn SignalAccess>`,
591/// never a per-instance `SimHandle`) -- before this task, no committed code
592/// anywhere in this tree ever drove `CompiledKernelProcess` through a real
593/// `Engine`, so this null-handle-always behavior, while real and by design on
594/// SCHED's side, had never actually reached a compiled kernel's own
595/// `sv_get_scalar`/`sv_set_scalar` calls in practice (every existing test
596/// calls `.resume()` directly with a handle it already controls). Since each
597/// `CompiledKernelProcess` is permanently bound to exactly one process
598/// INSTANCE (and therefore exactly one STATE `SimHandle`) for its whole
599/// life, the fix is for this adapter to own and use that handle itself,
600/// ignoring whatever `resume()`'s own `state` parameter happens to carry --
601/// consistent with SCHED's own "generic over `SignalAccess`, no `SimHandle`
602/// concept" design, not a workaround of it.
603/// Set on the parent word of a [`ForkKernel::captures`] pair that copies the
604/// other way: child frame word -> parent frame word, when the child is done
605/// (a recursive task's `output`/`inout` formals).
606pub const RETURN_COPY: u32 = 1 << 31;
607
608/// The words a finished fork child hands its parent: (parent word, value).
609type ReturnBox = std::rc::Rc<RefCell<Option<Vec<(u32, u64)>>>>;
610
611pub struct CompiledKernelProcess {
612 pub kernel: ProcessFn,
613 pub activation: Box<ActivationRecord>,
614 pub resume_pc: u32,
615 frame: Box<[u8]>,
616 sim_handle: SimHandle,
617 /// R-run Task 17 (PR-085): the whole compiled program's fork-child
618 /// table (see [`ResolvedSuspend::ForkJoin`]'s own doc comment) --
619 /// empty (`Arc::from([])`, [`Self::new`]'s default) for every kernel
620 /// that never lowers a fork/join site, in which case
621 /// [`decode_reschedule_with_fork_table`] never actually indexes it (no
622 /// `KIND_FORK_JOIN` byte is ever produced for such a kernel). Shared
623 /// (`Arc`, never a per-instance clone of the whole table) because every
624 /// `CompiledKernelProcess` instance minted from kernels of the SAME
625 /// compiled program indexes the SAME table.
626 fork_table: std::sync::Arc<[SpawnSpec]>,
627 /// The whole compiled program's kernels, indexed by [`SpawnSpec::kernel`]
628 /// (`None` for an index that is not a fork-branch kernel, e.g. a
629 /// top-level process). Shared like `fork_table`; empty for a program
630 /// with no fork/join site. [`CompiledProcess::mint_fork_child`] builds
631 /// each branch's child process from it.
632 fork_kernels: std::sync::Arc<[Option<ForkKernel>]>,
633 /// Whether `sim_handle` is a real STATE `InstanceHandle`, so waited-on
634 /// template slots can be mapped to this instance's cells (see `resume`).
635 map_slots_to_instance: bool,
636 /// The program's multi-term wait table ([`WaitTableEntry`]), shared like
637 /// `fork_table`; empty when no kernel emits a multi-term wait.
638 wait_table: std::sync::Arc<[WaitTableEntry]>,
639 /// The instance and `(resume pc, label, exit pc)` table a `disable` of
640 /// another process finds this one by ([`CompiledProcess::disable_exit`]).
641 disable_instance: Option<u32>,
642 disable_scopes: std::sync::Arc<[(u32, String, u32)]>,
643 /// The last suspend was a fork/join wait ([`CompiledProcess::redirect_resume`]).
644 joined: bool,
645 /// The process ran to its end: nothing is left to disable.
646 finished: bool,
647 /// Every kernel resume runs on a fiber, so that C code of an import task
648 /// it calls can block in an export task (see [`Self::with_task_fibers`]).
649 task_fibers: bool,
650 /// The assertion inputs the process's first run reads
651 /// ([`proc_assert::set_action_inputs`]): the sampled arguments of a task
652 /// spawned from a sequence match item (16.11); taken by that run.
653 start_inputs: Option<Vec<i64>>,
654 /// The kernel call parked on its fiber inside an import task (LRM 35.5.3),
655 /// until the export task it asked for has run.
656 call: Option<TaskCall>,
657 /// Where the fiber leaves the `EvalResult` the kernel returned.
658 fiber_result: std::rc::Rc<Cell<Option<EvalResult>>>,
659 /// What the parked [`Self::call`] is waiting for; read by
660 /// [`CompiledProcess::mint_fork_child`] to start the export task.
661 request: Option<ExportRequest>,
662 /// On an export-task child: the argument image of the import task's
663 /// thunk, which receives the child's frame when the child is done so the
664 /// `output`/`inout` formals reach the C caller.
665 image_back: Option<(usize, usize)>,
666 /// On a fork child: its frame words (child word, parent word) the parent
667 /// gets when the child is done, and where it leaves them.
668 returns: Vec<(u32, u32)>,
669 return_box: Option<ReturnBox>,
670 /// The children this process waits for that return words, copied into
671 /// the frame when the process resumes.
672 pending_returns: RefCell<Vec<ReturnBox>>,
673}
674
675/// One fork-branch kernel a [`CompiledKernelProcess`] can mint a child from:
676/// its entry point and its own frame size (see [`CompiledKernelProcess::new`]).
677#[derive(Clone, Debug)]
678pub struct ForkKernel {
679 pub kernel: ProcessFn,
680 pub frame_word_count: u32,
681 /// The variables the branch shares with the process that forks it:
682 /// (parent frame word, child frame word) of each one's cell pointer,
683 /// copied into the child as it is minted.
684 pub captures: std::sync::Arc<[(u32, u32)]>,
685 /// The branch's `(resume pc, label, exit pc)` disable scopes: a `disable`
686 /// of a block nested in the branch from another process (LRM 9.6.2).
687 pub disable_scopes: std::sync::Arc<[(u32, String, u32)]>,
688}
689
690// SAFETY: `sim_handle` is a `SimHandle` minted by `state::instance::
691// InstanceHandle::into_sim_handle` and never dereferenced by this struct
692// itself (only handed, opaquely, to the compiled kernel fn pointer, exactly
693// like `resume()`'s own `state: SimHandle` parameter that every other
694// `CompiledProcess` implementor already receives across whatever thread
695// `Engine` runs on). `vj-sim`'s own `Engine`/`ProcessTable` already
696// require `CompiledProcess: Send` for every registered process, so a
697// `CompiledKernelProcess` that could never cross a thread boundary would be
698// useless to register at all; this mirrors that same existing, accepted
699// obligation (STATE's `InstanceHandle`/`Arena` are themselves designed to be
700// accessed from whichever thread ENGINE's WOCB-barrier discipline currently
701// grants exclusive access to, never two threads concurrently).
702unsafe impl Send for CompiledKernelProcess {}
703
704impl CompiledKernelProcess {
705 /// `frame_word_count` is `vj-sim::process::liveness::FrameLayout`'s
706 /// own total word count for this process TEMPLATE (each word 8 bytes,
707 /// matching every other word-addressed frame convention in this
708 /// workspace) -- `0` for a process with no declared locals at all (the
709 /// fast-path, suspend-free case), in which case `resume()` passes a null
710 /// `local` pointer, matching `vj-codegen::process_lower`'s own
711 /// fast-path lowering, which never dereferences it. `sim_handle` is this
712 /// process's own, permanently-bound `SimHandle` (see this struct's own
713 /// doc comment for why `resume()` uses it, not its `state` parameter) --
714 /// `SimHandle(std::ptr::null_mut())` is a legal value here too, and is
715 /// exactly what every pre-Task-S7 test in this file already passes.
716 pub fn new(kernel: ProcessFn, frame_word_count: u32, sim_handle: SimHandle) -> Self {
717 let byte_len = frame_word_count as usize * 8;
718 Self {
719 kernel,
720 activation: Box::new(ActivationRecord {
721 pc: 0,
722 frame: ArenaRange {
723 base: 0,
724 count: frame_word_count,
725 },
726 pending_wait: WaitSpecStorage::None,
727 fork_state: None,
728 }),
729 resume_pc: 0,
730 frame: vec![0u8; byte_len].into_boxed_slice(),
731 sim_handle,
732 fork_table: std::sync::Arc::from(Vec::new()),
733 fork_kernels: std::sync::Arc::from(Vec::new()),
734 map_slots_to_instance: false,
735 wait_table: std::sync::Arc::from(Vec::new()),
736 disable_instance: None,
737 disable_scopes: std::sync::Arc::from(Vec::new()),
738 joined: false,
739 finished: false,
740 task_fibers: false,
741 start_inputs: None,
742 call: None,
743 fiber_result: std::rc::Rc::new(Cell::new(None)),
744 request: None,
745 image_back: None,
746 returns: Vec::new(),
747 return_box: None,
748 pending_returns: RefCell::new(Vec::new()),
749 }
750 }
751
752 /// Runs every resume of the kernel on a fiber (LRM 35.5.3, 35.7): when C
753 /// code of an import task calls an export task which consumes time, the
754 /// fiber parks with the kernel's whole stack, this process becomes the
755 /// parent of the export task's process (`Suspend::ForkJoin`), and the
756 /// kernel continues where it was once that process is done. Only for a
757 /// design with a timed export task; any other pays nothing. The children
758 /// this process forks inherit it.
759 pub fn with_task_fibers(mut self) -> Self {
760 self.task_fibers = true;
761 self
762 }
763
764 /// Makes this process reachable by a `disable <label>;` of the other
765 /// processes of `instance` (LRM 9.6.2): while it is suspended and will
766 /// resume at `pc`, a disable of `label` resumes it at the exit `exit` of
767 /// that block or task activation instead, for each `(pc, label, exit)`.
768 pub fn with_disable_scopes(
769 mut self,
770 instance: u32,
771 scopes: std::sync::Arc<[(u32, String, u32)]>,
772 ) -> Self {
773 self.disable_instance = Some(instance);
774 self.disable_scopes = scopes;
775 self
776 }
777
778 /// The values the process's first run reads as its assertion inputs.
779 pub fn with_start_inputs(mut self, inputs: Vec<i64>) -> Self {
780 self.start_inputs = Some(inputs);
781 self
782 }
783
784 /// Installs the program's multi-term wait table (see [`WaitTableEntry`]).
785 pub fn with_wait_table(mut self, wait_table: std::sync::Arc<[WaitTableEntry]>) -> Self {
786 self.wait_table = wait_table;
787 self
788 }
789
790 /// Declares `sim_handle` a real STATE `InstanceHandle` (what `vj
791 /// run` always passes): the template slots a kernel waits on are then
792 /// mapped to this instance's own cells. Must not be set for a handle
793 /// that points at anything else (e.g. a unit test's mock state).
794 pub fn with_instance_slot_mapping(mut self) -> Self {
795 self.map_slots_to_instance = true;
796 self
797 }
798
799 /// R-run Task 17 (PR-085): a builder-style setter installing this
800 /// instance's own fork-child table -- see [`Self::fork_table`]'s own
801 /// doc comment. A real caller (e.g. a fork-child minter closure built
802 /// from `vj-codegen`'s own emitted table) calls this once, right
803 /// after [`Self::new`], for every kernel drawn from a compiled program
804 /// that contains at least one fork/join site (harmless to call for one
805 /// that does not -- the table is simply never consulted).
806 pub fn with_fork_table(mut self, fork_table: std::sync::Arc<[SpawnSpec]>) -> Self {
807 self.fork_table = fork_table;
808 self
809 }
810
811 /// Installs the program's fork-branch kernels (see [`Self::fork_kernels`]).
812 pub fn with_fork_kernels(mut self, fork_kernels: std::sync::Arc<[Option<ForkKernel>]>) -> Self {
813 self.fork_kernels = fork_kernels;
814 self
815 }
816
817 /// Task 7 (PR-116, XC-044): a read-only view of this instance's own
818 /// permanent frame bytes, exactly as `resume()` hands them to the
819 /// compiled kernel's `local` parameter. Needed so a test can prove a
820 /// declared local that the SV source never reads back (PR-017/PR-018's
821 /// "every declared local gets a permanent slot regardless of use," the
822 /// one claim no SV-visible read/write can observe on its own) still had
823 /// its pre-suspend write survive, byte-for-byte, across a real
824 /// suspend/resume round trip -- `activation`/`resume_pc` are already
825 /// `pub` for the identical reason (direct internal-state inspection by
826 /// a test, never by production scheduler code).
827 pub fn frame_bytes(&self) -> &[u8] {
828 &self.frame
829 }
830}
831
832// PR-101/PR-102's ownership-boundary acceptance: this is the ONLY place a
833// compiled kernel is bridged into a `CompiledProcess` -- see
834// `exactly_one_compiledprocess_impl_over_a_kernel_backed_type` (this
835// module's own test, below) for the structural proof and its documented
836// carve-out for `sched::control::UnmintedForkChild` (a pre-existing,
837// unrelated fork/join placeholder, not a kernel adapter).
838impl CompiledProcess for CompiledKernelProcess {
839 /// A fork branch runs in the parent's own instance (same `SimHandle`) and
840 /// shares the program's fork tables, so a nested `fork` inside a branch
841 /// mints its own children the same way.
842 fn mint_fork_child(&self, spec: &SpawnSpec) -> Option<Box<dyn CompiledProcess>> {
843 let fk = self.fork_kernels.get(spec.kernel as usize)?.clone()?;
844 let mut child = CompiledKernelProcess::new(fk.kernel, fk.frame_word_count, self.sim_handle)
845 .with_fork_table(self.fork_table.clone())
846 .with_fork_kernels(self.fork_kernels.clone())
847 .with_wait_table(self.wait_table.clone());
848 child.map_slots_to_instance = self.map_slots_to_instance;
849 child.task_fibers = self.task_fibers;
850 // A timed export task, asked for by the import task parked in this
851 // process: it runs in the instance of the C code's `svScope`, with the
852 // thunk's argument image as its frame, which gets the frame back when
853 // it is done.
854 if let Some(req) = self.request
855 && let Some(row) = self.fork_table.get(req.export as usize)
856 && row.kernel == spec.kernel
857 && row.entry_pc == spec.entry_pc
858 {
859 if !req.handle.is_null() {
860 child.sim_handle = SimHandle(req.handle);
861 }
862 let n = child.frame.len();
863 // SAFETY: `req.args` points at the image on the parked fiber's
864 // stack, `frame_words` words (the thunk sized it from the same
865 // kernel); it is alive while the request is outstanding.
866 unsafe {
867 std::ptr::copy_nonoverlapping(req.args, child.frame.as_mut_ptr(), n);
868 }
869 child.image_back = Some((req.args as usize, n));
870 }
871 // The branch runs in the parent's instance, so a `disable` of a block
872 // nested in it finds the child like any other process there.
873 child.disable_instance = self.disable_instance;
874 child.disable_scopes = fk.disable_scopes.clone();
875 // The variables it shares with this process: their cell pointers.
876 for &(p, c) in fk.captures.iter() {
877 if p & RETURN_COPY != 0 {
878 child.returns.push((c, p & !RETURN_COPY));
879 continue;
880 }
881 let (p, c) = (p as usize * 8, c as usize * 8);
882 if p + 8 <= self.frame.len() && c + 8 <= child.frame.len() {
883 child.frame[c..c + 8].copy_from_slice(&self.frame[p..p + 8]);
884 }
885 }
886 if !child.returns.is_empty() {
887 let bx: ReturnBox = std::rc::Rc::new(RefCell::new(None));
888 child.return_box = Some(bx.clone());
889 self.pending_returns.borrow_mut().push(bx);
890 }
891 Some(Box::new(child))
892 }
893
894 fn disable_instance(&self) -> Option<u32> {
895 self.disable_instance
896 }
897
898 fn disable_exit(&self, label: &str) -> Option<u32> {
899 if self.finished {
900 return None;
901 }
902 if let Some(call) = &self.call {
903 // LRM 35.9: `disable t;` of the import task this process is
904 // blocked in (its C call is parked on an export task) ends the
905 // call; the process resumes where it is, right after the call
906 // returns ([`Self::redirect_resume`]).
907 return (self.request.is_some() && call.import_name() == label)
908 .then_some(self.resume_pc);
909 }
910 self.disable_scopes
911 .iter()
912 .find(|(pc, l, _)| *pc == self.resume_pc && l == label)
913 .map(|(_, _, exit)| *exit)
914 }
915
916 fn owns_disable_scope(&self, label: &str) -> bool {
917 self.disable_scopes.iter().any(|(_, l, _)| l == label)
918 }
919
920 fn redirect_resume(&mut self, pc: u32) -> bool {
921 if let Some(call) = self.call.as_mut() {
922 // The parked import call sees the disable (C: `svIsDisabledState`,
923 // return 1) at its next poll; the export task it waits for is
924 // killed with the rest of the joined children.
925 call.disable();
926 self.request = None;
927 return std::mem::take(&mut self.joined);
928 }
929 self.resume_pc = pc;
930 self.activation.pc = pc;
931 std::mem::take(&mut self.joined)
932 }
933
934 fn resume(&mut self, _state: SimHandle, arg: ResumeArg, ops: &dyn SchedulerOps) -> Suspend {
935 // Resuming from an event control or `wait`, or being disabled, is a
936 // flush point for the process's pending deferred assertion reports
937 // (16.4.2, `sched::deferred`); a delay is not (`#0` stays in the same
938 // time step's Active/Inactive iteration, before any Observed region).
939 if matches!(
940 arg,
941 ResumeArg::EventFired { .. } | ResumeArg::WaitOrderSatisfied | ResumeArg::Disabled
942 ) && let Some(proc) = current_process()
943 {
944 crate::sched::deferred::flush_deferred(proc);
945 }
946 // ... and, when it resumes from an event control or `wait` (not from
947 // a delay), for its pending procedural concurrent assertion instances
948 // (16.14.6.2).
949 if !matches!(arg, ResumeArg::Start | ResumeArg::TimeElapsed)
950 && let Some(proc) = current_process()
951 {
952 crate::sched::proc_assert::flush(proc);
953 }
954 // Only an event control or `wait` resumption (or an `always_comb`
955 // triggered again, which resumes the same way) is a violation report
956 // flush point (12.4.2.1); a delay or a fork join is not.
957 if matches!(
958 arg,
959 ResumeArg::EventFired { .. } | ResumeArg::WaitOrderSatisfied
960 ) && let Some(proc) = current_process()
961 {
962 crate::sched::violation::flush_violation_reports(proc);
963 }
964 if let Some(inputs) = self.start_inputs.take() {
965 crate::sched::proc_assert::set_action_inputs(inputs);
966 }
967 ABORTED.with(|c| c.set(false));
968 let mask = current_process().map_or(0, take_fired);
969 FIRED_MASK.with(|c| c.set(mask));
970 FIRED_TERM.with(|c| {
971 c.set(match arg {
972 ResumeArg::EventFired { term_index } => term_index as i64,
973 _ => -1,
974 })
975 });
976 // The results the children joined since the last run returned.
977 if !self.pending_returns.get_mut().is_empty() {
978 let frame = &mut self.frame;
979 self.pending_returns.get_mut().retain(|bx| {
980 let Some(words) = bx.borrow_mut().take() else {
981 return true;
982 };
983 for (p, v) in words {
984 let at = p as usize * 8;
985 if at + 8 <= frame.len() {
986 frame[at..at + 8].copy_from_slice(&v.to_le_bytes());
987 }
988 }
989 false
990 });
991 }
992 let local_ptr = if self.frame.is_empty() {
993 std::ptr::null_mut()
994 } else {
995 self.frame.as_mut_ptr()
996 };
997 // Task S7 finding: `self.sim_handle`, never `resume()`'s own `state`
998 // parameter -- see this struct's own doc comment.
999 //
1000 // Task S7 (the end-to-end pipeline): `ops` is made reachable, for
1001 // exactly the duration of this raw kernel call, to `nba_enqueue`'s/
1002 // `sv_rt_finish`'s real production bodies below via `CURRENT_OPS` --
1003 // see that thread-local's own doc comment for why this is the one
1004 // sound hook point (the frozen `ProcessFn` ABI itself carries no
1005 // scheduler-ops parameter, and `SimHandle` reaches only STATE).
1006 let handle = self.sim_handle;
1007 let result: EvalResult = if self.task_fibers {
1008 if self.call.is_none() {
1009 let (kernel, pc, out) = (self.kernel, self.resume_pc, self.fiber_result.clone());
1010 let local = local_ptr as usize;
1011 self.call = TaskCall::new(move || {
1012 out.set(Some(kernel(handle, local as *mut _, pc)));
1013 0
1014 });
1015 assert!(
1016 self.call.is_some(),
1017 "no stack could be mapped for a process fiber"
1018 );
1019 }
1020 let call = self.call.as_mut().expect("created above");
1021 match with_scheduler_ops(ops, || call.poll()) {
1022 CallPoll::Finished(_) => {
1023 self.call = None;
1024 self.request = None;
1025 self.fiber_result
1026 .take()
1027 .expect("a finished process fiber left no result")
1028 }
1029 CallPoll::Request(req) => {
1030 // The C code of an import task wants an export task run:
1031 // wait for it as the parent of its process.
1032 let row = self.fork_table.get(req.export as usize).unwrap_or_else(|| {
1033 panic!(
1034 "export task {} has no fork-table row (internal)",
1035 req.export
1036 )
1037 });
1038 let spec = row.clone();
1039 self.request = Some(req);
1040 self.joined = true;
1041 return Suspend::ForkJoin(JoinKind::All, vec![spec]);
1042 }
1043 }
1044 } else {
1045 with_scheduler_ops(ops, || (self.kernel)(handle, local_ptr, self.resume_pc))
1046 };
1047
1048 let mut pc_bytes = [0u8; 4];
1049 pc_bytes.copy_from_slice(
1050 &result.reschedule.opaque[NEXT_RESUME_PC_OFFSET..NEXT_RESUME_PC_OFFSET + 4],
1051 );
1052 self.resume_pc = u32::from_le_bytes(pc_bytes);
1053 self.activation.pc = self.resume_pc;
1054
1055 // `CompiledProcess::resume` (owned by the **sched** track,
1056 // `docs/CONTRACTS.md`'s `vj-sim::sched` ownership table) is
1057 // frozen to return `Suspend`, not `Result` -- changing that trait's
1058 // signature is out of this task's authority (integration rules:
1059 // "do not change a contract owned by another track except
1060 // minimally"). `decode_reschedule`'s `Err` arms are exactly the
1061 // kinds `build_eval_result` above (the only producer of the bytes
1062 // this call ever decodes) never emits -- see `DecodeRescheduleError`'s
1063 // own doc comment -- so this `unreachable!` documents a real,
1064 // checked invariant of THIS call site, not a masked bug; it is
1065 // `decode_reschedule` itself (usable independently, e.g. by a future
1066 // caller that constructs its own `EvalResult`) that must never
1067 // panic, and now never does.
1068 // R-run Task 17: routes through `decode_reschedule_with_fork_table`
1069 // (never the plain `decode_reschedule` directly any more) so a
1070 // `KIND_FORK_JOIN` byte -- now a real, producible encoding, see
1071 // `ResolvedSuspend::ForkJoin` -- resolves against this instance's
1072 // own `self.fork_table` instead of always failing with
1073 // `UnresolvedForkJoin`. For every kernel with no fork/join site,
1074 // `self.fork_table` is empty and this call behaves identically to
1075 // the old direct `decode_reschedule` call (that kind byte is simply
1076 // never produced for such a kernel).
1077 let suspend = decode_reschedule_with_tables(result, &self.fork_table, &self.wait_table)
1078 .unwrap_or_else(|e| {
1079 unreachable!(
1080 "CompiledKernelProcess::resume: decode_reschedule_with_fork_table returned \
1081 {e:?}, but build_eval_result (the only producer of RescheduleReq bytes in \
1082 this tree) never constructs an EdgeList/invalid kind, and a real \
1083 KIND_FORK_JOIN's (base,count) always indexes this same instance's own \
1084 fork_table (an internal invariant of how vj-codegen pairs a kernel \
1085 with its program's fork table -- see CompiledKernelProcess::with_fork_table)"
1086 )
1087 });
1088 self.joined = matches!(
1089 suspend,
1090 Suspend::ForkJoin(JoinKind::All | JoinKind::Any, _)
1091 | Suspend::WaitFork
1092 | Suspend::WaitOrder(_)
1093 );
1094 self.finished = matches!(suspend, Suspend::Done);
1095 if self.finished
1096 && let Some(bx) = self.return_box.take()
1097 {
1098 let words = self
1099 .returns
1100 .iter()
1101 .map(|&(c, p)| {
1102 let at = c as usize * 8;
1103 let mut w = [0u8; 8];
1104 w.copy_from_slice(&self.frame[at..at + 8]);
1105 (p, u64::from_le_bytes(w))
1106 })
1107 .collect();
1108 *bx.borrow_mut() = Some(words);
1109 }
1110 if self.finished
1111 && let Some((image, n)) = self.image_back.take()
1112 {
1113 // SAFETY: see `mint_fork_child`; the importing fiber is still
1114 // parked on that image.
1115 unsafe {
1116 std::ptr::copy_nonoverlapping(self.frame.as_ptr(), image as *mut u8, n);
1117 }
1118 }
1119 // Codegen encodes a waited-on signal by its TEMPLATE slot (the same
1120 // kernel serves every instance of its module); map it to THIS
1121 // instance's absolute cell here. Without this, every instance waited
1122 // on the first instance's signal (e.g. a second counter instance
1123 // clocked by its own `en` followed the first's `clk`). Only for a
1124 // handle that really is a STATE `InstanceHandle`
1125 // (`with_instance_slot_mapping`, set by `vj run`); unit-test
1126 // kernels run against mock state and keep their cells as given.
1127 if !self.map_slots_to_instance {
1128 return suspend;
1129 }
1130 match suspend {
1131 Suspend::WaitEvents(mut set) => {
1132 for term in &mut set.terms {
1133 remap_term_to_instance(term, self.sim_handle);
1134 }
1135 Suspend::WaitEvents(set)
1136 }
1137 other => other,
1138 }
1139 }
1140}
1141
1142/// See the remapping note in `CompiledKernelProcess::resume`.
1143fn remap_term_to_instance(term: &mut EventTerm, handle: SimHandle) {
1144 let map = |c: &mut StateCellId| {
1145 // The shared-variables and object-state cells are no instance's slot.
1146 if *c == OBJECT_STATE_CELL {
1147 OBJECT_STATE_WATCHED.store(true, std::sync::atomic::Ordering::Relaxed);
1148 } else if *c != SHARED_VARS_CELL {
1149 *c = crate::state::cell_for_slot(handle, vj_abi::SlotId(c.0 as u32));
1150 }
1151 };
1152 match term {
1153 EventTerm::Edge { cell, .. } | EventTerm::Select { cell, .. } => map(cell),
1154 EventTerm::ImplicitAny(cells) => cells.iter_mut().for_each(map),
1155 EventTerm::NamedEventTrigger(cell) => map(cell),
1156 EventTerm::Repeat { inner, .. } => remap_term_to_instance(inner, handle),
1157 }
1158}
1159
1160/// The delay-ticks value marking a [`ResolvedSuspend::DynamicDelay`]: the
1161/// real value is the one the kernel last stored with
1162/// [`real_sv_rt_set_delay`] (the kernel call and its decode run back to back
1163/// on one thread).
1164const DYNAMIC_DELAY_MARK: u64 = u64::MAX;
1165
1166thread_local! {
1167 static DYNAMIC_DELAY: Cell<u64> = const { Cell::new(0) };
1168}
1169
1170/// `void sv_rt_set_delay(u64 ticks)`: the tick count of the run-time delay
1171/// the calling kernel is about to suspend on.
1172#[cfg_attr(feature = "aot-exports", unsafe(export_name = "sv_rt_set_delay"))]
1173pub extern "C" fn real_sv_rt_set_delay(ticks: u64) {
1174 DYNAMIC_DELAY.with(|c| c.set(ticks));
1175}
1176
1177thread_local! {
1178 /// The index of the wait term that woke the running kernel (-1 if it
1179 /// was not woken by an event).
1180 static FIRED_TERM: Cell<i64> = const { Cell::new(-1) };
1181 /// Terms (bit k = term k, k < 64) that matched a process's wait since it
1182 /// last resumed, by process; moved to [`FIRED_MASK`] at resume.
1183 /// Indexed by process id (dense, from `register_process`): one load and
1184 /// store per matched term, no hashing, on the wake hot path.
1185 static FIRED_MASKS: std::cell::RefCell<Vec<u64>> = const { std::cell::RefCell::new(Vec::new()) };
1186 /// The resuming kernel's matched-term set (`sv_rt_fired_mask`).
1187 static FIRED_MASK: Cell<u64> = const { Cell::new(0) };
1188 /// Whether the running process ran `$finish`/`$stop`/`$fatal` (directly,
1189 /// in a subroutine, or as a null virtual interface's fatal error).
1190 static ABORTED: Cell<bool> = const { Cell::new(false) };
1191}
1192
1193thread_local! {
1194 /// The cells holding variables a process shares with its fork branches
1195 /// (`sv_rt_cell_alloc`), kept for the whole run.
1196 static CELLS: std::cell::RefCell<Vec<Box<[u64]>>> = const { std::cell::RefCell::new(Vec::new()) };
1197}
1198
1199/// `ptr sv_rt_cell_alloc(u64 bytes)`: a new zeroed cell for a variable a
1200/// process shares with its fork branches (LRM 1800-2023 §9.3.2: the forked
1201/// processes use the forking activation's own variable). One per creation
1202/// of such a variable, alive until the run ends.
1203#[cfg_attr(feature = "aot-exports", unsafe(export_name = "sv_rt_cell_alloc"))]
1204pub extern "C" fn real_sv_rt_cell_alloc(bytes: u64) -> *mut u8 {
1205 let words = (bytes as usize).div_ceil(8).max(1);
1206 let mut cell: Box<[u64]> = vec![0u64; words].into_boxed_slice();
1207 let ptr = cell.as_mut_ptr().cast::<u8>();
1208 CELLS.with(|c| c.borrow_mut().push(cell));
1209 ptr
1210}
1211
1212/// The template slot a wait encodes for "a variable shared between
1213/// processes changed" (`SHARED_VARS_CELL`; never a real STATE slot).
1214pub const SHARED_VARS_SLOT: u32 = u32::MAX;
1215
1216/// The cell a process waiting on a variable it shares with other processes
1217/// subscribes to: [`real_sv_rt_cell_store`] notifies it when such a variable
1218/// changes (the waiter's guard then re-checks its condition).
1219pub const SHARED_VARS_CELL: crate::state::StateCellId =
1220 crate::state::StateCellId(SHARED_VARS_SLOT as u64);
1221
1222/// The template slot a wait encodes for "class-object state changed": a
1223/// `wait (...)` condition that reads a class property, calls a method of a
1224/// class, or calls `weak_reference::get` / `mailbox::num` reads heap state, and
1225/// a heap change is not a signal change. [`notify_object_state_change`] wakes
1226/// the waiters on it (the resume guard re-checks the condition). Never a real
1227/// STATE slot, like [`SHARED_VARS_SLOT`].
1228pub const OBJECT_STATE_SLOT: u32 = u32::MAX - 1;
1229
1230/// The cell a wait on class-object state subscribes to (see
1231/// [`OBJECT_STATE_SLOT`]).
1232pub const OBJECT_STATE_CELL: crate::state::StateCellId =
1233 crate::state::StateCellId(OBJECT_STATE_SLOT as u64);
1234
1235/// Whether any process has ever suspended on [`OBJECT_STATE_CELL`] in this
1236/// run. Heap writes are frequent; until a wait on object state exists the
1237/// notification is one relaxed load. Never cleared (a later wait may come back).
1238static OBJECT_STATE_WATCHED: std::sync::atomic::AtomicBool =
1239 std::sync::atomic::AtomicBool::new(false);
1240
1241/// Marks object state as watched by a process that is statically sensitive to
1242/// [`OBJECT_STATE_CELL`] (a class covergroup's clocking-event sampler, LRM 19.4)
1243/// rather than suspended on it, so heap writes notify it (see
1244/// [`OBJECT_STATE_WATCHED`]).
1245pub fn watch_object_state() {
1246 OBJECT_STATE_WATCHED.store(true, std::sync::atomic::Ordering::Relaxed);
1247}
1248
1249/// Whether any process has suspended on [`OBJECT_STATE_CELL`] (see
1250/// [`OBJECT_STATE_WATCHED`]); the scheduler's own notification sites (a process
1251/// ending is object state for `wait (p.status() == process::FINISHED)`) test it
1252/// before building a notify sink.
1253pub(crate) fn object_state_watched() -> bool {
1254 OBJECT_STATE_WATCHED.load(std::sync::atomic::Ordering::Relaxed)
1255}
1256
1257/// A class object's observable state changed: a property was written, an
1258/// object was freed (a `weak_reference` to it reads null now), or a mailbox's
1259/// queue changed. Wakes the processes waiting on object state so they re-check
1260/// their conditions. Callers must not hold the objects/mailbox table lock.
1261/// Outside a running kernel (unit tests of the tables) there is nothing to wake.
1262pub fn notify_object_state_change() {
1263 if OBJECT_STATE_WATCHED.load(std::sync::atomic::Ordering::Relaxed) {
1264 with_current_ops(|ops| ops.notify_blocking_row_change(OBJECT_STATE_CELL));
1265 }
1266}
1267
1268/// `void sv_rt_cell_store(ptr cell, ptr value, u64 bytes)`: stores a shared
1269/// variable's value into its cell, waking the processes waiting on shared
1270/// variables if it changed.
1271///
1272/// # Safety
1273/// `cell` and `value` each point to at least `bytes` valid bytes.
1274#[cfg_attr(feature = "aot-exports", unsafe(export_name = "sv_rt_cell_store"))]
1275pub unsafe extern "C" fn real_sv_rt_cell_store(cell: *mut u8, value: *const u8, bytes: u64) {
1276 // SAFETY: the caller's contract.
1277 let (to, from) = unsafe {
1278 (
1279 std::slice::from_raw_parts_mut(cell, bytes as usize),
1280 std::slice::from_raw_parts(value, bytes as usize),
1281 )
1282 };
1283 if to != from {
1284 to.copy_from_slice(from);
1285 with_current_ops(|ops| ops.notify_blocking_row_change(SHARED_VARS_CELL));
1286 }
1287}
1288
1289thread_local! {
1290 /// The value each event-control term on a shared variable last saw:
1291 /// (waiting frame, site) -> its bits.
1292 static EDGE_SNAPSHOTS: std::cell::RefCell<std::collections::HashMap<(u64, u64), Vec<u8>>> =
1293 std::cell::RefCell::new(std::collections::HashMap::new());
1294}
1295
1296/// [`ELEM_WAITS`]'s table: process -> `(site, array cell, row)` rows.
1297type ElemWaits = std::collections::HashMap<ProcessId, Vec<(u64, StateCellId, u64)>>;
1298
1299thread_local! {
1300 /// Which element of which event array each process waits on
1301 /// (LRM 1800-2023 37.27 detail 1, `vpiWaitingProcesses` of an array
1302 /// element): process -> `(snapshot site, array cell, storage row)`,
1303 /// recorded by the element-term snapshots of its suspend. The scheduler
1304 /// wakes every waiter on the array's one cell; this is the only record
1305 /// of which element each one waits on. A snapshot of another suspend
1306 /// point (the site's high bits) replaces the process's older rows, and a
1307 /// reader intersects with the scheduler's waiters, so a process that has
1308 /// moved on is never reported.
1309 static ELEM_WAITS: std::cell::RefCell<ElemWaits> =
1310 std::cell::RefCell::new(std::collections::HashMap::new());
1311}
1312
1313/// Bit of an event-array element term's snapshot site that marks the snapshot
1314/// as carrying `[row, valid, count, slot, handle]` (40 bytes) rather than a
1315/// bare value: see [`ELEM_WAITS`].
1316pub const ELEM_SITE_BIT: u64 = 1 << 62;
1317
1318/// The processes now waiting on row `row` of the event array in `cell`
1319/// (a subset of the cell's waiters; see [`ELEM_WAITS`]).
1320pub fn event_elem_waiters(cell: StateCellId, row: u64) -> Vec<ProcessId> {
1321 ELEM_WAITS.with(|m| {
1322 let mut v: Vec<ProcessId> = m
1323 .borrow()
1324 .iter()
1325 .filter(|(_, rows)| rows.iter().any(|&(_, c, r)| c == cell && r == row))
1326 .map(|(p, _)| *p)
1327 .collect();
1328 v.sort_unstable();
1329 v
1330 })
1331}
1332
1333/// The `EDGE_SNAPSHOTS` key of (`frame`, `site`). A process whose frame is
1334/// empty gets a null `frame` (`CompiledKernelProcess::resume`), which every
1335/// such process shares: it is keyed by the running process instead (the high
1336/// bit keeps that id apart from any real frame address).
1337fn snapshot_key(frame: u64, site: u64) -> (u64, u64) {
1338 match frame {
1339 0 => (
1340 current_process().map_or(0, |p| (1 << 63) | u64::from(p)),
1341 site,
1342 ),
1343 _ => (frame, site),
1344 }
1345}
1346
1347/// `void sv_rt_edge_snapshot(u64 frame, u64 site, ptr value, u64 bytes)`:
1348/// records the value an event control on a shared variable waits to see
1349/// change (its process's `frame`, the term's `site`).
1350///
1351/// # Safety
1352/// `value` points to at least `bytes` valid bytes.
1353#[cfg_attr(feature = "aot-exports", unsafe(export_name = "sv_rt_edge_snapshot"))]
1354pub unsafe extern "C" fn real_sv_rt_edge_snapshot(
1355 frame: u64,
1356 site: u64,
1357 value: *const u8,
1358 bytes: u64,
1359) {
1360 // SAFETY: the caller's contract.
1361 let v = unsafe { std::slice::from_raw_parts(value, bytes as usize) }.to_vec();
1362 if site & ELEM_SITE_BIT != 0
1363 && bytes == 40
1364 && let Some(proc) = current_process()
1365 {
1366 let word = |i: usize| u64::from_le_bytes(v[i * 8..i * 8 + 8].try_into().unwrap());
1367 // Same suspend point (site bits above the term index): the wait
1368 // being re-entered replaces its own rows; another one starts over.
1369 let wait_of = |site: u64| site >> 16;
1370 ELEM_WAITS.with(|m| {
1371 let mut m = m.borrow_mut();
1372 let rows = m.entry(proc).or_default();
1373 rows.retain(|&(s, ..)| wait_of(s) == wait_of(site) && s != site);
1374 if word(1) != 0 {
1375 // SAFETY: word 4 is the process's own `SimHandle`, which the
1376 // snapshot's emitter loaded from the accessor context.
1377 let handle = SimHandle(word(4) as usize as *mut std::ffi::c_void);
1378 let cell = crate::state::cell_for_slot(handle, vj_abi::SlotId(word(3) as u32));
1379 rows.push((site, cell, word(0)));
1380 }
1381 });
1382 }
1383 EDGE_SNAPSHOTS.with(|m| m.borrow_mut().insert(snapshot_key(frame, site), v));
1384}
1385
1386/// `i32 sv_rt_edge_check(u64 frame, u64 site, ptr value, u64 bytes, u32
1387/// width, u32 edge)`: whether the shared variable now holding `value` (a
1388/// packed 4-state value of `width` bits: values in bits 0.., unknown flags
1389/// from bit `width`) has made the event `edge` (0 any change, 1 posedge, 2
1390/// negedge, 3 either edge -- LRM 1800-2023 Table 9-2, on the least
1391/// significant bit) since the snapshot, which is then moved to `value`.
1392///
1393/// # Safety
1394/// `value` points to at least `bytes` valid bytes.
1395#[cfg_attr(feature = "aot-exports", unsafe(export_name = "sv_rt_edge_check"))]
1396pub unsafe extern "C" fn real_sv_rt_edge_check(
1397 frame: u64,
1398 site: u64,
1399 value: *const u8,
1400 bytes: u64,
1401 width: u32,
1402 edge: u32,
1403) -> i32 {
1404 // SAFETY: the caller's contract.
1405 let new = unsafe { std::slice::from_raw_parts(value, bytes as usize) }.to_vec();
1406 let bit = |buf: &[u8], i: usize| buf.get(i / 8).is_some_and(|b| b >> (i % 8) & 1 == 1);
1407 let used_bits = 2 * width as usize;
1408 let changed = |old: &[u8]| (0..used_bits).any(|i| bit(old, i) != bit(&new, i));
1409 let key = snapshot_key(frame, site);
1410 let fired = EDGE_SNAPSHOTS.with(|m| {
1411 let mut m = m.borrow_mut();
1412 let fired = match m.get(&key) {
1413 None => false,
1414 Some(old) => {
1415 // (known 0, known 1, unknown) of the least significant bit.
1416 let state = |buf: &[u8]| {
1417 let (v, u) = (bit(buf, 0), bit(buf, width as usize));
1418 (!u && !v, !u && v, u)
1419 };
1420 let (o0, o1, ox) = state(old);
1421 let (n0, n1, nx) = state(&new);
1422 let pos = (o0 && (n1 || nx)) || (ox && n1);
1423 let neg = (o1 && (n0 || nx)) || (ox && n0);
1424 match edge {
1425 1 => pos,
1426 2 => neg,
1427 3 => pos || neg,
1428 _ => changed(old),
1429 }
1430 }
1431 };
1432 m.insert(key, new.clone());
1433 fired
1434 });
1435 i32::from(fired)
1436}
1437
1438/// `void sv_rt_edge_load(u64 frame, u64 site, ptr out, u64 bytes)`: copies
1439/// the bytes recorded by [`real_sv_rt_edge_snapshot`] for (`frame`, `site`)
1440/// into `out`; all zero when none was recorded (or it is shorter). Unlike
1441/// [`real_sv_rt_edge_check`] it leaves the snapshot in place, so a guard that
1442/// re-suspends keeps comparing against the state at the wait's start.
1443///
1444/// # Safety
1445/// `out` points to at least `bytes` writable bytes.
1446pub unsafe extern "C" fn real_sv_rt_edge_load(frame: u64, site: u64, out: *mut u8, bytes: u64) {
1447 // SAFETY: the caller's contract.
1448 let dst = unsafe { std::slice::from_raw_parts_mut(out, bytes as usize) };
1449 dst.fill(0);
1450 EDGE_SNAPSHOTS.with(|m| {
1451 if let Some(v) = m.borrow().get(&snapshot_key(frame, site)) {
1452 let n = v.len().min(dst.len());
1453 dst[..n].copy_from_slice(&v[..n]);
1454 }
1455 });
1456}
1457
1458/// `void sv_rt_set_fired_term(i64 term)`: overrides which term of the wait
1459/// the resuming kernel reports from [`real_sv_rt_fired_term`]. A resume guard
1460/// of an event list holding array-element terms uses it: the scheduler wakes
1461/// all of them on the array's one cell and names the first, so the guard
1462/// replaces that with the term whose element actually triggered.
1463pub extern "C" fn real_sv_rt_set_fired_term(term: i64) {
1464 FIRED_TERM.with(|c| c.set(term));
1465}
1466
1467/// Frees every cell of the previous run (`vj`'s run start).
1468pub fn reset_cells_for_new_run() {
1469 CELLS.with(|c| c.borrow_mut().clear());
1470 crate::state::reset_driver_counts_for_new_run();
1471 EDGE_SNAPSHOTS.with(|m| m.borrow_mut().clear());
1472 ELEM_WAITS.with(|m| m.borrow_mut().clear());
1473 // Process ids restart from zero: a waiter queued by the previous run must
1474 // not be mistaken for one of this run's processes.
1475 super::semaphore::reset_for_new_run();
1476 super::class_event::reset_for_new_run();
1477 super::mailbox::reset_for_new_run();
1478 crate::sched::proc_assert::reset();
1479 // 20.11: no assertion control task has run in a new run.
1480 crate::sched::assert_control::reset();
1481}
1482
1483/// `i32 sv_rt_aborted()`: 1 if the running process has ended the run
1484/// (`$finish`/`$stop`/`$fatal`, LRM 1800-2023 §20.2/§20.10) since it
1485/// resumed -- code after such a call inside a subroutine must not run, and
1486/// neither may the caller's (`vj-codegen`'s `leave_if_aborted`).
1487#[cfg_attr(feature = "aot-exports", unsafe(export_name = "sv_rt_aborted"))]
1488pub extern "C" fn real_sv_rt_aborted() -> i32 {
1489 ABORTED.with(|c| i32::from(c.get()))
1490}
1491
1492/// Records that term `term` of `proc`'s event wait matched in the current
1493/// time step (see [`real_sv_rt_fired_mask`]).
1494pub(crate) fn note_fired_term(proc: u32, term: usize) {
1495 if term < 64 {
1496 FIRED_MASKS.with(|m| {
1497 let mut m = m.borrow_mut();
1498 let i = proc as usize;
1499 if i >= m.len() {
1500 m.resize(i + 1, 0);
1501 }
1502 m[i] |= 1u64 << term;
1503 });
1504 }
1505}
1506
1507/// Takes and clears `proc`'s recorded matched-term mask (0 if none).
1508fn take_fired(proc: u32) -> u64 {
1509 FIRED_MASKS.with(|m| {
1510 m.borrow_mut()
1511 .get_mut(proc as usize)
1512 .map_or(0, std::mem::take)
1513 })
1514}
1515
1516/// Takes the terms (bit k = term k) of the calling process's event wait that
1517/// matched in the wake now running; for a native (non-kernel) process such as
1518/// a covergroup sampler, which nothing else drains. 0 outside a process.
1519pub fn take_fired_mask() -> u64 {
1520 current_process().map_or(0, take_fired)
1521}
1522
1523/// `i64 sv_rt_fired_mask()`: every term of the event wait the calling kernel
1524/// resumed from that matched before it ran (bit k = term k), 0 if unknown.
1525/// Two members of one event list can fire in the same wake (LRM 1800-2023
1526/// 9.4.2.3) and `sv_rt_fired_term` names only the first.
1527#[cfg_attr(feature = "aot-exports", unsafe(export_name = "sv_rt_fired_mask"))]
1528pub extern "C" fn real_sv_rt_fired_mask() -> i64 {
1529 FIRED_MASK.with(|c| c.get()) as i64
1530}
1531
1532/// `i64 sv_rt_fired_term()`: which term of the event wait the calling
1533/// kernel resumed from woke it (`wait_order`), -1 if none.
1534#[cfg_attr(feature = "aot-exports", unsafe(export_name = "sv_rt_fired_term"))]
1535pub extern "C" fn real_sv_rt_fired_term() -> i64 {
1536 FIRED_TERM.with(|c| c.get())
1537}
1538
1539// --- Task S7: the `SimHandle`-reachability bridge for `nba_enqueue`/
1540// `sv_rt_finish`'s real production bodies. -----------------------------
1541
1542thread_local! {
1543 /// While a compiled kernel's raw `ProcessFn` pointer is executing (inside
1544 /// [`with_scheduler_ops`], called only from [`CompiledKernelProcess::
1545 /// resume`] above), this holds the real `&dyn SchedulerOps` that
1546 /// `resume()` call was given -- the bridge this module's own doc comment
1547 /// (and `vj-codegen::process_lower::declare_rt_finish`'s doc comment)
1548 /// both flag as the still-open "the frozen ABI carries no scheduler-ops
1549 /// parameter, and `SimHandle` reaches only STATE" gap. This task closes
1550 /// it with the minimal, additive hook available: a thread-local set
1551 /// (and restored, not merely cleared, so a hypothetical nested call
1552 /// cannot leak its caller's `ops` early -- v1's single-threaded model
1553 /// never actually nests these calls, but the save/restore discipline
1554 /// costs nothing and removes the need to rely on that).
1555 ///
1556 /// `real_nba_enqueue`/`real_sv_rt_finish` below are its only readers;
1557 /// [`with_scheduler_ops`] is its only writer.
1558 ///
1559 /// Stored as a `'static`-erased trait-object pointer (a thread-local's
1560 /// contents must be `'static`, and `ops`'s real lifetime is only the
1561 /// duration of one `resume()` call) -- the `unsafe` lifetime-erasing
1562 /// transmute this requires lives in [`with_scheduler_ops`] alone, whose
1563 /// own save-and-restore discipline is exactly what keeps that erasure
1564 /// sound (the pointer is never read after the real `ops` it points at
1565 /// stops being alive).
1566 static CURRENT_OPS: RefCell<Option<*const (dyn SchedulerOps + 'static)>> =
1567 const { RefCell::new(None) };
1568
1569 /// Review fix (A2-part2 r1, binding rule 7): the runtime-side lookup
1570 /// this module's own doc comment (and `process_lower::
1571 /// lower_nonblocking_assignment`'s doc comment) flagged as "no committed
1572 /// code implements yet". The frozen `ProcessFn` ABI carries no
1573 /// `ProcessId` parameter at all, so a compiled kernel itself can never
1574 /// pass its own identity to `nba_enqueue`; the *scheduler* (SCHED's own
1575 /// `ResumeDriver::resume_for`/`start`, `resume.rs`) already knows which
1576 /// `ProcessId` it is about to resume before it ever calls `.resume(..)`,
1577 /// so that is the one ambient-state hook this bridge needs -- mirrors
1578 /// [`CURRENT_OPS`] exactly (same save/restore discipline, same "set for
1579 /// the duration of one `resume()` call" scope). [`with_current_process`]
1580 /// is its only writer; [`real_nba_enqueue`] is its only reader.
1581 static CURRENT_PROCESS: RefCell<Option<ProcessId>> = const { RefCell::new(None) };
1582
1583 /// Set while a kernel stages a clocking-block synchronous drive (between
1584 /// the `sv_rt_reactive_drive(1)` / `(0)` calls codegen brackets it with):
1585 /// the nonblocking writes it stages belong to the Re-NBA region (LRM
1586 /// 1800-2023 14.16), not the NBA region.
1587 static REACTIVE_DRIVE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
1588
1589 /// Within an edge-skewed clocking drive: the clock signal's cell, the edge
1590 /// (see [`EdgeWait`]) and the ticks after it (LRM 1800-2023 14.4).
1591 static EDGE_DRIVE: std::cell::Cell<Option<EdgeWait>> = const { std::cell::Cell::new(None) };
1592}
1593
1594/// An edge output skew's wait (`output negedge #1 q`): the drive lands `after`
1595/// ticks past the next `edge` of `cell`.
1596#[derive(Clone, Copy)]
1597pub(super) struct EdgeWait {
1598 pub(super) cell: StateCellId,
1599 pub(super) edge: EdgeKind,
1600 pub(super) after: u64,
1601 /// A drive to the block's own event (counter `cell`, LRM 14.16): it
1602 /// matures now when the event was already triggered in this time step.
1603 pub(super) on_event: bool,
1604}
1605
1606/// Whether the running kernel is inside a clocking-block synchronous drive.
1607pub(super) fn reactive_drive() -> bool {
1608 REACTIVE_DRIVE.with(std::cell::Cell::get)
1609}
1610
1611/// The edge wait of the clocking drive the running kernel is staging, if its
1612/// output skew is an edge.
1613pub(super) fn edge_drive() -> Option<EdgeWait> {
1614 EDGE_DRIVE.with(std::cell::Cell::get)
1615}
1616
1617/// The staging delay a kernel passes for a write that waits on
1618/// [`edge_drive`]'s edge instead of a tick count.
1619pub(super) const EDGE_DELAY: u64 = u64::MAX;
1620
1621/// The real body of `sv_rt_reactive_drive`: `on != 0` opens, `0` closes the
1622/// span in which the kernel's nonblocking writes are a clocking drive staged
1623/// to the Re-NBA region (LRM 1800-2023 14.16). An open call with `edge != 0`
1624/// (1 posedge, 2 negedge, 3 either) is an edge output skew (14.4); `4` waits
1625/// for the next trigger of the clocking block's event (14.16; `clock_slot` is
1626/// its event counter): its writes
1627/// carry [`EDGE_DELAY`] and land `after` ticks past the next such edge of
1628/// `clock_slot`.
1629///
1630/// # Safety
1631/// `handle` must be the running kernel's instance handle.
1632#[cfg_attr(feature = "aot-exports", unsafe(export_name = "sv_rt_reactive_drive"))]
1633pub unsafe extern "C" fn real_sv_rt_reactive_drive(
1634 handle: SimHandle,
1635 on: u32,
1636 clock_slot: u32,
1637 edge: u32,
1638 after: u64,
1639) {
1640 if on != 0 {
1641 with_current_ops(|ops| ops.note_reactive_drive());
1642 }
1643 REACTIVE_DRIVE.with(|c| c.set(on != 0));
1644 let wait = (on != 0 && edge != 0).then(|| EdgeWait {
1645 cell: crate::state::cell_for_slot(handle, vj_abi::SlotId(clock_slot)),
1646 edge: match edge {
1647 1 => EdgeKind::Posedge,
1648 2 => EdgeKind::Negedge,
1649 _ => EdgeKind::BothEdges,
1650 },
1651 after,
1652 on_event: edge == 4,
1653 });
1654 EDGE_DRIVE.with(|c| c.set(wait));
1655}
1656
1657/// Runs `f` (one `resume()` call, dispatched via `ResumeDriver`) with `proc`
1658/// reachable through [`CURRENT_PROCESS`] for exactly that duration,
1659/// restoring whatever was there before on return -- see that thread-local's
1660/// own doc comment. Called only from `sched::resume::ResumeDriver::
1661/// resume_for`/`start`, the two places `proc` is known before the dispatch
1662/// into `CompiledProcess::resume` happens. (`pub` so a runtime-ABI unit test
1663/// can stand in for a running process -- `vj_runtime::semaphore_rt`.)
1664pub fn with_current_process<R>(proc: ProcessId, f: impl FnOnce() -> R) -> R {
1665 let prev = CURRENT_PROCESS.with(|c| c.replace(Some(proc)));
1666 let result = f();
1667 CURRENT_PROCESS.with(|c| *c.borrow_mut() = prev);
1668 result
1669}
1670
1671/// B-runtime Task 6 review fix r1 (RT-078): the read-only, cross-crate
1672/// half of the same [`CURRENT_PROCESS`] ambient-state bridge
1673/// [`real_nba_enqueue`] already uses to attribute an NBA write to a real
1674/// `ProcessId` despite the frozen `ProcessFn` ABI carrying none.
1675/// `vj_runtime::random_sink`'s `$random`/`$urandom`/`$urandom_range`
1676/// sink has the identical problem -- their ABI symbols
1677/// (`vj_codegen::expr::SV_RT_RANDOM_SYMBOL` et al.) are genuinely
1678/// zero-argument (no `SimHandle`, no `ProcessId`; see that module's own doc
1679/// comment for why), so this is the one already-live ambient hook that can
1680/// supply a real per-process identity to key a seed store by. Returns
1681/// `None` outside of any `ResumeDriver::resume_for`/`start` dispatch
1682/// (exactly [`with_current_process`]'s own "not currently installed" case) --
1683/// callers key an isolated fallback slot for that case, never conflating it
1684/// with a real `ProcessId`.
1685pub fn current_process() -> Option<ProcessId> {
1686 CURRENT_PROCESS.with(|c| *c.borrow())
1687}
1688
1689/// Runs `f` (one raw kernel call) with `ops` reachable through
1690/// [`CURRENT_OPS`] for exactly that duration, restoring whatever was there
1691/// before on return (see that thread-local's own doc comment).
1692pub fn with_scheduler_ops<R>(ops: &dyn SchedulerOps, f: impl FnOnce() -> R) -> R {
1693 let raw: *const dyn SchedulerOps = ops;
1694 // SAFETY: erases `raw`'s real (sub-`'static`, tied to this call's own
1695 // `ops` parameter) lifetime so it can be stored in a `thread_local!`
1696 // (which requires `'static` contents). This is sound only because this
1697 // function itself restores `CURRENT_OPS` to `prev` (see below) before
1698 // returning -- i.e. before `ops`'s real borrow ends -- so no reader
1699 // (`with_current_ops`, called only from inside `f`'s own dynamic extent)
1700 // can ever observe this pointer after the object it points at is no
1701 // longer alive.
1702 let raw_static: *const (dyn SchedulerOps + 'static) = unsafe { std::mem::transmute(raw) };
1703 let prev = CURRENT_OPS.with(|c| c.replace(Some(raw_static)));
1704 let result = f();
1705 CURRENT_OPS.with(|c| *c.borrow_mut() = prev);
1706 result
1707}
1708
1709/// Calls `f` with the current [`CURRENT_OPS`], if any is set. A miss (no
1710/// `with_scheduler_ops` call is currently on this thread's stack) is
1711/// unreachable for every kernel this tree's own codegen emits -- `nba_enqueue`/
1712/// `sv_rt_finish` are only ever invoked from inside a JIT'd kernel body, which
1713/// is only ever invoked from [`CompiledKernelProcess::resume`], which always
1714/// wraps the call in [`with_scheduler_ops`] -- so a miss here is silently
1715/// doing nothing for a call shape nothing in this tree can actually produce,
1716/// not a masked bug.
1717pub fn with_current_ops(f: impl FnOnce(&dyn SchedulerOps)) {
1718 // The pointer is copied out so the `RefCell` borrow ends before `f` runs:
1719 // `f` may itself enter `with_scheduler_ops` (an assertion action block
1720 // resumed from a Reactive callback, plan T29), which replaces the cell.
1721 let raw = CURRENT_OPS.with(|c| *c.borrow());
1722 if let Some(raw) = raw {
1723 // SAFETY: `raw` is only ever `Some` while the `resume()` call
1724 // that set it via `with_scheduler_ops` is still executing on
1725 // this thread (see that function's own body: it clears/restores
1726 // before returning), and that call's own `ops: &dyn
1727 // SchedulerOps` argument is alive for its whole duration -- so
1728 // this pointer is live for exactly as long as it can be read.
1729 let ops: &dyn SchedulerOps = unsafe { &*raw };
1730 f(ops);
1731 }
1732}
1733
1734/// Task S7's real production body for the frozen `nba_enqueue` ABI symbol
1735/// (`vj_abi::process::NbaEnqueueFn`/`NBA_ENQUEUE_SYMBOL`) -- the ONE place
1736/// a nonblocking write a compiled kernel stages reaches the scheduler's real
1737/// [`SchedulerOps::stage_nba`]. Bound into a real JIT'd module via
1738/// `Jit::add_global_mapping(nba_fv, real_nba_enqueue as *const () as usize)`
1739/// (`vj::run`) -- no `#[no_mangle]`/linker-visible symbol is needed,
1740/// since `add_global_mapping` binds by explicit address, exactly like every
1741/// accessor-ABI stub this workspace's own tests already bind this same way.
1742#[cfg_attr(feature = "aot-exports", unsafe(export_name = "nba_enqueue"))]
1743pub extern "C" fn real_nba_enqueue(
1744 handle: SimHandle,
1745 slot: vj_abi::SlotId,
1746 value: u128,
1747 source: u32,
1748) {
1749 // SAFETY: identical justification to `state::accessor_impl::
1750 // sv_get_scalar` -- every real caller of this ABI-named symbol (the
1751 // JIT'd kernel `CompiledKernelProcess::resume` invokes) is handed a
1752 // `SimHandle` minted by `InstanceHandle::into_sim_handle`.
1753 let ih = unsafe { InstanceHandle::from_sim_handle(handle) };
1754 let cell = ih.cell_for_slot(slot.0 as usize);
1755 // Review fix (A2-part2 r1, binding rule 7): `source` (the frozen ABI's
1756 // own `u32` parameter) is always the codegen's constant `0` --
1757 // `process_lower::lower_nonblocking_assignment`'s own doc comment
1758 // explains why the frozen ABI has nowhere to carry a real `ProcessId`.
1759 // [`CURRENT_PROCESS`] is the real attribution: `ResumeDriver::
1760 // resume_for`/`start` (`sched::resume`) set it, via
1761 // [`with_current_process`], for the exact duration of the `resume()`
1762 // call that is (transitively, through this very kernel invocation)
1763 // calling this function right now, so a hit here is the ProcessId
1764 // actually being resumed. A miss (`None`) falls back to the raw ABI
1765 // value -- reachable only for the handful of this module's own unit
1766 // tests that call `CompiledKernelProcess::resume` directly, bypassing
1767 // `ResumeDriver` entirely; every real `vj run` dispatch goes
1768 // through `ResumeDriver`, so production code always hits `Some`.
1769 let source = CURRENT_PROCESS.with(|c| *c.borrow()).unwrap_or(source);
1770 with_current_ops(|ops| ops.stage_nba(cell, FourStateValue::Narrow(value), source));
1771}
1772
1773/// A nonblocking write to part of a signal (`q[3:0] <= v`), to a memory row
1774/// (`mem[i] <= v`, `has_row != 0`), or to a >64-bit signal: stages
1775/// `(cell, row, value, mask)` for the NBA commit (`nba::MaskedNbaUpdate`).
1776/// `val`/`unk`/`mask` each point at `nwords` words holding the target's
1777/// `width` bits in codegen's val/unk planes, the new bits already shifted
1778/// into position; only the bits set in `mask` are written. A nonzero `delay`
1779/// (`q <= #d v`) defers the write to the time step `delay` ticks away.
1780///
1781/// # Safety
1782/// `val`, `unk` and `mask` must each point at `nwords` readable `u64`s, and
1783/// `nwords` must be `width.div_ceil(64)` (codegen's `build_nba_masked`).
1784#[cfg_attr(feature = "aot-exports", unsafe(export_name = "sv_rt_nba_masked"))]
1785pub unsafe extern "C" fn real_sv_rt_nba_masked(
1786 handle: SimHandle,
1787 slot: u32,
1788 row: u64,
1789 has_row: u32,
1790 val: *const u64,
1791 unk: *const u64,
1792 mask: *const u64,
1793 nwords: u32,
1794 width: u32,
1795 delay: u64,
1796) {
1797 // SAFETY: identical justification to `real_nba_enqueue` above.
1798 let ih = unsafe { InstanceHandle::from_sim_handle(handle) };
1799 let cell = ih.cell_for_slot(slot as usize);
1800 let n = nwords as usize;
1801 // SAFETY: the caller's contract (this function's `# Safety`).
1802 let (val, unk, mask) = unsafe {
1803 (
1804 std::slice::from_raw_parts(val, n).to_vec(),
1805 std::slice::from_raw_parts(unk, n).to_vec(),
1806 std::slice::from_raw_parts(mask, n).to_vec(),
1807 )
1808 };
1809 // STATE's encodings: `Narrow` is val / unk (unk at bit 64); `Wide` is
1810 // IEEE aval / bval, aval = val ^ unk.
1811 let value = if width <= 64 {
1812 FourStateValue::Narrow(u128::from(val[0]) | (u128::from(unk[0]) << 64))
1813 } else {
1814 FourStateValue::Wide {
1815 aval: val.iter().zip(&unk).map(|(v, u)| v ^ u).collect(),
1816 bval: unk,
1817 }
1818 };
1819 let row = (has_row != 0).then_some(row);
1820 let source = CURRENT_PROCESS.with(|c| *c.borrow()).unwrap_or(0);
1821 with_current_ops(|ops| {
1822 if delay == 0 {
1823 ops.stage_nba_masked(cell, row, value, mask, source);
1824 } else {
1825 ops.stage_nba_masked_delayed(cell, row, value, mask, source, delay);
1826 }
1827 });
1828}
1829
1830/// The real body of `sv_rt_nba_coll`: a nonblocking write of element
1831/// `index` of the queue / dynamic array whose handle is held by `slot` (row
1832/// `row` when `has_row != 0`), LRM 1800-2023 10.4.2 / 7.10. `elem` points at
1833/// `elem_words` words (the element's packed bits); `flags` bit 0 is a queue
1834/// (an index equal to the size appends), bit 1 an object-handle element. The
1835/// handle is looked up and the index checked when the update commits, so an
1836/// out-of-range write is ignored then. `delay` ticks defer the staging.
1837/// A non-null `key` (`key_kind`/`key_width` as in `sv_rt_coll`) selects the
1838/// element of an associative array by key instead of `index`.
1839///
1840/// # Safety
1841/// `elem` must point at `elem_words` readable `u64`s, `key` (when non-null)
1842/// at the key's words.
1843#[cfg_attr(feature = "aot-exports", unsafe(export_name = "sv_rt_nba_coll"))]
1844pub unsafe extern "C" fn real_sv_rt_nba_coll(
1845 handle: SimHandle,
1846 slot: u32,
1847 row: u64,
1848 has_row: u32,
1849 index: i64,
1850 flags: u32,
1851 elem: *const u64,
1852 elem_words: u32,
1853 delay: u64,
1854 key: *const u64,
1855 key_kind: u32,
1856 key_width: u32,
1857) {
1858 // SAFETY: identical justification to `real_nba_enqueue` above.
1859 let ih = unsafe { InstanceHandle::from_sim_handle(handle) };
1860 let cell = ih.cell_for_slot(slot as usize);
1861 // SAFETY: the caller's contract (this function's `# Safety`).
1862 let elem = unsafe { std::slice::from_raw_parts(elem, elem_words as usize) }.to_vec();
1863 let obj = flags & 2 != 0;
1864 let mut shape = crate::collections::Shape {
1865 elem_words: elem_words as usize,
1866 assoc: false,
1867 key: crate::collections::KeyKind::Int {
1868 width: 0,
1869 signed: false,
1870 },
1871 obj,
1872 nested: crate::collections::Nested::NONE,
1873 };
1874 // An associative array's key is decoded now (a string key is a handle
1875 // to text that may change before the update lands).
1876 let mut key_val = None;
1877 if !key.is_null() {
1878 shape.assoc = true;
1879 shape.key = if key_kind == 2 {
1880 crate::collections::KeyKind::Str
1881 } else if key_kind == 3 {
1882 crate::collections::KeyKind::Obj
1883 } else {
1884 crate::collections::KeyKind::Int {
1885 width: key_width,
1886 signed: key_kind == 1,
1887 }
1888 };
1889 let kw = if key_kind == 2 {
1890 1
1891 } else {
1892 (2 * key_width as usize).div_ceil(64).max(1)
1893 };
1894 // SAFETY: the caller's contract: `key` points at the key's words.
1895 let bits = unsafe { std::slice::from_raw_parts(key, kw) };
1896 match shape.key_from_bits(bits) {
1897 Some(k) => key_val = Some(k),
1898 // An invalid key (x/z bits) writes nothing (LRM 7.8.6).
1899 None => {
1900 shape.discard(&elem);
1901 return;
1902 }
1903 }
1904 }
1905 let coll = super::nba::CollWrite {
1906 shape,
1907 index,
1908 key: key_val,
1909 queue: flags & 1 != 0,
1910 };
1911 let row = (has_row != 0).then_some(row);
1912 let source = CURRENT_PROCESS.with(|c| *c.borrow()).unwrap_or(0);
1913 with_current_ops(|ops| ops.stage_nba_coll(cell, row, elem, coll, source, delay));
1914}
1915
1916/// Called by `sv_rt_obj`'s `FieldNba` (no symbol of its own): the real body of
1917/// a nonblocking class-property write: a nonblocking write of the `mask` bits
1918/// of the `words` packed words at `offset` of object `h`'s property storage
1919/// (`obj.p <= v`, `obj.v[3:0] <= v`; LRM 1800-2023 10.4.2, 8.x). The object is
1920/// retained until the NBA region commits it, so a handle overwritten or
1921/// dropped in between does not lose the update. A null or dead `h` stages
1922/// nothing. `delay` ticks defer the staging.
1923///
1924/// # Safety
1925/// `val` and `mask` must each point at `words` readable `u64`s.
1926pub unsafe extern "C" fn real_sv_rt_nba_field(
1927 h: u64,
1928 offset: u64,
1929 words: u32,
1930 val: *const u64,
1931 mask: *const u64,
1932 delay: u64,
1933) {
1934 if !crate::objects::is_live(h) {
1935 return;
1936 }
1937 let n = words as usize;
1938 // SAFETY: the caller's contract (this function's `# Safety`).
1939 let (val, mask) = unsafe {
1940 (
1941 std::slice::from_raw_parts(val, n).to_vec(),
1942 std::slice::from_raw_parts(mask, n).to_vec(),
1943 )
1944 };
1945 let w = super::nba::ObjWrite::new(h, offset as usize, val, mask);
1946 let source = CURRENT_PROCESS.with(|c| *c.borrow()).unwrap_or(0);
1947 with_current_ops(|ops| ops.stage_nba_obj(w, source, delay));
1948}
1949
1950/// A nonblocking trigger (`->> e`, `->> #d e`; LRM 1800-2023 15.5.1) of class
1951/// event `id` (`sched::class_event`), `delay` ticks from now: staged like a
1952/// class-property write and committed in the NBA region, where it wakes the
1953/// event's waiters. A `null` id triggers nothing (15.5.5.2).
1954pub fn real_sv_rt_nba_class_event(id: u64, delay: u64) {
1955 if id == 0 {
1956 return;
1957 }
1958 let w = super::nba::ObjWrite::class_event(id);
1959 let source = CURRENT_PROCESS.with(|c| *c.borrow()).unwrap_or(0);
1960 with_current_ops(|ops| ops.stage_nba_obj(w, source, delay));
1961}
1962
1963/// The real body of `sv_rt_nba_pulse`: a nonblocking event trigger
1964/// (`->> e`, `->> #d e`; LRM 1800-2023 15.5.2) of the event in `slot` (row
1965/// `row` of an event array when `has_row != 0`), `delay` ticks from now. The
1966/// trigger lands in the NBA region of its step (`NbaLog::stage_pulse`).
1967///
1968/// # Safety
1969/// `handle` must be a live `SimHandle` of the running instance.
1970#[cfg_attr(feature = "aot-exports", unsafe(export_name = "sv_rt_nba_pulse"))]
1971pub unsafe extern "C" fn real_sv_rt_nba_pulse(
1972 handle: SimHandle,
1973 slot: u32,
1974 row: u64,
1975 has_row: u32,
1976 delay: u64,
1977) {
1978 // SAFETY: identical justification to `real_nba_enqueue` above.
1979 let ih = unsafe { InstanceHandle::from_sim_handle(handle) };
1980 let cell = ih.cell_for_slot(slot as usize);
1981 let row = (has_row != 0).then_some(row);
1982 let source = CURRENT_PROCESS.with(|c| *c.borrow()).unwrap_or(0);
1983 with_current_ops(|ops| ops.stage_nba_pulse(cell, row, source, delay));
1984}
1985
1986/// Task S7 finding: the real production body for the frozen `sv_set_scalar`
1987/// accessor-ABI symbol (`vj_abi::accessor::SV_SET_SCALAR_SYMBOL`), for a
1988/// COMPILED-KERNEL caller running under a real `Engine` -- deliberately NOT
1989/// `state::accessor_impl::sv_set_scalar` itself (that fn writes straight into
1990/// STATE's arena via `commit_write_at`'s own `&mut ()` no-op sink, which is
1991/// exactly correct for a debugger/VPI-style direct poke, but never notifies
1992/// `Engine`'s sensitivity table -- verified empirically while building this
1993/// task's own end-to-end pipeline: a compiled kernel's blocking write to a
1994/// clock signal through the bare accessor never woke a process waiting on its
1995/// posedge, because nothing told `Engine` the value had changed). A blocking
1996/// (`=`) write from a compiled process MUST go through
1997/// [`SchedulerOps::commit_blocking_write`] instead -- the same real path
1998/// `EngineOps::commit_blocking_write` (`sched::control`) already uses for
1999/// every OTHER blocking-write producer this crate has, which itself performs
2000/// the STATE write (through `Engine`'s own held `SignalAccess`) AND the
2001/// sensitivity-table notification together, as one atomic step. This
2002/// function therefore does NOT also call the bare `sv_set_scalar` -- doing so
2003/// would be a redundant second write through an unrelated path, not a
2004/// correctness requirement.
2005///
2006/// Bound into a real JIT'd module via `Jit::add_global_mapping(set_scalar_fv,
2007/// real_sv_set_scalar as *const () as usize)` (`vj::run`), in place
2008/// of the bare accessor -- the same binding mechanism [`real_nba_enqueue`]
2009/// above uses.
2010#[cfg_attr(feature = "aot-exports", unsafe(export_name = "sv_set_scalar"))]
2011pub extern "C" fn real_sv_set_scalar(handle: SimHandle, slot: vj_abi::SlotId, value: u128) {
2012 // SAFETY: identical justification to `real_nba_enqueue` above.
2013 let ih = unsafe { InstanceHandle::from_sim_handle(handle) };
2014 let cell = ih.cell_for_slot(slot.0 as usize);
2015 with_current_ops(|ops| ops.commit_blocking_write(cell, FourStateValue::Narrow(value)));
2016}
2017
2018/// [`real_sv_set_scalar`]'s wide-tier sibling, bound in place of the bare
2019/// `state::sv_set_wide` for the identical reason: the bare accessor writes
2020/// STATE but never tells `Engine`, so a process waiting on a >64-bit signal
2021/// (`@w`, `wait (w == ...)`, a continuous assign or `always @*` reading it)
2022/// was never woken.
2023#[cfg_attr(feature = "aot-exports", unsafe(export_name = "sv_set_wide"))]
2024pub extern "C" fn real_sv_set_wide(
2025 handle: SimHandle,
2026 slot: vj_abi::SlotId,
2027 value: vj_abi::WideView,
2028) {
2029 // SAFETY: identical justification to `real_nba_enqueue` above.
2030 let ih = unsafe { InstanceHandle::from_sim_handle(handle) };
2031 let cell = ih.cell_for_slot(slot.0 as usize);
2032 let n = value.word_count as usize;
2033 // SAFETY: per the accessor-ABI contract, a `SvSetWideFn` caller's
2034 // `WideView` points at `word_count` initialized words in each plane for
2035 // the duration of the call (codegen's `build_set_wide` passes its own
2036 // stack buffers sized to the slot).
2037 let aval = unsafe { std::slice::from_raw_parts(value.aval, n) }.to_vec();
2038 let bval = unsafe { std::slice::from_raw_parts(value.bval, n) }.to_vec();
2039 with_current_ops(|ops| ops.commit_blocking_write(cell, FourStateValue::Wide { aval, bval }));
2040}
2041
2042/// `sv_rt_huge`'s operation codes (`vj-codegen::huge`'s `HugeRt`).
2043pub const HUGE_OP_PRIME: u32 = 0;
2044pub const HUGE_OP_MARK: u32 = 1;
2045pub const HUGE_OP_COMMIT: u32 = 2;
2046pub const HUGE_OP_NBA: u32 = 3;
2047
2048/// The one runtime entry for a compiled process's huge-signal shadow
2049/// (`state::huge_mirror`), `void* sv_rt_huge(u32 op, SimHandle, u32 slot,
2050/// u32 width, i64 a, i64 b, const u64* p, u64 delay)`:
2051///
2052/// * [`HUGE_OP_PRIME`]: the signal's long-lived PK image, current, at a stable
2053/// address (returned); `a != 0` re-reads the planes (external code wrote
2054/// them).
2055/// * [`HUGE_OP_MARK`]: widens the image's dirty window by the value bits
2056/// `[a, a + b)`.
2057/// * [`HUGE_OP_COMMIT`]: stores the dirty window to STATE and wakes the
2058/// processes sensitive to the signal iff its value changed.
2059///
2060/// * [`HUGE_OP_NBA`]: stages the nonblocking write `sig[a +: b] <= image`,
2061/// `p` the PK image of the `b`-bit right-hand side (val plane at bit 0, unk
2062/// plane at bit `b`), `delay` its intra-assignment delay; only the plane
2063/// words the select covers are staged.
2064///
2065/// `p` and `delay` are ignored by the other operations. Other operations
2066/// return null.
2067///
2068/// # Safety
2069/// For [`HUGE_OP_NBA`], `p` must point at `ceil(2 * b / 64)` readable words.
2070#[allow(clippy::too_many_arguments)]
2071#[cfg_attr(feature = "aot-exports", unsafe(export_name = "sv_rt_huge"))]
2072pub unsafe extern "C" fn real_sv_rt_huge(
2073 op: u32,
2074 handle: SimHandle,
2075 slot: u32,
2076 width: u32,
2077 a: i64,
2078 b: i64,
2079 p: *const u64,
2080 delay: u64,
2081) -> *mut u64 {
2082 match op {
2083 HUGE_OP_NBA => {
2084 if b > 0 && !p.is_null() {
2085 // SAFETY: the caller's contract (this function's `# Safety`).
2086 let img = unsafe { std::slice::from_raw_parts(p, (2 * b as usize).div_ceil(64)) };
2087 if let Some(w) =
2088 crate::state::huge_nba_window(handle, vj_abi::SlotId(slot), width, a, b, img)
2089 {
2090 let value = FourStateValue::Wide {
2091 aval: w.aval,
2092 bval: w.bval,
2093 };
2094 let source = CURRENT_PROCESS.with(|c| *c.borrow()).unwrap_or(0);
2095 with_current_ops(|ops| {
2096 ops.stage_nba_window(w.cell, w.window, value, w.mask, source, delay);
2097 });
2098 }
2099 }
2100 std::ptr::null_mut()
2101 }
2102 HUGE_OP_PRIME => crate::state::huge_prime(handle, vj_abi::SlotId(slot), width, a != 0),
2103 HUGE_OP_MARK => {
2104 crate::state::huge_mark(handle, vj_abi::SlotId(slot), a, b);
2105 std::ptr::null_mut()
2106 }
2107 HUGE_OP_COMMIT => {
2108 huge_commit_and_notify(handle, slot, width);
2109 std::ptr::null_mut()
2110 }
2111 _ => std::ptr::null_mut(),
2112 }
2113}
2114
2115/// Commits a huge signal's dirty window to STATE and wakes the processes
2116/// sensitive to it iff the value changed (`state::huge_commit`): the windowed
2117/// stand-in for `build_set_huge` + [`real_sv_set_wide`].
2118fn huge_commit_and_notify(handle: SimHandle, slot: u32, width: u32) {
2119 use crate::state::HugeCommit;
2120 let slot = vj_abi::SlotId(slot);
2121 let wants = |cell| {
2122 let answer = std::cell::Cell::new(false);
2123 with_current_ops(|ops| answer.set(ops.wants_delta(cell)));
2124 answer.get()
2125 };
2126 match crate::state::huge_commit(handle, slot, width, &wants) {
2127 HugeCommit::Nothing | HugeCommit::Unchanged => {}
2128 HugeCommit::Changed {
2129 cell,
2130 old,
2131 new,
2132 delta,
2133 } => {
2134 let edge = crate::sched::edge_between(old, new);
2135 with_current_ops(|ops| ops.notify_blocking_wide_change(cell, edge, delta));
2136 }
2137 HugeCommit::Full {
2138 cell,
2139 value,
2140 revalidate,
2141 } => {
2142 with_current_ops(|ops| ops.commit_blocking_write(cell, value));
2143 crate::state::huge_commit_full_done(handle, slot, revalidate);
2144 }
2145 }
2146}
2147
2148/// [`real_sv_set_scalar`]'s memory-row sibling: commits the row through
2149/// STATE's own row path, then wakes processes sensitive to the memory iff
2150/// the row changed (`SchedulerOps::notify_blocking_row_change`).
2151#[cfg_attr(feature = "aot-exports", unsafe(export_name = "sv_set_mem_word"))]
2152pub extern "C" fn real_sv_set_mem_word(
2153 handle: SimHandle,
2154 slot: vj_abi::SlotId,
2155 row_index: u64,
2156 value: vj_abi::WideView,
2157) {
2158 let (handle, slot) = crate::state::resolve_foreign_slot(handle, slot);
2159 let (cell, changed) =
2160 crate::state::set_mem_word_reporting_change(handle, slot, row_index, value);
2161 if changed {
2162 with_current_ops(|ops| ops.notify_blocking_row_change(cell));
2163 }
2164}
2165
2166/// Task S7's real production body for the frozen `sv_rt_finish` ABI symbol
2167/// (`vj-codegen::process_lower::SV_RT_FINISH_SYMBOL`/
2168/// `declare_rt_finish`) -- reaches the scheduler's real
2169/// [`SchedulerOps::request_finish`], the one canonical path
2170/// `Engine::run_until` already checks once per timestep (SC-075). Bound the
2171/// same way as [`real_nba_enqueue`] above.
2172#[cfg_attr(feature = "aot-exports", unsafe(export_name = "sv_rt_finish"))]
2173pub extern "C" fn real_sv_rt_finish(_handle: SimHandle, code: i32) {
2174 ABORTED.with(|c| c.set(true));
2175 with_current_ops(|ops| ops.request_finish(code));
2176}
2177
2178/// `disable fork;` (LRM 9.6.3, SC-125): the real production body for
2179/// `sv_rt_disable_fork`. Kills every live descendant the CURRENT process
2180/// forked, through [`SchedulerOps::disable_fork`] -- reached via the same
2181/// [`CURRENT_OPS`]/[`CURRENT_PROCESS`] bridges as [`real_sv_rt_finish`]/
2182/// [`real_nba_enqueue`] (the frozen `ProcessFn` ABI carries no process id).
2183#[cfg_attr(feature = "aot-exports", unsafe(export_name = "sv_rt_disable_fork"))]
2184pub extern "C" fn real_sv_rt_disable_fork(_handle: SimHandle) {
2185 if let Some(proc) = current_process() {
2186 with_current_ops(|ops| ops.disable_fork(proc));
2187 }
2188}
2189
2190/// `disable f;` in a branch of fork `f`: the real production body for
2191/// `sv_rt_disable_fork_block` ([`SchedulerOps::disable_fork_block`]).
2192#[cfg_attr(
2193 feature = "aot-exports",
2194 unsafe(export_name = "sv_rt_disable_fork_block")
2195)]
2196pub extern "C" fn real_sv_rt_disable_fork_block(_handle: SimHandle) {
2197 if let Some(proc) = current_process() {
2198 with_current_ops(|ops| ops.disable_fork_block(proc));
2199 }
2200}
2201
2202/// A cross-process/fork-sibling `disable <label>;` (LRM 9.6.2): the real
2203/// production body for `sv_rt_disable_named` ([`SchedulerOps::
2204/// disable_named`]). `label` is a NUL-terminated C string (same convention
2205/// as `vj::dump::sv_rt_dump_vars`'s own `scopes` argument) -- a
2206/// static string literal `vj-codegen::process_lower` builds from the
2207/// disable statement's own source text, never attacker/user-controlled
2208/// binary data, so a `CStr`/UTF-8 decode failure here is an internal
2209/// codegen bug, surfaced loudly (`expect`), never swallowed. Sets this
2210/// kernel's own abort flag ([`abort_current_kernel`]) when the call ends
2211/// the CALLER itself (it is, or descends from, one of the named live
2212/// processes) -- `vj-codegen`'s caller then calls `leave_if_aborted`
2213/// right after, the same two-step `process::kill()` already uses.
2214///
2215/// # Safety
2216/// `label` must point to a valid, NUL-terminated, UTF-8 C string for the
2217/// duration of this call -- guaranteed by codegen always passing a `'static`
2218/// global string constant it compiled from the source `disable` statement.
2219#[cfg_attr(feature = "aot-exports", unsafe(export_name = "sv_rt_disable_named"))]
2220pub unsafe extern "C" fn real_sv_rt_disable_named(
2221 _handle: SimHandle,
2222 label: *const std::ffi::c_char,
2223) {
2224 // SAFETY: the caller's contract (this function's own doc comment).
2225 let label = unsafe { CStr::from_ptr(label) }
2226 .to_str()
2227 .expect("sv_rt_disable_named: codegen must pass a valid UTF-8 C string");
2228 if let Some(proc) = current_process() {
2229 let mut ends_caller = false;
2230 with_current_ops(|ops| ends_caller = ops.disable_named(proc, label));
2231 if ends_caller {
2232 abort_current_kernel();
2233 }
2234 }
2235}
2236
2237/// Classes plan Task 13 (LRM 9.7): the `process` class's scheduler bridge,
2238/// called by `vj_runtime::process_class`. Like `real_sv_rt_disable_fork`
2239/// it reaches the scheduler through [`CURRENT_OPS`]/[`CURRENT_PROCESS`]
2240/// (the frozen `ProcessFn` ABI carries no process id). All four are no-ops
2241/// (or report `Running`) outside a kernel call.
2242///
2243/// Queues `req` for the calling process; `true` iff it terminates the
2244/// caller itself (the caller then leaves its kernel at once -- see
2245/// [`abort_current_kernel`]).
2246pub fn process_class_request(req: ProcessRequest) -> bool {
2247 let Some(requester) = current_process() else {
2248 return false;
2249 };
2250 let mut ends_caller = false;
2251 with_current_ops(|ops| ends_caller = ops.process_request(requester, req));
2252 ends_caller
2253}
2254
2255/// `p.status()` for process `target`.
2256pub fn process_class_state(target: ProcessId) -> ProcessState {
2257 let mut state = ProcessState::Running;
2258 with_current_ops(|ops| state = ops.process_state(target));
2259 state
2260}
2261
2262/// Marks the calling process as one a `process` handle exists for and
2263/// returns its id (`process::self()`), or `None` outside a process.
2264pub fn process_class_track_current() -> Option<ProcessId> {
2265 let proc = current_process()?;
2266 with_current_ops(|ops| ops.process_track(proc));
2267 Some(proc)
2268}
2269
2270/// Makes the running kernel's next `leave_if_aborted` check fire, which ends
2271/// the calling process's segment (`Suspend::Done`) -- the one mechanism
2272/// `$finish` uses, reused here for a process that killed itself.
2273pub fn abort_current_kernel() {
2274 ABORTED.with(|c| c.set(true));
2275}
2276
2277/// A `width`-bit window at bit `lo` of a `total`-bit cell: the value (from
2278/// codegen's `val`/`unk` lanes, `n` words each, LSB aligned) in the cell's
2279/// own shape, and the window's mask words.
2280///
2281/// # Safety
2282/// `val`/`unk` address `width.div_ceil(64)` words each.
2283unsafe fn force_window(
2284 total: u32,
2285 wide: bool,
2286 lo: u32,
2287 width: u32,
2288 val: *const u64,
2289 unk: *const u64,
2290) -> (FourStateValue, Vec<u64>) {
2291 let n = width.div_ceil(64).max(1) as usize;
2292 // SAFETY: the caller's contract.
2293 let (val, unk) = unsafe {
2294 (
2295 std::slice::from_raw_parts(val, n),
2296 std::slice::from_raw_parts(unk, n),
2297 )
2298 };
2299 let words = total.div_ceil(64).max(1) as usize;
2300 let (mut v, mut u, mut m) = (vec![0u64; words], vec![0u64; words], vec![0u64; words]);
2301 for i in 0..width {
2302 let (src, dst) = (i as usize, (lo + i) as usize);
2303 if dst >= total as usize {
2304 break;
2305 }
2306 let bit = |w: &[u64]| (w[src / 64] >> (src % 64)) & 1;
2307 v[dst / 64] |= bit(val) << (dst % 64);
2308 u[dst / 64] |= bit(unk) << (dst % 64);
2309 m[dst / 64] |= 1 << (dst % 64);
2310 }
2311 let value = if wide {
2312 FourStateValue::Wide {
2313 aval: v.iter().zip(&u).map(|(a, b)| a ^ b).collect(),
2314 bval: u,
2315 }
2316 } else {
2317 FourStateValue::Narrow(u128::from(v[0]) | (u128::from(u[0]) << 64))
2318 };
2319 (value, m)
2320}
2321
2322/// [`crate::state::slot_state`], reporting (and aborting the running process
2323/// on) a slot `force` cannot reach.
2324fn slot_state_or_abort(
2325 handle: SimHandle,
2326 slot: vj_abi::SlotId,
2327) -> Option<(StateCellId, u32, bool, FourStateValue, u32)> {
2328 match crate::state::slot_state(handle, slot) {
2329 Ok(state) => Some(state),
2330 Err(what) => {
2331 // A refused force must not look like a run that went on: end the
2332 // simulation with a failing exit code (as `$finish(1)` does).
2333 eprintln!("vj: runtime error: {what} is not supported");
2334 real_sv_rt_finish(handle, 1);
2335 None
2336 }
2337 }
2338}
2339
2340/// A flag bit of `sv_rt_force`'s `is_net` and `sv_rt_release`'s `flags`: the
2341/// statement is a procedural `assign`/`deassign`, not a `force`/`release`.
2342pub const FORCE_PROCEDURAL_ASSIGN: u32 = 2;
2343
2344/// A flag bit of `sv_rt_force`'s `is_net`: a `force`'s continuous driver
2345/// re-applying it after an operand changed (LRM 10.6.2), not the statement
2346/// executing -- no force event.
2347pub const FORCE_REFRESH: u32 = 4;
2348
2349/// A force, release, procedural assign or deassign, as reported to an
2350/// embedder that asked for them ([`record_force_events`]): VPI's
2351/// `cbForce`/`cbRelease`/`cbAssign`/`cbDeassign` (IEEE 1800-2023 38.36.1).
2352#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2353pub struct ForceEvent {
2354 /// The forced object's STATE cell.
2355 pub cell: StateCellId,
2356 pub release: bool,
2357 /// `assign`/`deassign` rather than `force`/`release`.
2358 pub procedural: bool,
2359}
2360
2361thread_local! {
2362 /// `Some` while an embedder records force events.
2363 static FORCE_EVENTS: RefCell<Option<Vec<ForceEvent>>> = const { RefCell::new(None) };
2364}
2365
2366/// Starts (or stops) recording force events for [`take_force_events`].
2367pub fn record_force_events(on: bool) {
2368 FORCE_EVENTS.with(|e| *e.borrow_mut() = on.then(Vec::new));
2369}
2370
2371/// The force events since the last call, in order.
2372pub fn take_force_events() -> Vec<ForceEvent> {
2373 FORCE_EVENTS.with(|e| {
2374 e.borrow_mut()
2375 .as_mut()
2376 .map(std::mem::take)
2377 .unwrap_or_default()
2378 })
2379}
2380
2381fn note_force(ev: ForceEvent) {
2382 FORCE_EVENTS.with(|e| {
2383 if let Some(v) = e.borrow_mut().as_mut() {
2384 v.push(ev);
2385 }
2386 });
2387}
2388
2389/// `force lhs = v;` (LRM 1800-2023 §10.6.2): forces the `width`-bit window
2390/// at bit `lo` of `slot` to `v` (codegen's `val`/`unk` lanes) on behalf of
2391/// the `force` statement at source offset `site` (`crate::state::force`).
2392/// `flags`: bit 0 for a net (released, it goes back to its drivers), plus
2393/// [`FORCE_PROCEDURAL_ASSIGN`] and [`FORCE_REFRESH`].
2394///
2395/// # Safety
2396/// `val`/`unk` address `width.div_ceil(64)` words each.
2397#[allow(clippy::too_many_arguments)]
2398#[cfg_attr(feature = "aot-exports", unsafe(export_name = "sv_rt_force"))]
2399pub unsafe extern "C" fn real_sv_rt_force(
2400 handle: SimHandle,
2401 slot: vj_abi::SlotId,
2402 lo: u32,
2403 width: u32,
2404 val: *const u64,
2405 unk: *const u64,
2406 flags: u32,
2407 site: u64,
2408) {
2409 if let Some((row, lo_base)) = crate::state::slot_row_alias(handle, slot) {
2410 // A ref port bound to an element of an unpacked array (LRM
2411 // 23.3.3.2): the force is the element's. A ref port binds a
2412 // variable, never a net, so the net bit the formal's own flags may
2413 // carry does not apply (a released variable keeps its value).
2414 // SAFETY: the caller's contract.
2415 unsafe {
2416 force_row_impl(
2417 handle,
2418 slot,
2419 row,
2420 lo + lo_base,
2421 width,
2422 val,
2423 unk,
2424 flags & !1,
2425 site,
2426 );
2427 }
2428 return;
2429 }
2430 let Some((cell, total, wide, current, lo_base)) = slot_state_or_abort(handle, slot) else {
2431 return;
2432 };
2433 let lo = lo + lo_base;
2434 // SAFETY: the caller's contract.
2435 let (value, mask) = unsafe { force_window(total, wide, lo, width, val, unk) };
2436 let site = (handle.0 as u64, site);
2437 if flags & FORCE_REFRESH == 0 {
2438 note_force(ForceEvent {
2439 cell,
2440 release: false,
2441 procedural: flags & FORCE_PROCEDURAL_ASSIGN != 0,
2442 });
2443 }
2444 let write = crate::state::with_force_table(handle, |t| {
2445 let w = t.force(
2446 cell,
2447 value,
2448 mask,
2449 flags & 1 != 0,
2450 site,
2451 current,
2452 flags & FORCE_PROCEDURAL_ASSIGN != 0,
2453 );
2454 t.set_bypass(true);
2455 w
2456 });
2457 with_current_ops(|ops| ops.commit_blocking_write(cell, write));
2458 crate::state::with_force_table(handle, |t| t.set_bypass(false));
2459}
2460
2461/// `release lhs;`: ends the force of the `width`-bit window at bit `lo` of
2462/// `slot`; a net goes back to its driven value. `flags` is
2463/// [`FORCE_PROCEDURAL_ASSIGN`] for a `deassign`, 0 for a `release`.
2464#[cfg_attr(feature = "aot-exports", unsafe(export_name = "sv_rt_release"))]
2465pub extern "C" fn real_sv_rt_release(
2466 handle: SimHandle,
2467 slot: vj_abi::SlotId,
2468 lo: u32,
2469 width: u32,
2470 flags: u32,
2471) {
2472 if let Some((row, lo_base)) = crate::state::slot_row_alias(handle, slot) {
2473 release_row_impl(handle, slot, row, lo + lo_base, width, flags);
2474 return;
2475 }
2476 let Some((cell, total, wide, _, lo_base)) = slot_state_or_abort(handle, slot) else {
2477 return;
2478 };
2479 let lo = lo + lo_base;
2480 note_force(ForceEvent {
2481 cell,
2482 release: true,
2483 procedural: flags & FORCE_PROCEDURAL_ASSIGN != 0,
2484 });
2485 let ones = vec![u64::MAX; width.div_ceil(64).max(1) as usize];
2486 // SAFETY: `ones` has the window's word count, used for both lanes.
2487 let (_, mask) = unsafe { force_window(total, wide, lo, width, ones.as_ptr(), ones.as_ptr()) };
2488 let procedural = flags & FORCE_PROCEDURAL_ASSIGN != 0;
2489 if let Some(restore) =
2490 crate::state::with_force_table(handle, |t| t.release(cell, &mask, procedural))
2491 {
2492 // The restored value is not a new driver write: with other windows
2493 // still forced it must not overwrite the entry's `driven` bits with
2494 // their forced values (LRM 10.6.2), so it bypasses the filter.
2495 crate::state::with_force_table(handle, |t| t.set_bypass(true));
2496 with_current_ops(|ops| ops.commit_blocking_write(cell, restore));
2497 crate::state::with_force_table(handle, |t| t.set_bypass(false));
2498 }
2499}
2500
2501/// `force`/`assign` of row `row` of the unpacked array in `slot` (LRM
2502/// 1800-2023 10.6.2): forces the `width`-bit window at bit `lo` of that
2503/// element. [`real_sv_rt_force`] for one memory row; `flags` as there.
2504///
2505/// # Safety
2506/// `val`/`unk` address `width.div_ceil(64)` words each.
2507#[allow(clippy::too_many_arguments)]
2508unsafe fn force_row_impl(
2509 handle: SimHandle,
2510 slot: vj_abi::SlotId,
2511 row: u64,
2512 lo: u32,
2513 width: u32,
2514 val: *const u64,
2515 unk: *const u64,
2516 flags: u32,
2517 site: u64,
2518) {
2519 let (cell, at, total, current) = crate::state::row_state(handle, slot, row);
2520 // SAFETY: the caller's contract; a memory row is always wide.
2521 let (value, mask) = unsafe { force_window(total, true, lo, width, val, unk) };
2522 let procedural = flags & FORCE_PROCEDURAL_ASSIGN != 0;
2523 if flags & FORCE_REFRESH == 0 {
2524 note_force(ForceEvent {
2525 cell,
2526 release: false,
2527 procedural,
2528 });
2529 }
2530 let site = (handle.0 as u64, site);
2531 let write = crate::state::with_force_table(handle, |t| {
2532 let w = t.force_row(
2533 cell,
2534 at,
2535 value,
2536 mask,
2537 flags & 1 != 0,
2538 site,
2539 current,
2540 procedural,
2541 );
2542 t.set_bypass(true);
2543 w
2544 });
2545 commit_row(handle, slot, row, write);
2546 crate::state::with_force_table(handle, |t| t.set_bypass(false));
2547}
2548
2549/// `release`/`deassign` of the `width`-bit window at bit `lo` of row `row`
2550/// of `slot`: [`real_sv_rt_release`] for one memory row.
2551fn release_row_impl(
2552 handle: SimHandle,
2553 slot: vj_abi::SlotId,
2554 row: u64,
2555 lo: u32,
2556 width: u32,
2557 flags: u32,
2558) {
2559 let (cell, at, total, _) = crate::state::row_state(handle, slot, row);
2560 let procedural = flags & FORCE_PROCEDURAL_ASSIGN != 0;
2561 note_force(ForceEvent {
2562 cell,
2563 release: true,
2564 procedural,
2565 });
2566 let ones = vec![u64::MAX; width.div_ceil(64).max(1) as usize];
2567 // SAFETY: `ones` has the window's word count, used for both lanes.
2568 let (_, mask) = unsafe { force_window(total, true, lo, width, ones.as_ptr(), ones.as_ptr()) };
2569 if let Some(restore) =
2570 crate::state::with_force_table(handle, |t| t.release_row(cell, at, &mask, procedural))
2571 {
2572 commit_row(handle, slot, row, restore);
2573 }
2574}
2575
2576/// VPI `vpi_put_value(vpiForceFlag)` on row `row` of the unpacked array in
2577/// `slot` (IEEE 1800-2023 38.34, 10.6.2): [`force_row_impl`], `is_net` the
2578/// net flag.
2579///
2580/// # Safety
2581/// `val`/`unk` address `width.div_ceil(64)` words each.
2582#[allow(clippy::too_many_arguments)]
2583pub unsafe fn vpi_force_row(
2584 handle: SimHandle,
2585 slot: vj_abi::SlotId,
2586 row: u64,
2587 lo: u32,
2588 width: u32,
2589 val: *const u64,
2590 unk: *const u64,
2591 is_net: bool,
2592 site: u64,
2593) {
2594 // SAFETY: the caller's contract.
2595 unsafe {
2596 force_row_impl(
2597 handle,
2598 slot,
2599 row,
2600 lo,
2601 width,
2602 val,
2603 unk,
2604 u32::from(is_net),
2605 site,
2606 );
2607 }
2608}
2609
2610/// VPI `vpi_put_value(vpiReleaseFlag)` on a row: [`release_row_impl`].
2611pub fn vpi_release_row(handle: SimHandle, slot: vj_abi::SlotId, row: u64, lo: u32, width: u32) {
2612 release_row_impl(handle, slot, row, lo, width, 0);
2613}
2614
2615/// A flag bit of `sv_rt_force_row`'s `flags`: the call is a release (a
2616/// deassign with [`FORCE_PROCEDURAL_ASSIGN`]); `val`/`unk` are unused.
2617pub const FORCE_ROW_RELEASE: u32 = 8;
2618
2619/// `force mem[i] = v;` / `release mem[i];` (and `assign`/`deassign`) on row
2620/// `row` of the unpacked array in `slot` (LRM 1800-2023 10.6.2):
2621/// [`real_sv_rt_force`]/[`real_sv_rt_release`] for one memory row, the
2622/// release when `flags` has [`FORCE_ROW_RELEASE`].
2623///
2624/// # Safety
2625/// `val`/`unk` address `width.div_ceil(64)` words each (unused for a
2626/// release).
2627#[allow(clippy::too_many_arguments)]
2628#[cfg_attr(feature = "aot-exports", unsafe(export_name = "sv_rt_force_row"))]
2629pub unsafe extern "C" fn real_sv_rt_force_row(
2630 handle: SimHandle,
2631 slot: vj_abi::SlotId,
2632 row: u64,
2633 lo: u32,
2634 width: u32,
2635 val: *const u64,
2636 unk: *const u64,
2637 flags: u32,
2638 site: u64,
2639) {
2640 if flags & FORCE_ROW_RELEASE != 0 {
2641 release_row_impl(handle, slot, row, lo, width, flags);
2642 } else {
2643 // SAFETY: the caller's contract.
2644 unsafe { force_row_impl(handle, slot, row, lo, width, val, unk, flags, site) };
2645 }
2646}
2647
2648/// Stores `v` (a wide value) into row `row` of `slot` through the one write
2649/// primitive and wakes what is sensitive to the memory.
2650fn commit_row(handle: SimHandle, slot: vj_abi::SlotId, row: u64, v: FourStateValue) {
2651 let FourStateValue::Wide { mut aval, mut bval } = v else {
2652 return;
2653 };
2654 let view = vj_abi::accessor::WideView {
2655 aval: aval.as_mut_ptr(),
2656 bval: bval.as_mut_ptr(),
2657 word_count: aval.len() as u32,
2658 };
2659 let (handle, slot) = crate::state::resolve_foreign_slot(handle, slot);
2660 let (cell, changed) = crate::state::set_mem_word_reporting_change(handle, slot, row, view);
2661 if changed {
2662 with_current_ops(|ops| ops.notify_blocking_row_change(cell));
2663 }
2664}
2665
2666/// Whether the `force` statement at source offset `site` of `handle`'s
2667/// instance is still in effect: its continuous driver re-forces only while
2668/// it is.
2669#[cfg_attr(feature = "aot-exports", unsafe(export_name = "sv_rt_force_active"))]
2670pub extern "C" fn real_sv_rt_force_active(handle: SimHandle, site: u64) -> u32 {
2671 u32::from(crate::state::with_force_table(handle, |t| {
2672 t.site_active((handle.0 as u64, site))
2673 }))
2674}
2675
2676/// `$countdrivers` (IEEE 1800-2023 D.2, Table D.1) of bit `bit` of the net in
2677/// `slot`, whose strength key is `key`: writes `out[0..5]` = net_is_forced,
2678/// number_of_01x_drivers, number_of_0_drivers, number_of_1_drivers,
2679/// number_of_x_drivers. A net a resolver drives has its recorded counts; any
2680/// other net has at most one driver, so its value (the one its driver last
2681/// wrote, when forced) says whether that driver is at 0, 1, x or `z`.
2682///
2683/// # Safety
2684///
2685/// `out` points to five writable `u32`.
2686#[cfg_attr(feature = "aot-exports", unsafe(export_name = "sv_rt_net_drivers"))]
2687pub unsafe extern "C" fn real_sv_rt_net_drivers(
2688 handle: SimHandle,
2689 slot: vj_abi::SlotId,
2690 key: u64,
2691 bit: u32,
2692 out: *mut u32,
2693) {
2694 // Every exit leaves `out` defined: the aborting paths below return with the
2695 // run stopped, but the calling process still reads the five words, and an
2696 // unwritten stack slot made the JIT and AOT builds print different values.
2697 for i in 0..5 {
2698 // SAFETY: the caller's contract.
2699 unsafe { out.add(i).write(0) };
2700 }
2701 let Some((cell, _total, _wide, current, lo_base)) = slot_state_or_abort(handle, slot) else {
2702 return;
2703 };
2704 let (forced, driven) =
2705 crate::state::with_force_table(handle, |t| (t.contains(cell), t.driven_value(cell)));
2706 let counts = crate::state::driver_counts_of(key, bit).unwrap_or_else(|| {
2707 let v = driven.as_ref().unwrap_or(¤t);
2708 match crate::state::four_state_bit_at(v, bit + lo_base) {
2709 vj_abi::Bit4::Zero => [1, 0, 0],
2710 vj_abi::Bit4::One => [0, 1, 0],
2711 vj_abi::Bit4::X => [0, 0, 1],
2712 vj_abi::Bit4::Z => [0, 0, 0],
2713 }
2714 });
2715 if counts == crate::state::UNCOUNTED {
2716 eprintln!(
2717 "vj: runtime error: $countdrivers of a net in a bidirectional switch network is not supported"
2718 );
2719 real_sv_rt_finish(handle, 1);
2720 return;
2721 }
2722 let all = [
2723 u32::from(forced),
2724 counts[0] + counts[1] + counts[2],
2725 counts[0],
2726 counts[1],
2727 counts[2],
2728 ];
2729 for (i, v) in all.into_iter().enumerate() {
2730 // SAFETY: the caller's contract.
2731 unsafe { out.add(i).write(v) };
2732 }
2733}
2734
2735/// A `$strobe`/`$monitor` argument thunk codegen emits per call site:
2736/// re-reads the call's arguments from STATE and prints them (LRM 21.2.2/
2737/// 21.2.3 evaluate those arguments at the END of the timestep, not at the
2738/// call).
2739pub type PostponedThunk = extern "C" fn(SimHandle);
2740
2741struct StrobeCallback {
2742 thunk: PostponedThunk,
2743 handle: SimHandle,
2744}
2745
2746// SAFETY: `handle` is the kernel's own instance `SimHandle`, only ever
2747// passed back to the thunk on the scheduler's own thread (same obligation
2748// `CompiledKernelProcess`'s `unsafe impl Send` documents).
2749unsafe impl Send for StrobeCallback {}
2750
2751impl super::RegionCallback for StrobeCallback {
2752 fn invoke(&mut self, _ctx: &dyn super::SchedulerCtx) {
2753 (self.thunk)(self.handle);
2754 }
2755}
2756
2757/// `$strobe(...)`: run `thunk` once, in this timestep's Postponed region.
2758#[cfg_attr(
2759 feature = "aot-exports",
2760 unsafe(export_name = "sv_rt_strobe_postponed")
2761)]
2762pub extern "C" fn real_sv_rt_strobe(handle: SimHandle, thunk: PostponedThunk) {
2763 with_current_ops(|ops| {
2764 ops.register_region_callback(
2765 vj_abi::Region::Postponed,
2766 ops.now(),
2767 Box::new(StrobeCallback { thunk, handle }),
2768 );
2769 });
2770}
2771
2772thread_local! {
2773 /// The active `$monitor`'s generation (0 = none yet). Only one monitor
2774 /// is ever active (LRM 21.2.3): starting a new one bumps this, which
2775 /// retires the old one's callback. Thread-local: one simulation runs on
2776 /// one thread.
2777 static MONITOR_GENERATION: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
2778 /// `$monitoron`/`$monitoroff`.
2779 static MONITOR_ENABLED: std::cell::Cell<bool> = const { std::cell::Cell::new(true) };
2780}
2781
2782/// High bit of `sv_rt_monitor_start`'s slot count: the slot array carries one
2783/// extra trailing entry, the mask of `$time`-like display values, and the
2784/// monitor prints only when some other value differs from the last print
2785/// (LRM 21.2.3: "each time a variable or an expression in the argument list
2786/// changes value").
2787pub const MONITOR_DEDUPE_N: u32 = 1 << 31;
2788
2789/// What one monitor-thunk call is doing (see [`MonitorDedupe`]).
2790#[derive(Clone, Copy, PartialEq, Eq)]
2791enum MonitorMode {
2792 /// Not inside a thunk call.
2793 Idle,
2794 /// Evaluating the arguments at a watched-cell update: the call compares
2795 /// them with the last evaluated values, sets `due` and prints nothing.
2796 Probe,
2797 /// The Postponed print: records the printed values and prints.
2798 Print,
2799}
2800
2801/// The active plain `$monitor`'s change-test state (LRM 21.2.3: a display is
2802/// due when some argument EXPRESSION took a different value at some update
2803/// during the step, even if a later update restored it): the mask of
2804/// `$time`-like display values, the values (as an opaque key) last evaluated,
2805/// and the per-step "due" flag.
2806struct MonitorDedupe {
2807 mask: u32,
2808 mode: MonitorMode,
2809 last: Option<Vec<u8>>,
2810 /// A display is owed at the end of this step (set by a probe that saw a
2811 /// changed expression, or by `$monitoron`; cleared by the print).
2812 due: bool,
2813 /// The thunk and instance handle a watched-cell update re-evaluates.
2814 probe: Option<(PostponedThunk, SimHandle)>,
2815}
2816
2817thread_local! {
2818 static MONITOR_DEDUPE: std::cell::RefCell<Option<MonitorDedupe>> =
2819 const { std::cell::RefCell::new(None) };
2820}
2821
2822/// A new `$monitor` starts: `Some(mask)` turns the expression-value change
2823/// test on (bit `i` of `mask` exempts display value `i`), `None` off. The
2824/// first display is always due.
2825pub fn monitor_dedupe_reset(mask: Option<u32>) {
2826 MONITOR_DEDUPE.with(|d| {
2827 *d.borrow_mut() = mask.map(|mask| MonitorDedupe {
2828 mask,
2829 mode: MonitorMode::Idle,
2830 last: None,
2831 due: true,
2832 probe: None,
2833 });
2834 });
2835}
2836
2837fn monitor_dedupe_set_mode(mode: MonitorMode) {
2838 MONITOR_DEDUPE.with(|d| {
2839 if let Some(m) = d.borrow_mut().as_mut() {
2840 m.mode = mode;
2841 }
2842 });
2843}
2844
2845/// Arms (or disarms) the print-side change record around one monitor thunk
2846/// call from the Postponed callback.
2847pub fn monitor_dedupe_arm(on: bool) {
2848 monitor_dedupe_set_mode(if on {
2849 MonitorMode::Print
2850 } else {
2851 MonitorMode::Idle
2852 });
2853}
2854
2855/// A watched cell changed value: when the active monitor tests expression
2856/// values, evaluates its arguments now (display suppressed) and marks a
2857/// display due if they differ from the last evaluation.
2858pub fn monitor_probe() {
2859 let probe = MONITOR_DEDUPE.with(|d| {
2860 d.borrow()
2861 .as_ref()
2862 .filter(|m| m.mode == MonitorMode::Idle)
2863 .and_then(|m| m.probe)
2864 });
2865 if let Some((thunk, handle)) = probe {
2866 monitor_dedupe_set_mode(MonitorMode::Probe);
2867 thunk(handle);
2868 monitor_dedupe_set_mode(MonitorMode::Idle);
2869 }
2870}
2871
2872/// `true` while the active `$monitor` tests expression values.
2873fn monitor_dedupe_active() -> bool {
2874 MONITOR_DEDUPE.with(|d| d.borrow().is_some())
2875}
2876
2877/// Reads and clears the active monitor's per-step "display due" flag.
2878fn monitor_dedupe_take_due() -> bool {
2879 MONITOR_DEDUPE.with(|d| {
2880 d.borrow_mut()
2881 .as_mut()
2882 .is_some_and(|m| std::mem::take(&mut m.due))
2883 })
2884}
2885
2886/// Builds this call's value key (`make_key` gets the `$time`-like mask) and
2887/// applies the mode: `Probe` compares it with the last evaluation, remembers
2888/// it, marks the display due on a difference, and returns `true` (suppress the
2889/// print); `Print` remembers it and returns `false`. Always `false` otherwise.
2890pub fn monitor_dedupe_check(make_key: impl FnOnce(u32) -> Vec<u8>) -> bool {
2891 MONITOR_DEDUPE.with(|d| {
2892 let mut d = d.borrow_mut();
2893 let Some(m) = d.as_mut().filter(|m| m.mode != MonitorMode::Idle) else {
2894 return false;
2895 };
2896 let key = make_key(m.mask);
2897 let probing = m.mode == MonitorMode::Probe;
2898 if probing && m.last.as_deref() != Some(key.as_slice()) {
2899 m.due = true;
2900 }
2901 m.last = Some(key);
2902 probing
2903 })
2904}
2905
2906struct MonitorCallback {
2907 thunk: PostponedThunk,
2908 handle: SimHandle,
2909 generation: u64,
2910}
2911
2912// SAFETY: see `StrobeCallback`.
2913unsafe impl Send for MonitorCallback {}
2914
2915impl super::RegionCallback for MonitorCallback {
2916 fn invoke(&mut self, ctx: &dyn super::SchedulerCtx) {
2917 if MONITOR_GENERATION.with(|g| g.get()) != self.generation {
2918 return; // superseded by a later `$monitor`: retire (no re-arm)
2919 }
2920 // Print only if a watched argument signal changed this timestep
2921 // (a change made while `$monitoroff` is held over until re-enabled).
2922 // Under the expression-value change test the probes decided whether
2923 // a display is due; otherwise any watched-signal change is.
2924 if MONITOR_ENABLED.with(|e| e.get()) {
2925 let changed = ctx.take_monitor_changed();
2926 let dedupe = monitor_dedupe_active();
2927 if (dedupe && monitor_dedupe_take_due()) || (!dedupe && changed) {
2928 monitor_dedupe_arm(true);
2929 (self.thunk)(self.handle);
2930 monitor_dedupe_arm(false);
2931 }
2932 }
2933 ctx.rearm(
2934 vj_abi::Region::Postponed,
2935 Box::new(MonitorCallback {
2936 thunk: self.thunk,
2937 handle: self.handle,
2938 generation: self.generation,
2939 }),
2940 );
2941 }
2942}
2943
2944/// `$monitor(...)`: make `thunk` the one active monitor. It is checked in the
2945/// Postponed region of this and every later timestep and prints whenever
2946/// one of its argument signals -- `slots`, this instance's template slot
2947/// numbers, `n` of them -- changed value during the timestep (LRM 21.2.3;
2948/// `$time`-like arguments are not signals, so their change alone never
2949/// triggers a print).
2950///
2951/// # Safety
2952/// `slots` must point at `n` readable `u32`s (codegen passes a constant
2953/// global array) and `handle` must be the calling kernel's instance handle.
2954#[cfg_attr(feature = "aot-exports", unsafe(export_name = "sv_rt_monitor_start"))]
2955pub unsafe extern "C" fn real_sv_rt_monitor(
2956 handle: SimHandle,
2957 thunk: PostponedThunk,
2958 slots: *const u32,
2959 n: u32,
2960) {
2961 let generation = MONITOR_GENERATION.with(|g| {
2962 let next = g.get() + 1;
2963 g.set(next);
2964 next
2965 });
2966 // High bit of `n`: one trailing entry holds the `$time`-like mask and
2967 // the monitor prints only when an argument expression's value changed.
2968 let dedupe = n & MONITOR_DEDUPE_N != 0;
2969 let n = n & !MONITOR_DEDUPE_N;
2970 let all: &[u32] = if n == 0 && !dedupe {
2971 &[]
2972 } else {
2973 // SAFETY: forwarded to this function's own `# Safety` section.
2974 unsafe { std::slice::from_raw_parts(slots, n as usize + usize::from(dedupe)) }
2975 };
2976 let (slots, mask) = if dedupe {
2977 (&all[..n as usize], Some(all[n as usize]))
2978 } else {
2979 (all, None)
2980 };
2981 monitor_dedupe_reset(mask);
2982 if mask.is_some() {
2983 MONITOR_DEDUPE.with(|d| {
2984 if let Some(m) = d.borrow_mut().as_mut() {
2985 m.probe = Some((thunk, handle));
2986 }
2987 });
2988 }
2989 let cells: Vec<StateCellId> = slots
2990 .iter()
2991 .map(|&s| crate::state::cell_for_slot(handle, vj_abi::SlotId(s)))
2992 .collect();
2993 with_current_ops(|ops| {
2994 ops.set_monitor_watch(cells);
2995 ops.register_region_callback(
2996 vj_abi::Region::Postponed,
2997 ops.now(),
2998 Box::new(MonitorCallback {
2999 thunk,
3000 handle,
3001 generation,
3002 }),
3003 );
3004 });
3005}
3006
3007/// `$monitoron` / `$monitoroff` (LRM 21.2.3).
3008#[cfg_attr(feature = "aot-exports", unsafe(export_name = "sv_rt_monitor_enable"))]
3009pub extern "C" fn real_sv_rt_monitor_enable(_handle: SimHandle, on: i32) {
3010 MONITOR_ENABLED.with(|e| e.set(on != 0));
3011 // LRM 21.2.3: `$monitoron` produces a display whether or not a value
3012 // changed (under the expression-value change test; the per-signal form
3013 // prints only held-over changes).
3014 if on != 0 {
3015 MONITOR_DEDUPE.with(|d| {
3016 if let Some(m) = d.borrow_mut().as_mut() {
3017 m.due = true;
3018 }
3019 });
3020 }
3021}
3022
3023thread_local! {
3024 /// The time step a concurrent assertion's leaf is being evaluated for, while the
3025 /// engine re-runs an attempt over its window (`sva::engine`): `$time`/`$realtime`
3026 /// in an assertion expression is the time of the position the leaf is sampled at
3027 /// (LRM 1800-2023 16.5.1), not that of the later step doing the re-run.
3028 static SVA_LEAF_TIME: std::cell::Cell<Option<u64>> = const { std::cell::Cell::new(None) };
3029}
3030
3031/// Runs `f` with `$time` answering `ticks` ([`SVA_LEAF_TIME`]).
3032pub fn with_sva_leaf_time<R>(ticks: u64, f: impl FnOnce() -> R) -> R {
3033 let prev = SVA_LEAF_TIME.replace(Some(ticks));
3034 let r = f();
3035 SVA_LEAF_TIME.set(prev);
3036 r
3037}
3038
3039/// Task F2 (`docs/superpowers/plans/2026-09-27-wave-A2-slice-finish.md`,
3040/// RT-043 partial): the real production body for the frozen `sv_rt_time`
3041/// ABI symbol (`vj_codegen::expr::SV_RT_TIME_SYMBOL`) that `$time`/
3042/// `$stime`/`$realtime` lower to -- reaches the scheduler's real
3043/// [`SchedulerOps::now`] through the same [`CURRENT_OPS`] thread-local
3044/// bridge [`real_nba_enqueue`]/[`real_sv_rt_finish`] above already use.
3045/// Bound the same way as those two (`Jit::add_global_mapping(time_fv,
3046/// real_sv_rt_time as *const () as usize)`, `vj::run`).
3047///
3048/// Takes no `SimHandle` parameter at all -- a deviation both from those two
3049/// functions' own shape and from the plan's literal `sv_rt_time(state:
3050/// SimHandle) -> u64` sketch, and load-bearing, not cosmetic:
3051/// `vj-codegen::expr::fetch_or_declare_sv_rt_time` declares the LLVM
3052/// side of this ABI boundary as a genuinely zero-argument function (that doc
3053/// comment records why codegen's call site has no `SimHandle`-carrying value
3054/// in scope to pass), so this Rust function's own signature must match it
3055/// exactly -- an extra Rust-side parameter here would desync the `extern
3056/// "C"` calling convention from the zero-argument call site `build_call`
3057/// emits (nothing would ever populate it, unlike [`real_sv_rt_finish`],
3058/// whose `_handle` parameter IS genuinely passed a real argument by its own
3059/// two-argument call site, just never read). The current time is
3060/// scheduler-global, not per-instance, so no `SimHandle` is needed here
3061/// either way.
3062///
3063/// A miss (`with_current_ops` finds no `CURRENT_OPS` set) falls through to
3064/// `0` -- unreachable for every kernel this tree's own codegen emits, per
3065/// [`with_current_ops`]'s own doc comment, not a silently-wrong nonzero
3066/// default masking a real bug.
3067#[cfg_attr(feature = "aot-exports", unsafe(export_name = "sv_rt_time"))]
3068pub extern "C" fn real_sv_rt_time() -> u64 {
3069 if let Some(t) = SVA_LEAF_TIME.get() {
3070 return t;
3071 }
3072 let mut ticks: u64 = 0;
3073 with_current_ops(|ops| ticks = ops.now().ticks);
3074 ticks
3075}
3076
3077/// The reconciliation note this module's own doc comment describes --
3078/// `sv_kernel_abi_reconciliation_note_exists_and_is_referenced` (below) is
3079/// the cheap compile-level proof it is not silently dropped in a later
3080/// refactor.
3081pub const SV_KERNEL_ABI_RECONCILIATION: &str = "CG-084/CG-111..CG-116 (docs/design/codegen.md §9's SvKernelTable/SvProcessDesc/SvWakeSpec/\
3082 SvResumeStatus literal draft) is superseded by ARCHITECTURE.md §5's frozen ABI \
3083 (SimHandle/ProcessFn/EvalResult/Checkpoint), the same reconciliation PR-037/PR-107 already \
3084 record for the process-model side. CompiledKernelProcess (vj-sim::sched::kernel_adapter) \
3085 and lower_kernel_fn (vj-codegen::process_lower) implement the frozen shape; the old \
3086 struct names are never constructed anywhere in this tree.";
3087
3088#[cfg(test)]
3089mod tests {
3090 use super::*;
3091 use crate::sched::Region;
3092
3093 /// Minimal `SchedulerOps` double: `CompiledKernelProcess::resume` never
3094 /// CALLS A METHOD on `ops` itself (it only calls the raw kernel fn
3095 /// pointer, and, since Task S7, stores `ops`'s address in
3096 /// `CURRENT_OPS` for that call's duration -- see `with_scheduler_ops`),
3097 /// so every method here is unreachable in these tests -- kept total (no
3098 /// `unimplemented!`) since a future `resume` revision that starts using
3099 /// `ops` should get a real (if trivial) answer, not a panic.
3100 struct NoOpSchedulerOps;
3101 impl SchedulerOps for NoOpSchedulerOps {
3102 fn now(&self) -> crate::sched::SchedTime {
3103 crate::sched::SchedTime { ticks: 0 }
3104 }
3105 fn current_region(&self) -> Region {
3106 Region::Active
3107 }
3108 fn stage_nba(
3109 &self,
3110 _cell: StateCellId,
3111 _value: crate::state::FourStateValue,
3112 _source: u32,
3113 ) {
3114 }
3115 fn commit_blocking_write(&self, _cell: StateCellId, _value: crate::state::FourStateValue) {}
3116 fn request_finish(&self, _code: i32) {}
3117 fn defer_io(
3118 &self,
3119 _at: crate::sched::SchedTime,
3120 _region: Region,
3121 _cb: crate::sched::DeferredIoCallback,
3122 ) {
3123 }
3124 fn register_region_callback(
3125 &self,
3126 _region: Region,
3127 _at: crate::sched::SchedTime,
3128 _cb: Box<dyn crate::sched::RegionCallback>,
3129 ) {
3130 }
3131 }
3132
3133 /// PR-012: `resume_pc == 0` means "fresh/never-run" -- a hand-written
3134 /// stub kernel asserts it was actually called with `0` on the first call,
3135 /// and reports a delay suspend the adapter must decode+persist correctly.
3136 extern "C" fn stub_kernel_initial_dispatch(
3137 _h: SimHandle,
3138 _local: *mut u8,
3139 resume_pc: u32,
3140 ) -> EvalResult {
3141 assert_eq!(
3142 resume_pc, 0,
3143 "PR-012: the first call to a freshly constructed CompiledKernelProcess must pass \
3144 resume_pc == 0"
3145 );
3146 build_eval_result(ResolvedSuspend::Delay(5), 1)
3147 }
3148
3149 #[test]
3150 fn resume_at_pc_zero_is_initial_dispatch() {
3151 let mut proc = CompiledKernelProcess::new(
3152 stub_kernel_initial_dispatch,
3153 0,
3154 SimHandle(std::ptr::null_mut()),
3155 );
3156 assert_eq!(proc.resume_pc, 0);
3157 let ops = NoOpSchedulerOps;
3158 let suspend = proc.resume(SimHandle(std::ptr::null_mut()), ResumeArg::Start, &ops);
3159 match suspend {
3160 Suspend::Delay(t) => assert_eq!(t, 5),
3161 other => panic!("expected Suspend::Delay(5), got {other:?}"),
3162 }
3163 assert_eq!(
3164 proc.resume_pc, 1,
3165 "the adapter must persist the decoded next resume_pc for the following call"
3166 );
3167 }
3168
3169 #[test]
3170 fn resume_pc_round_trips_through_evalresult_and_reschedulereq() {
3171 let er_edge = build_eval_result(
3172 ResolvedSuspend::Edge {
3173 cell: StateCellId(7),
3174 edge: EdgeKind::Posedge,
3175 },
3176 3,
3177 );
3178 match decode_reschedule(er_edge).expect("KIND_EDGE must decode to Ok") {
3179 Suspend::WaitEvents(set) => {
3180 assert_eq!(set.terms.len(), 1);
3181 match &set.terms[0] {
3182 EventTerm::Edge { cell, edge, iff } => {
3183 assert_eq!(*cell, StateCellId(7));
3184 assert_eq!(*edge, EdgeKind::Posedge);
3185 assert!(iff.is_none());
3186 }
3187 other => panic!("expected EventTerm::Edge, got {other:?}"),
3188 }
3189 }
3190 other => panic!("expected Suspend::WaitEvents, got {other:?}"),
3191 }
3192
3193 let er_delay = build_eval_result(ResolvedSuspend::Delay(10), 2);
3194 match decode_reschedule(er_delay).expect("KIND_DELAY must decode to Ok") {
3195 Suspend::Delay(t) => assert_eq!(t, 10),
3196 other => panic!("expected Suspend::Delay(10), got {other:?}"),
3197 }
3198
3199 let er_done = build_eval_result(ResolvedSuspend::Done, 0);
3200 match decode_reschedule(er_done).expect("KIND_DONE must decode to Ok") {
3201 Suspend::Done => {}
3202 other => panic!("expected Suspend::Done, got {other:?}"),
3203 }
3204 }
3205
3206 /// Classes plan Task 13: `ResolvedSuspend::ByRequest` (the wire form of
3207 /// `p.await()` / `p.suspend()`) round-trips to `Suspend::ByRequest` on
3208 /// its own wire kind -- never aliased to `Done`/`Delay`, which would
3209 /// terminate or re-time the process instead of resuming it.
3210 #[test]
3211 fn by_request_round_trips_through_reschedule_req() {
3212 let er = build_eval_result(ResolvedSuspend::ByRequest, 9);
3213 match decode_reschedule(er).expect("KIND_BY_REQUEST must decode to Ok") {
3214 Suspend::ByRequest => {}
3215 other => panic!("expected Suspend::ByRequest, got {other:?}"),
3216 }
3217 }
3218
3219 /// Task 6 (CG-081/CG-171): `build_eval_result`/`decode_reschedule` round
3220 /// trip for `ResolvedSuspend::RepeatEdge` -- the wire tag `SuspendPoint::
3221 /// CycleDelay` resolves into. Asserts the decoded `Suspend::WaitEvents`
3222 /// carries exactly one `EventTerm::Repeat{count, inner: Edge{cell,
3223 /// edge}}` term -- the raw, un-multiplied cycle count, never a
3224 /// pre-resolved tick offset (ARCHITECTURE §6.5's own "converted at
3225 /// runtime" requirement, proven bit-for-bit at this wire layer).
3226 #[test]
3227 fn repeat_edge_round_trips_through_reschedule_req_as_a_live_repeat_over_edge_term() {
3228 let er = build_eval_result(
3229 ResolvedSuspend::RepeatEdge {
3230 count: 3,
3231 cell: StateCellId(11),
3232 edge: EdgeKind::Posedge,
3233 },
3234 4,
3235 );
3236 match decode_reschedule(er).expect("KIND_REPEAT_EDGE must decode to Ok") {
3237 Suspend::WaitEvents(set) => {
3238 assert_eq!(set.terms.len(), 1);
3239 match &set.terms[0] {
3240 EventTerm::Repeat { count, inner } => {
3241 assert_eq!(*count, 3);
3242 match inner.as_ref() {
3243 EventTerm::Edge { cell, edge, iff } => {
3244 assert_eq!(*cell, StateCellId(11));
3245 assert_eq!(*edge, EdgeKind::Posedge);
3246 assert!(iff.is_none());
3247 }
3248 other => panic!("expected EventTerm::Edge, got {other:?}"),
3249 }
3250 }
3251 other => panic!("expected EventTerm::Repeat, got {other:?}"),
3252 }
3253 }
3254 other => panic!("expected Suspend::WaitEvents, got {other:?}"),
3255 }
3256 }
3257
3258 /// Task 6: the repeat count's full `u64` range survives the wire's `a`/
3259 /// `b` split -- mirrors `KIND_DELAY`'s own near-`u64::MAX` proof.
3260 #[test]
3261 fn repeat_edge_count_round_trips_near_u64_max_through_the_wire() {
3262 let near_max: u64 = u64::MAX - 1;
3263 let er = build_eval_result(
3264 ResolvedSuspend::RepeatEdge {
3265 count: near_max,
3266 cell: StateCellId(1),
3267 edge: EdgeKind::Negedge,
3268 },
3269 0,
3270 );
3271 match decode_reschedule(er).expect("KIND_REPEAT_EDGE must decode to Ok") {
3272 Suspend::WaitEvents(set) => match &set.terms[0] {
3273 EventTerm::Repeat { count, .. } => assert_eq!(*count, near_max),
3274 other => panic!("expected EventTerm::Repeat, got {other:?}"),
3275 },
3276 other => panic!("expected Suspend::WaitEvents, got {other:?}"),
3277 }
3278 }
3279
3280 /// **Review fix (wB-sched Task 5 r1) -- the reachability proof behind
3281 /// `docs/CONTRACTS.md`'s "Partial closures (wave B)" / SC-068 entry.**
3282 /// [`ResolvedSuspend::Edge`] (this module's own boundary type, and the
3283 /// *only* shape `vj-codegen::process_lower::resolve_suspend_point`
3284 /// -- the one real, non-test producer of a suspend request for a
3285 /// compiled kernel -- can construct for an edge wait) has no `iff` field
3286 /// at all, so no real compiled kernel can ever cross this ABI boundary
3287 /// with one: [`build_eval_result`]/[`decode_reschedule`]'s round trip
3288 /// always yields `EventTerm::Edge { iff: None, .. }`, regardless of
3289 /// whether the original SystemVerilog wait was `iff`-qualified
3290 /// (`@(posedge clk iff en)`). This is why `EngineNotifySink::
3291 /// notify_change`'s `Some(pred)` filter branch (`sched/control.rs`,
3292 /// SC-068) is exercised, today, only by a test that hand-builds an
3293 /// `EventTerm::Edge { iff: Some(_), .. }` directly (e.g. `sched::
3294 /// control::tests::engine_never_calls_resume_when_iff_is_false_but_does_
3295 /// on_the_next_true_edge`) -- never by anything this crate's own ABI can
3296 /// produce from a real kernel. `iff` correctness for a real compiled
3297 /// kernel instead comes entirely from the separate resume-prologue
3298 /// mechanism in `vj-codegen::process_lower` (`compile_iff_guard`,
3299 /// settled by `docs/design/process.md`'s "iff re-check" section) -- the
3300 /// two mechanisms are not reconciled. If a future change threads `iff`
3301 /// across this ABI boundary (`RescheduleReqLayoutProposal` gaining a
3302 /// field, `resolve_suspend_point` populating it), this assertion flips
3303 /// to `Some` -- that is the documented flip condition; update this test
3304 /// and the CONTRACTS.md/ledger entries together rather than deleting it.
3305 ///
3306 /// **Task 7 (AB-096) status -- this test is still exactly correct,
3307 /// unchanged:** the ABI-boundary wire representation half of the flip
3308 /// condition now exists (`RescheduleReqLayoutProposal::guard_fn`/
3309 /// `ResolvedSuspend::EdgeWithGuard`, proved by
3310 /// `guard_fn_round_trips_through_reschedule_req` below) and IS reachable
3311 /// via `decode_reschedule` -- but only for that NEW variant, never for a
3312 /// plain `ResolvedSuspend::Edge` (still the ONLY shape
3313 /// `resolve_suspend_point` itself ever constructs today). See
3314 /// `docs/CONTRACTS.md`'s SC-068 entry, "R-run Task 7 addendum," for the
3315 /// full accounting of what still blocks `resolve_suspend_point` from
3316 /// constructing `EdgeWithGuard` for a real compiled kernel.
3317 #[test]
3318 fn sc068_decode_reschedule_can_never_produce_a_some_iff_edge_term() {
3319 let er = build_eval_result(
3320 ResolvedSuspend::Edge {
3321 cell: StateCellId(9),
3322 edge: EdgeKind::Posedge,
3323 },
3324 0,
3325 );
3326 match decode_reschedule(er).expect("KIND_EDGE must decode to Ok") {
3327 Suspend::WaitEvents(set) => {
3328 assert_eq!(set.terms.len(), 1);
3329 match &set.terms[0] {
3330 EventTerm::Edge { iff, .. } => assert!(
3331 iff.is_none(),
3332 "SC-068 reachability gap regressed: decode_reschedule must never invent \
3333 an iff predicate -- ResolvedSuspend::Edge (the only shape a real \
3334 compiled kernel's resolve_suspend_point can produce) has no iff field \
3335 to carry one across this ABI boundary today"
3336 ),
3337 other => panic!("expected EventTerm::Edge, got {other:?}"),
3338 }
3339 }
3340 other => panic!("expected Suspend::WaitEvents, got {other:?}"),
3341 }
3342 }
3343
3344 /// A real, top-level `extern "C" fn` for the guard round-trip test below
3345 /// -- never a closure (no stable, ABI-crossable function pointer of the
3346 /// `extern "C" fn(SimHandle) -> bool` shape).
3347 extern "C" fn stub_guard_fn(_h: SimHandle) -> bool {
3348 false
3349 }
3350
3351 /// Task 7 (AB-096): a real `extern "C" fn` pointer round-trips
3352 /// byte-identical through [`build_eval_result`]/[`decode_reschedule`] --
3353 /// the literal "documented flip condition"
3354 /// `sc068_decode_reschedule_can_never_produce_a_some_iff_edge_term`'s
3355 /// own doc comment names: this is the future change that flips that
3356 /// assertion to `Some`, for the `ResolvedSuspend::EdgeWithGuard` shape
3357 /// specifically (a plain `ResolvedSuspend::Edge` still decodes `iff:
3358 /// None`, proven by that same still-passing, unmodified test).
3359 #[test]
3360 fn guard_fn_round_trips_through_reschedule_req() {
3361 let er = build_eval_result(
3362 ResolvedSuspend::EdgeWithGuard {
3363 cell: StateCellId(11),
3364 edge: EdgeKind::Negedge,
3365 guard_fn: stub_guard_fn,
3366 },
3367 3,
3368 );
3369 match decode_reschedule(er).expect("KIND_EDGE (with guard) must decode to Ok") {
3370 Suspend::WaitEvents(set) => {
3371 assert_eq!(set.terms.len(), 1);
3372 match &set.terms[0] {
3373 EventTerm::Edge { cell, edge, iff } => {
3374 assert_eq!(*cell, StateCellId(11));
3375 assert_eq!(*edge, EdgeKind::Negedge);
3376 let PredicateFn(decoded) =
3377 iff.expect("an EdgeWithGuard suspend must decode iff: Some(..)");
3378 assert_eq!(
3379 decoded as *const () as usize, stub_guard_fn as *const () as usize,
3380 "the decoded guard function pointer must be byte-identical to the \
3381 original"
3382 );
3383 }
3384 other => panic!("expected EventTerm::Edge, got {other:?}"),
3385 }
3386 }
3387 other => panic!("expected Suspend::WaitEvents, got {other:?}"),
3388 }
3389 }
3390
3391 /// Review fix (A2-part2 r2, binding rule 8): `decode_reschedule` must
3392 /// return a typed [`DecodeRescheduleError`], never panic, for
3393 /// `KIND_EDGE_LIST`/`KIND_FORK_JOIN` -- constructed here directly via
3394 /// `RescheduleReqLayoutProposal::from_edge_list`/`from_fork_join` (never
3395 /// through `build_eval_result`, which -- by design, see
3396 /// `DecodeRescheduleError`'s own doc comment -- never produces these
3397 /// kinds) and packed into the wire form the same way
3398 /// `build_eval_result` packs every other kind, so this test exercises
3399 /// the real decode path, not a hand-rolled shortcut.
3400 #[test]
3401 fn decode_reschedule_reports_typed_errors_for_edge_list_and_fork_join() {
3402 use crate::sched::JoinKind;
3403
3404 fn wrap(proposal: RescheduleReqLayoutProposal) -> EvalResult {
3405 let mut opaque = [0u8; vj_abi::eval::RESCHEDULE_REQ_SIZE];
3406 opaque[..PROPOSAL_SIZE].copy_from_slice(&proposal.to_bytes());
3407 EvalResult {
3408 outcome: EvalOutcome::SuspendedOnEvent,
3409 reschedule: RescheduleReq { opaque },
3410 }
3411 }
3412
3413 let edge_list = wrap(RescheduleReqLayoutProposal::from_edge_list(ArenaRange {
3414 base: 3,
3415 count: 5,
3416 }));
3417 match decode_reschedule(edge_list) {
3418 Err(DecodeRescheduleError::UnresolvedEdgeList { base, count }) => {
3419 assert_eq!(base, 3);
3420 assert_eq!(count, 5);
3421 }
3422 other => panic!(
3423 "KIND_EDGE_LIST must decode to Err(UnresolvedEdgeList{{3,5}}), never panic; got \
3424 {other:?}"
3425 ),
3426 }
3427
3428 let fork_join = wrap(RescheduleReqLayoutProposal::from_fork_join(
3429 JoinKind::All,
3430 ArenaRange { base: 10, count: 2 },
3431 ));
3432 match decode_reschedule(fork_join) {
3433 Err(DecodeRescheduleError::UnresolvedForkJoin {
3434 join_kind,
3435 base,
3436 count,
3437 }) => {
3438 assert_eq!(join_kind, JoinKind::All as u8);
3439 assert_eq!(base, 10);
3440 assert_eq!(count, 2);
3441 }
3442 other => panic!(
3443 "KIND_FORK_JOIN must decode to Err(UnresolvedForkJoin{{..}}), never panic; got \
3444 {other:?}"
3445 ),
3446 }
3447 }
3448
3449 /// The sibling case for a byte outside `KIND_DELAY..=KIND_DONE` entirely
3450 /// (an ABI/version mismatch) -- also a typed `Err`, never a panic.
3451 #[test]
3452 fn decode_reschedule_reports_a_typed_error_for_an_out_of_range_kind_byte() {
3453 let mut opaque = [0u8; vj_abi::eval::RESCHEDULE_REQ_SIZE];
3454 opaque[0] = 200;
3455 let er = EvalResult {
3456 outcome: EvalOutcome::Completed,
3457 reschedule: RescheduleReq { opaque },
3458 };
3459 match decode_reschedule(er) {
3460 Err(DecodeRescheduleError::InvalidKind(200)) => {}
3461 other => panic!("expected Err(InvalidKind(200)), never a panic; got {other:?}"),
3462 }
3463 }
3464
3465 #[test]
3466 fn sv_kernel_abi_reconciliation_note_exists_and_is_referenced() {
3467 assert!(
3468 SV_KERNEL_ABI_RECONCILIATION.contains("CG-084"),
3469 "the reconciliation note must cite CG-084 so a reader auditing that ledger row \
3470 finds this exact note"
3471 );
3472 }
3473
3474 /// PR-101/PR-102's structural proof: exactly one `impl CompiledProcess
3475 /// for` anywhere in this crate's PRODUCTION code (never inside a
3476 /// `#[cfg(test)]` body, which could trivially inflate the count with a
3477 /// throwaway test double) bridges a *kernel-backed* type. Mirrors
3478 /// `sched::process_table`'s own `scan_is_genuinely_slotmap_backed`
3479 /// pattern: strip every `#[cfg(test)]` block first via the shared
3480 /// `determinism::scan_support` helper, then search what remains.
3481 ///
3482 /// **Documented carve-out:** `sched::control::UnmintedForkChild`
3483 /// (pre-existing, committed before this task) is also a production
3484 /// `impl CompiledProcess for` -- but it is a fork/join placeholder
3485 /// ("a fork child whose real compiled body codegen has not minted yet",
3486 /// out of the A2 slice's scope entirely, PR-086) that resumes to
3487 /// `Suspend::Done` unconditionally and bridges no kernel/ABI boundary at
3488 /// all. It is excluded by name here rather than silently miscounted as a
3489 /// second kernel adapter -- the two are not the same claim, and pretending
3490 /// they were would either make this test permanently fail for a reason
3491 /// unrelated to PR-101/PR-102, or force deleting an unrelated, correct
3492 /// placeholder this task does not own.
3493 #[test]
3494 fn exactly_one_compiledprocess_impl_over_a_kernel_backed_type() {
3495 const UNRELATED_PLACEHOLDER: &str = "impl CompiledProcess for UnmintedForkChild";
3496
3497 let src_root = std::path::Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/src"));
3498 let mut total = 0usize;
3499 let mut stack = vec![src_root.to_path_buf()];
3500 while let Some(dir) = stack.pop() {
3501 for entry in std::fs::read_dir(&dir).expect("read_dir must succeed under src/") {
3502 let entry = entry.expect("dir entry must be readable");
3503 let path = entry.path();
3504 if path.is_dir() {
3505 stack.push(path);
3506 continue;
3507 }
3508 if path.extension().and_then(|e| e.to_str()) != Some("rs") {
3509 continue;
3510 }
3511 let text = std::fs::read_to_string(&path)
3512 .unwrap_or_else(|e| panic!("must be able to read {path:?}: {e}"));
3513 let production_only =
3514 crate::determinism::scan_support::strip_cfg_test_blocks(&text);
3515 let here = production_only.matches("impl CompiledProcess for").count()
3516 - production_only.matches(UNRELATED_PLACEHOLDER).count();
3517 total += here;
3518 }
3519 }
3520 assert_eq!(
3521 total, 1,
3522 "expected exactly one kernel-backed production `impl CompiledProcess for` in the \
3523 whole crate (this module's own, on CompiledKernelProcess) -- found {total}"
3524 );
3525 }
3526
3527 /// Task S7: a `SchedulerOps` double that records every `request_finish`
3528 /// call it receives.
3529 struct RecordingSchedulerOps {
3530 finish_calls: std::cell::RefCell<Vec<i32>>,
3531 }
3532 impl SchedulerOps for RecordingSchedulerOps {
3533 fn now(&self) -> crate::sched::SchedTime {
3534 crate::sched::SchedTime { ticks: 0 }
3535 }
3536 fn current_region(&self) -> Region {
3537 Region::Active
3538 }
3539 fn stage_nba(
3540 &self,
3541 _cell: StateCellId,
3542 _value: crate::state::FourStateValue,
3543 _source: u32,
3544 ) {
3545 }
3546 fn commit_blocking_write(&self, _cell: StateCellId, _value: crate::state::FourStateValue) {}
3547 fn request_finish(&self, code: i32) {
3548 self.finish_calls.borrow_mut().push(code);
3549 }
3550 fn defer_io(
3551 &self,
3552 _at: crate::sched::SchedTime,
3553 _region: Region,
3554 _cb: crate::sched::DeferredIoCallback,
3555 ) {
3556 }
3557 fn register_region_callback(
3558 &self,
3559 _region: Region,
3560 _at: crate::sched::SchedTime,
3561 _cb: Box<dyn crate::sched::RegionCallback>,
3562 ) {
3563 }
3564 }
3565
3566 /// Task S7 (the `SimHandle`-reachability bridge): `with_scheduler_ops`
3567 /// makes `ops` reachable to `real_sv_rt_finish` for exactly the duration
3568 /// of the wrapped call -- proving the actual mechanism `CompiledKernelProcess::
3569 /// resume` now uses to make `nba_enqueue`/`sv_rt_finish`'s real bodies
3570 /// reach the scheduler, not merely that `request_finish` itself works.
3571 #[test]
3572 fn with_scheduler_ops_makes_ops_reachable_to_real_sv_rt_finish() {
3573 let ops = RecordingSchedulerOps {
3574 finish_calls: std::cell::RefCell::new(Vec::new()),
3575 };
3576 let handle = SimHandle(std::ptr::null_mut());
3577 with_scheduler_ops(&ops, || {
3578 real_sv_rt_finish(handle, 7);
3579 });
3580 assert_eq!(
3581 *ops.finish_calls.borrow(),
3582 vec![7],
3583 "real_sv_rt_finish must reach the real ops.request_finish while inside \
3584 with_scheduler_ops's callback"
3585 );
3586 }
3587
3588 /// Outside any `with_scheduler_ops` call, `real_sv_rt_finish`/
3589 /// `with_current_ops` must be a silent no-op, never a panic -- proven by
3590 /// simply calling it with nothing installed and observing no crash (see
3591 /// `with_current_ops`'s own doc comment for why this shape never arises
3592 /// from a real kernel).
3593 #[test]
3594 fn real_sv_rt_finish_outside_with_scheduler_ops_does_not_panic() {
3595 let handle = SimHandle(std::ptr::null_mut());
3596 real_sv_rt_finish(handle, 1);
3597 }
3598
3599 /// Review fix (A2-part2 r1, binding rule 7): `with_current_process`
3600 /// makes `proc` reachable through `CURRENT_PROCESS` for exactly the
3601 /// duration of the wrapped call, and restores the prior (here: `None`)
3602 /// value afterward -- the same "installed for one call, then restored"
3603 /// contract `with_scheduler_ops`/`CURRENT_OPS` already give `ops`,
3604 /// proven the identical way (`with_scheduler_ops_makes_ops_reachable_to_
3605 /// real_sv_rt_finish` above).
3606 #[test]
3607 fn with_current_process_makes_proc_reachable_for_exactly_one_call_and_restores_after() {
3608 assert_eq!(
3609 CURRENT_PROCESS.with(|c| *c.borrow()),
3610 None,
3611 "precondition: no other test on this thread left CURRENT_PROCESS set"
3612 );
3613 let seen_inside = with_current_process(42, || CURRENT_PROCESS.with(|c| *c.borrow()));
3614 assert_eq!(
3615 seen_inside,
3616 Some(42),
3617 "CURRENT_PROCESS must read back the ProcessId with_current_process installed, \
3618 from inside the wrapped call"
3619 );
3620 assert_eq!(
3621 CURRENT_PROCESS.with(|c| *c.borrow()),
3622 None,
3623 "with_current_process must restore the prior (None) value after the wrapped call \
3624 returns, not leave 42 installed"
3625 );
3626 }
3627
3628 /// The save/restore half of the same contract, under nesting: returning
3629 /// from an inner `with_current_process` must restore the OUTER caller's
3630 /// `ProcessId`, never merely clear it to `None` -- the exact property
3631 /// `CURRENT_OPS`'s own doc comment cites as the reason it saves-and-
3632 /// restores instead of clearing (a hypothetical nested compiled-kernel
3633 /// call, e.g. a future fork/join child resumed from inside a parent's
3634 /// own `resume()`, must see ITS OWN `ProcessId` while running, and the
3635 /// parent's `ProcessId` must still be correct once control returns to
3636 /// it).
3637 #[test]
3638 fn with_current_process_restores_the_outer_value_not_merely_clears_it() {
3639 with_current_process(1, || {
3640 let inner = with_current_process(2, || CURRENT_PROCESS.with(|c| *c.borrow()));
3641 assert_eq!(inner, Some(2));
3642 assert_eq!(
3643 CURRENT_PROCESS.with(|c| *c.borrow()),
3644 Some(1),
3645 "returning from a nested with_current_process must restore the OUTER caller's \
3646 ProcessId, not clear it to None"
3647 );
3648 });
3649 assert_eq!(
3650 CURRENT_PROCESS.with(|c| *c.borrow()),
3651 None,
3652 "after the outermost with_current_process call returns, CURRENT_PROCESS must be \
3653 back to None"
3654 );
3655 }
3656
3657 /// Review fix (A2-part2 r2, finding 3): `two_process_nba_same_var_
3658 /// matches_iverilog_bit_for_bit` (`vj-harness`) proves `commit_nba`'s
3659 /// own push-order hazard resolution (a correct, independent SC-039
3660 /// design -- see `sched::notify::commit_nba`'s doc comment: it never
3661 /// reads `NbaUpdate.source`/`entry.source` at all, tie-breaking purely
3662 /// by push/insertion order). That test's own fixture is a causal
3663 /// (insertion-order) dependency between its two processes, so it passes
3664 /// or fails on push-order correctness alone and cannot tell "CURRENT_
3665 /// PROCESS wired correctly" apart from "CURRENT_PROCESS absent/always-0"
3666 /// -- reverting the whole `CURRENT_PROCESS` bridge would not change that
3667 /// test's outcome. This test exercises the one thing r1's fix actually
3668 /// changed instead: `real_nba_enqueue`'s own `source` output, directly,
3669 /// with and without `with_current_process` installed -- proving the
3670 /// bridge is genuinely live, not merely present-but-unread.
3671 #[test]
3672 fn real_nba_enqueue_prefers_current_process_over_the_raw_abi_source() {
3673 // Minimal, frontend-free single-scalar-slot template/instance --
3674 // `state::instance`'s own doc comments explain why `TemplateRegistry`/
3675 // `StateLayout`/`SlotDescriptor` need no `sv_lang` dependency at all,
3676 // so this test builds one without a real `sv_lang::Design`.
3677 let template = crate::netlist::TemplateKey {
3678 definition: 1,
3679 params_hash: 0,
3680 };
3681 let layout = crate::state::StateLayout {
3682 slots: vec![crate::state::SlotDescriptor {
3683 key: crate::netlist::SymbolKey(0),
3684 offset: crate::state::SlotOffset {
3685 class: crate::state::SlotClass::Inline,
3686 word_offset: 0,
3687 bit_width: 1,
3688 elem_word_count: 1,
3689 row_stride_words: 0,
3690 num_rows: 0,
3691 },
3692 initial: None,
3693 net_info: None,
3694 }],
3695 inline_words: 1,
3696 wide_words: 0,
3697 };
3698 let registry = crate::state::TemplateRegistry::new();
3699 // SAFETY: single-threaded test; no other live `&StateLayout` borrow
3700 // and no concurrent `register` call are in flight -- exactly
3701 // `TemplateRegistry::register`'s own documented obligation.
3702 unsafe {
3703 registry.register(template, layout);
3704 }
3705 let mut engine = crate::state::StateEngine::new(registry);
3706 let handle = engine
3707 .allocate_instance(template)
3708 .expect("template was just registered above");
3709 let cell = handle.cell_for_slot(0);
3710 let h = handle.into_sim_handle();
3711
3712 struct NbaRecordingOps {
3713 calls: std::cell::RefCell<Vec<(StateCellId, u32)>>,
3714 }
3715 impl SchedulerOps for NbaRecordingOps {
3716 fn now(&self) -> crate::sched::SchedTime {
3717 crate::sched::SchedTime { ticks: 0 }
3718 }
3719 fn current_region(&self) -> Region {
3720 Region::Nba
3721 }
3722 fn stage_nba(
3723 &self,
3724 cell: StateCellId,
3725 _value: crate::state::FourStateValue,
3726 source: u32,
3727 ) {
3728 self.calls.borrow_mut().push((cell, source));
3729 }
3730 fn commit_blocking_write(
3731 &self,
3732 _cell: StateCellId,
3733 _value: crate::state::FourStateValue,
3734 ) {
3735 }
3736 fn request_finish(&self, _code: i32) {}
3737 fn defer_io(
3738 &self,
3739 _at: crate::sched::SchedTime,
3740 _region: Region,
3741 _cb: crate::sched::DeferredIoCallback,
3742 ) {
3743 }
3744 fn register_region_callback(
3745 &self,
3746 _region: Region,
3747 _at: crate::sched::SchedTime,
3748 _cb: Box<dyn crate::sched::RegionCallback>,
3749 ) {
3750 }
3751 }
3752
3753 let ops = NbaRecordingOps {
3754 calls: std::cell::RefCell::new(Vec::new()),
3755 };
3756
3757 // No CURRENT_PROCESS installed (the shape every direct-call test in
3758 // this file, bypassing ResumeDriver, produces): real_nba_enqueue
3759 // must fall back to the raw ABI `source` value (99) it was actually
3760 // called with.
3761 with_scheduler_ops(&ops, || {
3762 real_nba_enqueue(h, vj_abi::SlotId(0), 1, 99);
3763 });
3764 assert_eq!(
3765 *ops.calls.borrow(),
3766 vec![(cell, 99)],
3767 "with no CURRENT_PROCESS installed, real_nba_enqueue must fall back to the raw ABI \
3768 source value"
3769 );
3770 ops.calls.borrow_mut().clear();
3771
3772 // CURRENT_PROCESS installed (exactly as ResumeDriver::resume_for/
3773 // start install it around every real resume() call): real_nba_enqueue
3774 // must prefer it (7) over the raw ABI source (99) -- the one
3775 // observable difference this review fix actually produces.
3776 with_current_process(7, || {
3777 with_scheduler_ops(&ops, || {
3778 real_nba_enqueue(h, vj_abi::SlotId(0), 1, 99);
3779 });
3780 });
3781 assert_eq!(
3782 *ops.calls.borrow(),
3783 vec![(cell, 7)],
3784 "with CURRENT_PROCESS installed, real_nba_enqueue must prefer it (7) over the raw \
3785 ABI source (99)"
3786 );
3787
3788 // SAFETY: reclaims the leaked `Box<InstanceHandle>` `into_sim_handle`
3789 // minted above -- the same reclaim pattern `state::accessor_impl`'s
3790 // own tests use after every `into_sim_handle` call.
3791 unsafe {
3792 drop(Box::from_raw(h.0 as *mut InstanceHandle));
3793 }
3794 }
3795}