Skip to main content

sv_sim_sim/process/
mod.rs

1//! A2-PROCESS (`sv-sim-sim::process`) — cross-track contract only: the process
2//! template lowering shape shared between the A2-process track and CODEGEN
3//! (process.md). Real liveness analysis / CFG construction belongs to the
4//! A2-process track, not this wave.
5
6use sv_sim_abi::{SimHandle, SimTime};
7
8use crate::sched::JoinKind;
9use crate::state::{EdgeKind, StateCellId};
10
11// Task S3a (CPS CFG + Segment IR) touches `sv_lang::Statement`/`Expression`
12// directly, so it lives behind this crate's `frontend` feature per
13// `docs/CONTRACTS.md` D4 -- unlike the plain POD shape types above, which
14// stay sv-lang-free and ungated.
15#[cfg(feature = "frontend")]
16pub mod cfg;
17
18// Task 7 (PR-116, XC-044): re-exported at this module's own path (rather
19// than only `process::cfg::`) so `suspending_statement_kinds` sits exactly
20// where the plan names it (`sv-sim-sim::process`) -- physically defined
21// next to `SuspendPoint` in `cfg` (see that function's own doc comment for
22// why), not redeclared here.
23#[cfg(feature = "frontend")]
24pub use cfg::{SuspendPointKind, suspending_statement_kinds};
25
26// Task S3b (Liveness + FrameLayout) touches `sv_lang::Symbol`/`SymbolId`
27// directly (declared-local discovery via `Statement::block_symbol()` +
28// `Symbol::members()`), so it lives behind this crate's `frontend` feature
29// for the same reason `cfg` does -- see `cfg`'s doc comment above.
30#[cfg(feature = "frontend")]
31pub mod liveness;
32
33// Task S5 (Template dedup + `ProcessInstanceId`) touches `sv_lang::Symbol`/
34// `SymbolId`/`ConstantValue` directly (`Symbol::parameters()`,
35// `instance_body_definition()`, `constant_value()`), so it lives behind this
36// crate's `frontend` feature for the same reason `cfg`/`liveness` do -- see
37// `cfg`'s doc comment above.
38#[cfg(feature = "frontend")]
39pub mod dedup;
40
41/// Stable, elaboration-order-derived tie-break key (process.md §6: "derived from
42/// a deterministic pre-order walk of the frozen Design's instance hierarchy,
43/// never from allocation order or thread arrival"; ledger PR-075..PR-077,
44/// PR-118, DT-011, DT-031). The scheduler consumes this as its canonical
45/// cross-thread tie-break.
46#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
47pub struct ProcessInstanceId(pub u64);
48
49/// Opaque, minted 1:1 by CODEGEN per trap-eligible statement at lowering time
50/// (debugger.md; ledger CG-142, CG-149, CG-154, SC-104, SC-126, DB-034): stable
51/// only within one compile, never across recompiles (no hot-reload of source into
52/// a live debug session).
53#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
54pub struct StmtId(pub u64);
55
56/// One frame slot's compile-time SHAPE only (process.md §3 `FrameSlot`/
57/// `FourStateI2N`/`WideFourState`; ledger ST-103..ST-105): the number of u64
58/// words the slot's payload occupies. This is layout metadata CODEGEN consumes
59/// when computing a TEMPLATE's fixed frame offsets ("Frame slot layout is
60/// decided once per process TEMPLATE... handed to codegen as a fixed struct
61/// layout") — never the runtime payload itself, which is why it carries
62/// counts, not `Vec<u64>` data (see [`ActivationRecord`]'s doc comment for why
63/// that distinction is load-bearing).
64#[derive(Clone, Copy, PartialEq, Eq, Debug)]
65pub enum FrameSlot {
66    Narrow4State {
67        word_count: u16,
68    },
69    Wide4State {
70        aval_word_count: u32,
71        bval_word_count: u32,
72    },
73    /// A fixed-size unpacked-array local: `rows` rows (all dimensions
74    /// flattened, lowest index first) of `row_words` words each, a row
75    /// holding one element's packed value.
76    Array {
77        rows: u32,
78        row_words: u16,
79    },
80}
81
82/// A fixed, pointer-free reference to a contiguous range of STATE-owned arena
83/// storage (word- or element-addressed, per the referencing field's own unit).
84/// Never an owning buffer — the one shape every [`ActivationRecord`]-embedded
85/// field below uses in place of a heap-owning `Vec`.
86#[repr(C)]
87#[derive(Clone, Copy, PartialEq, Eq, Debug)]
88pub struct ArenaRange {
89    pub base: u32,
90    pub count: u32,
91}
92
93/// POD, pointer-free, memcpy-checkpointable — the only per-instance execution
94/// state that must survive a suspend or a Time-Warp rollback (process.md §3;
95/// ledger CG-164, CG-176, ST-005).
96///
97/// Fixed vs. an earlier draft of this contract (caught in review): storing
98/// `frame: Vec<FrameSlot>` / `pending_wait: Option<WaitSpec>` (its `EdgeList`
99/// variant owns a `Vec`) / `fork_state: Option<ForkFrame>` (its
100/// `child_handles` owned a `Vec`) directly here made the "memcpy-
101/// checkpointable" claim false — a `Vec` is a heap pointer + len + cap, so
102/// byte-copying this struct for checkpoint/rollback would duplicate that
103/// pointer, and dropping or mutating either copy double-frees or
104/// use-after-frees the other. `frame: [FrameSlot]` in
105/// `docs/design/process.md`'s own struct sketch (§3, line 40) is a DST slice,
106/// meaningful only as a *reference* into memory STATE owns ("owned/allocated
107/// by STATE, laid out by this subsystem via FrameLayout") — never data this
108/// struct owns inline. `frame` below is exactly that reference: an
109/// [`ArenaRange`] into STATE's per-process frame arena (word-addressed); the
110/// per-slot shape list (`FrameLayout`/`Vec<FrameSlot>`) stays a compile-time-
111/// only artifact of `ProcessLoweringPlan`, analogous to `state::SlotOffset`,
112/// and is never embedded in the per-instance runtime record. `pending_wait` /
113/// `fork_state` are fixed for the same reason — see [`WaitSpecStorage`] /
114/// [`ForkFrame`]'s own doc comments.
115// No `PartialEq`/`Eq`: transitively embeds `PredicateFn` (a bare fn pointer,
116// whose equality is deliberately not derived -- see `PredicateFn`'s own doc
117// comment). `Copy` is what the POD/memcpy-checkpointable contract needs.
118#[repr(C)]
119#[derive(Clone, Copy, Debug)]
120pub struct ActivationRecord {
121    pub pc: u32,
122    pub frame: ArenaRange,
123    pub pending_wait: WaitSpecStorage,
124    pub fork_state: Option<ForkFrame>,
125}
126
127/// The POD cache of the last-returned suspend reason, embedded directly in
128/// [`ActivationRecord`]. `docs/design/process.md` §3/§4 names this exact
129/// split: the persisted field is `WaitSpecStorage`, distinct from the richer
130/// [`WaitSpec`] the suspend/resume ABI exchanges with the scheduler on every
131/// call ("last-returned WaitSpec, re-derivable but cached for O(1) resume
132/// dispatch and for the debugger inspector"). `Edge`'s single term is already
133/// `Copy` (`PredicateFn` is a bare `extern "C"` fn pointer) so it is inlined
134/// directly; `EdgeList`'s term count is unbounded (an arbitrary-length
135/// `@(a or b or c or ...)`), so like `ActivationRecord::frame` it is instead
136/// an [`ArenaRange`] into a static, elaboration-time-built edge-term table
137/// (the concrete `StateCellId`s for a given suspend point are fixed once
138/// elaboration resolves the instance's signals and never change for the life
139/// of the run, so this table is a read-only lookup, never a rollback hazard).
140// No `PartialEq`/`Eq`: the `Edge` variant embeds `Option<PredicateFn>` -- see
141// `PredicateFn`'s own doc comment for why fn-pointer equality isn't derived.
142#[repr(C)]
143#[derive(Clone, Copy, Debug)]
144pub enum WaitSpecStorage {
145    None,
146    Delay(SimTime),
147    Edge {
148        edge: EdgeKind,
149        signal: StateCellId,
150        iff: Option<PredicateFn>,
151    },
152    EdgeList(ArenaRange),
153    ImplicitComb(SubscriptionSetId),
154}
155
156/// Fork/join bookkeeping (process.md §7: "construction only, no scheduling
157/// semantics in v1"; ledger PR-086, fork/join family PR-001, PR-047, PR-061).
158/// `children` is a fixed [`ArenaRange`] into the engine's dynamic fork-child
159/// table — never an owned `Vec`, the same memcpy-safety reason as
160/// `ActivationRecord::frame`. Its elements are [`crate::sched::ProcessId`],
161/// **not** [`ProcessInstanceId`]: scheduler.md is explicit that "fork
162/// children are dynamically created ProcessIds not present in static
163/// elaboration," so `ProcessInstanceId` (elaboration-derived, static-only per
164/// this file's own doc comment above) structurally cannot name them. The
165/// branch *count* for a given fork site is statically known at lowering time;
166/// the concrete `ProcessId`s are assigned dynamically at fork time and live
167/// in that out-of-line table, never inline here.
168#[repr(C)]
169#[derive(Clone, Copy, PartialEq, Eq, Debug)]
170pub struct ForkFrame {
171    pub children: ArenaRange,
172    pub join_kind: JoinKind,
173    pub remaining: u32,
174}
175
176/// The settled `CondThunk` shape for a `SignalEventControl`'s `iff` re-check
177/// AND `wait(expr)`'s own re-check (PR-106, A2-part2 Task 3): a plain
178/// `extern "C"` function pointer over the shared `SimHandle`, produced by
179/// exactly one compiler entry point, `sv-sim-codegen::process_lower::
180/// compile_iff_guard` -- see that function's own doc comment. Through Task 1
181/// this shape was left an open question (process.md: "needs to be settled
182/// jointly with codegen's dispatcher shape... not decided unilaterally
183/// here"); Task 3 settles it: a raw function pointer into a standalone,
184/// independently-compiled guard function, never a re-entry into a dedicated
185/// micro-segment.
186// Deliberately no `PartialEq`/`Eq`: fn-pointer comparisons are not
187// meaningful (address is not guaranteed stable/unique across codegen units,
188// merged identical functions can collide) -- `rustc` warns on this
189// (`unpredictable_function_pointer_comparisons`). `Copy` alone is what the
190// POD/memcpy-checkpointable contract actually needs.
191#[derive(Clone, Copy, Debug)]
192pub struct PredicateFn(pub extern "C" fn(SimHandle) -> bool);
193
194/// A precomputed, static subscription-set id for `always_comb`/`always_latch`'s
195/// implicit sensitivity (scheduler.md §4/§5) — O(1) resubscribe, computed once
196/// ever from `AnalyzedProcedure::read_set()`.
197#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
198pub struct SubscriptionSetId(pub u32);
199
200/// The ABI-facing resume contract (process.md §4 `WaitSpec`; ledger CG-169,
201/// CG-170, SC-085, SC-119, PR-012, PR-037) a compiled process kernel reports back
202/// through [`ProcessStatus::Suspended`].
203///
204/// Reconciliation: distinct from [`crate::sched::Suspend`]. `sched::Suspend` is
205/// the Rust-trait-level abstraction `CompiledProcess::resume()` returns to the
206/// *in-process* scheduler orchestration loop (scheduler.md §5); `WaitSpec` here is
207/// the lower-level, codegen-facing shape process.md §4 sketches for the compiled
208/// `process_kernel`-style ABI. ARCHITECTURE.md does not collapse the two designs'
209/// differing field lists into one type, so per the reconciliation rule both are
210/// kept, layered rather than merged: `sched::Suspend` is built by decoding a
211/// returned `WaitSpec`/`RescheduleReq`, not by replacing it.
212#[derive(Clone, Debug)]
213pub enum WaitSpec {
214    Delay(SimTime),
215    Edge {
216        edge: EdgeKind,
217        signal: StateCellId,
218        iff: Option<PredicateFn>,
219    },
220    EdgeList(Vec<(EdgeKind, StateCellId, Option<PredicateFn>)>),
221    ImplicitComb(SubscriptionSetId),
222}
223
224/// What woke a process (process.md §4 `ResumeInfo`/`ResumeReason`).
225#[derive(Clone, Copy, Debug)]
226pub struct ResumeInfo {
227    pub reason: ResumeReason,
228    pub at_time: SimTime,
229}
230
231#[derive(Clone, Copy, PartialEq, Eq, Debug)]
232pub enum ResumeReason {
233    InitialDispatch,
234    EdgeFired { signal_generation: u64 },
235    DelayElapsed,
236    DebugStep,
237    ForkChildDone,
238}
239
240#[derive(Clone, Debug)]
241pub struct ProcessResult {
242    pub status: ProcessStatus,
243}
244
245#[derive(Clone, Debug)]
246pub enum ProcessStatus {
247    Suspended {
248        wait: WaitSpec,
249    },
250    Finished,
251    /// Debug-mode only: paused at a micro-segment boundary, NOT a real suspend
252    /// (process.md §8).
253    Yielded,
254}
255
256/// The compiled-kernel calling convention every process TEMPLATE compiles to
257/// (process.md §4). **Not** the literal ABI-crossing symbol — that is
258/// [`sv_sim_abi::ProcessFn`] (`extern "C"`, using the frozen `SimHandle`/
259/// `EvalResult`/`RescheduleReq` shapes so every field stays FFI-safe). This is the
260/// richer, process-track-internal Rust-level decoding of that call, matching
261/// process.md's own explicitly-flagged scope ("proposed here at the logical
262/// (Suspend/ResumeArg) level only; the physical ABI needs to be pinned jointly
263/// with CODEGEN") — an ordinary Rust `fn` pointer, deliberately not `extern "C"`,
264/// since [`ProcessResult`] owns a heap-allocated field (`WaitSpec::EdgeList`'s
265/// `Vec<..>`, via `ProcessStatus::Suspended`) that is not FFI-safe by value.
266/// [`ActivationRecord`] itself is `#[repr(C)]`/POD (see its own doc comment) —
267/// it is the *return type*, not the activation pointer, that blocks
268/// `extern "C"` here.
269pub type ProcessKernelFn =
270    fn(state: *mut u8, activation: *mut ActivationRecord, resume: ResumeInfo) -> ProcessResult;
271
272/// Rollback-safety classification per `LoweredStmt`/`Segment` (process.md §6),
273/// consumed by the engine's Time-Warp commit policy.
274#[derive(Clone, Copy, PartialEq, Eq, Debug)]
275pub enum Effect {
276    Pure,
277    StateWrite,
278    Irrevocable,
279}
280
281/// Task 10 (A2-part2, PR-122): the per-partition additive bias CODEGEN adds
282/// to a `SignalSlot::slot` arena-slot integer operand before it reaches
283/// `accessor::build_get_scalar`/`build_set_scalar`, so two processes in
284/// different WOCB partitions (`region_id`s) address disjoint arena-base
285/// bands: partition `region_id`'s slots occupy `[region_id *
286/// slots_per_region, (region_id + 1) * slots_per_region)`. A plain multiply,
287/// not a hardcoded zero -- `region_id == 0` (v1's only real value) yields
288/// `0`, leaving every real accessor call's slot operand byte-identical to
289/// today's unbiased value (this task changes zero observable behavior for
290/// any real v1 design); a caller that hand-sets a synthetic `region_id >= 1`
291/// (no such partition exists yet -- the WOCB analysis pass that would assign
292/// one is unbuilt) gets a real, disjoint, nonzero band, which is exactly
293/// what the codegen-side IR-inspection test proves. One definition, reused
294/// by both the real biasing call site (`sv-sim-codegen::process_lower`'s
295/// `prime_external_signals`/`flush_external_signals`) and this task's own
296/// unit test -- never reimplemented a second time.
297pub fn arena_base_offset_for_region(region_id: u32, slots_per_region: u32) -> u32 {
298    region_id * slots_per_region
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304
305    /// PR-104: a source file "looks like" 4-state truth-table/operator-
306    /// semantics logic when it references all four `sv_lang::Bit` per-bit
307    /// variants (`Bit::Zero`/`Bit::One`/`Bit::X`/`Bit::Z`) AND combines them
308    /// with a bitwise operator -- the exact shape `sv-sim-codegen::
309    /// ops::ref4`'s own `and`/`or`/`xor` functions have (verified against
310    /// that module's real source: a `vu`/`bit` per-bit decode/encode pair
311    /// plus a `&`/`|`/`^` formula on the decoded value/unknown planes).
312    /// `sv-sim-sim::process` must contain none of this -- PR-104's whole
313    /// claim is that this crate decides no 4-state operator semantics at
314    /// all, that being codegen's job exclusively.
315    ///
316    /// **Crate-boundary note (this task's own, mirroring `sched::
317    /// kernel_adapter.rs`'s identical precedent):** this test needs
318    /// `crate::determinism::scan_support::strip_cfg_test_blocks`, which is
319    /// `pub(crate)` and therefore unreachable from an external `tests/`
320    /// binary -- `crates/sv-sim-sim/tests/process_contract.rs`'s own doc
321    /// comment records this same reasoning for why this test (and its
322    /// injected-fixture sibling below) lives here instead.
323    fn looks_like_four_state_truth_table(src: &str) -> bool {
324        let has_all_bit_variants = ["Bit::Zero", "Bit::One", "Bit::X", "Bit::Z"]
325            .iter()
326            .all(|m| src.contains(m));
327        let has_bitwise_combinator =
328            src.contains(" & ") || src.contains(" | ") || src.contains(" ^ ");
329        has_all_bit_variants && has_bitwise_combinator
330    }
331
332    #[test]
333    fn no_truth_table_logic_in_process_crate() {
334        let src_root = std::path::Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/src/process"));
335        let mut offenders = Vec::new();
336        let mut stack = vec![src_root.to_path_buf()];
337        while let Some(dir) = stack.pop() {
338            for entry in std::fs::read_dir(&dir).expect("read_dir must succeed under src/process") {
339                let entry = entry.expect("dir entry must be readable");
340                let path = entry.path();
341                if path.is_dir() {
342                    stack.push(path);
343                    continue;
344                }
345                if path.extension().and_then(|e| e.to_str()) != Some("rs") {
346                    continue;
347                }
348                let text = std::fs::read_to_string(&path)
349                    .unwrap_or_else(|e| panic!("must be able to read {path:?}: {e}"));
350                let production_only =
351                    crate::determinism::scan_support::strip_cfg_test_blocks(&text);
352                if looks_like_four_state_truth_table(&production_only) {
353                    offenders.push(path);
354                }
355            }
356        }
357        assert!(
358            offenders.is_empty(),
359            "PR-104: found 4-state truth-table-shaped logic in the process crate (codegen's \
360             job alone, never this crate's): {offenders:?}"
361        );
362    }
363
364    /// The negative-fixture half of PR-104's own acceptance ("include a
365    /// negative fixture proving the scan fires on an injected truth table"):
366    /// proves [`looks_like_four_state_truth_table`] is a real, firing check,
367    /// not a gate that cannot fail (it requires ALL FOUR variants present,
368    /// so a fixture referencing only one, like a real early-return arm
369    /// might, must NOT false-positive).
370    #[test]
371    fn no_truth_table_logic_scan_fires_on_injected_truth_table() {
372        let one_variant_only =
373            "fn and(a: Bit, b: Bit) -> Bit { let v = (a & b) | (a ^ b); Bit::Zero }";
374        assert!(
375            !looks_like_four_state_truth_table(one_variant_only),
376            "sanity: referencing only one of the four Bit variants must not false-positive"
377        );
378
379        let injected_truth_table = format!(
380            "{one_variant_only}\nconst _EXAMPLES: [Bit; 4] = [Bit::Zero, Bit::One, Bit::X, Bit::Z];"
381        );
382        assert!(
383            looks_like_four_state_truth_table(&injected_truth_table),
384            "the scan must fire once an injected fixture references all four Bit variants \
385             alongside a bitwise combinator"
386        );
387    }
388
389    #[test]
390    fn wait_spec_delay_round_trips_sim_time() {
391        let t = SimTime {
392            sim_units: 42,
393            delta: 0,
394        };
395        let w = WaitSpec::Delay(t);
396        match w {
397            WaitSpec::Delay(got) => assert_eq!(got, t),
398            _ => panic!("expected Delay"),
399        }
400    }
401
402    /// Compile-time proof of the review-fixed claim: `ActivationRecord` (and
403    /// everything it embeds) must be `Copy` — i.e. own no heap pointer — for
404    /// "memcpy-checkpointable" to be true. Before the fix this would not even
405    /// compile (`Vec` fields are not `Copy`), which was exactly the point: the
406    /// original code silently skipped `derive(Copy)` entirely instead of
407    /// surfacing that failure.
408    #[test]
409    fn activation_record_is_pod_and_memcpy_safe() {
410        fn assert_copy<T: Copy>() {}
411        assert_copy::<ActivationRecord>();
412        assert_copy::<ForkFrame>();
413        assert_copy::<WaitSpecStorage>();
414        assert_copy::<FrameSlot>();
415        assert_copy::<ArenaRange>();
416
417        let ar = ActivationRecord {
418            pc: 0,
419            frame: ArenaRange { base: 0, count: 4 },
420            pending_wait: WaitSpecStorage::EdgeList(ArenaRange { base: 4, count: 2 }),
421            fork_state: Some(ForkFrame {
422                children: ArenaRange { base: 8, count: 3 },
423                join_kind: JoinKind::All,
424                remaining: 3,
425            }),
426        };
427        // A plain byte-copy (what checkpoint/rollback does) must never
428        // require a Drop impl or duplicate a heap allocation.
429        let copy = ar;
430        assert_eq!(copy.pc, ar.pc);
431        assert_eq!(copy.frame.count, 4);
432    }
433}