vj-sim 0.3.0

Netlist, scheduler and process model of the vj SystemVerilog simulator.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
//! Task 19 — `KernelCtx` field-ownership boundary and the compiled-kernel
//! purity contract (ledger ST-131, ST-132; ARCHITECTURE §6.2/state.md §3).
//!
//! Wave A has no CODEGEN track (ENGINE's compiled-kernel `KernelCtx` type —
//! current sim time, the rest of the suspend/resume contract — is jointly
//! defined by ENGINE and CODEGEN, neither of which is an owning track this
//! wave). This module therefore does **not** attempt to redefine the whole
//! `KernelCtx`: it names only the two fields ST-131 says STATE itself owns
//! ([`StateKernelCtxFields`]), and states + tests the STATE-side half of the
//! ST-132 purity contract ([`PureKernel`] plus the structural scan below).
//!
//! **Unresolved-review-findings note (ST-146, plan review, blocking):** the
//! review flags Task 14's `NetShape::AliasedSingleDriver` default as a
//! *unilateral* STATE decision masquerading as ST-146's bilateral
//! STATE/CODEGEN sign-off, since no CODEGEN track exists this wave to
//! actually agree. That finding is about ST-146 / Task 14's net-read-codegen
//! default, not about this task's ST-131/ST-132 scope (this module never
//! touches `NetShape`/`NetLayout` at all), so it is out of scope here and is
//! not re-litigated by this file. Nothing in this task claims a bilateral
//! sign-off: [`StateKernelCtxFields`]'s doc comment below states explicitly
//! that it names only STATE's own fields, exactly as ST-131 requires, with
//! no joint-decision claim being made either way.

use std::ffi::c_void;

use vj_abi::{SimHandle, SlotId};

use super::write::VcState;

/// ST-131: **this struct names only the fields STATE owns.** The full
/// `KernelCtx` a compiled kernel actually receives (current sim time, the
/// rest of the suspend/resume contract) is jointly defined by ENGINE/CODEGEN
/// and is out of this crate's scope — it is **not** redefined here. This is
/// a documented sub-shape other tracks' real `KernelCtx` embeds (by value or
/// by reference), never a claim that STATE owns or defines the whole thing.
///
/// - `arena_resolution_cb`: the resolution-callback vtable slot
///   ARCHITECTURE §6.2 / state.md §3 name as jointly owned — the entry point
///   a compiled kernel calls to resolve a net read through STATE's own
///   multi-driver resolution (`driver::resolve_net_step`), rather than
///   reading a slot directly. Its exact wiring to a real elaborated net is
///   out of this wave's scope (see the plan's Deferred section, `NetKind`
///   gap); this field only names the calling-convention slot, not a live
///   implementation of it.
/// - `vc_side_channel`: an opaque pointer to this crate's speculative-buffer
///   append point (Task 8's [`VcState`]) — the mechanism a compiled kernel's
///   write path reaches to append a `VcRecord`, without STATE handing the
///   kernel anything more specific than an opaque handle it round-trips back
///   through STATE's own accessor ABI.
///
/// `#[repr(C)]`: this struct's fields are the exact bytes a compiled-kernel
/// calling convention embeds, so its layout must be FFI-stable — no field
/// reordering/padding a Rust-only `#[repr(Rust)]` struct would otherwise be
/// free to apply.
#[repr(C)]
pub struct StateKernelCtxFields {
    pub arena_resolution_cb: extern "C" fn(handle: SimHandle, net: SlotId),
    pub vc_side_channel: *mut c_void,
}

impl StateKernelCtxFields {
    /// Builds the STATE-owned sub-shape from a live [`super::StateEngine`]'s
    /// own speculative-buffer state, keeping the actual pointer construction
    /// (and its safety argument) in exactly one place rather than letting
    /// every future caller re-derive `Rc::as_ptr` casts of its own.
    ///
    /// `vc_side_channel` is `Rc::as_ptr(vc)` cast to an opaque `*mut c_void`
    /// — the same [`VcState`] Task 8/9's `commit_write_at` already threads
    /// through every write path this crate has, not a copy or a decoy
    /// stand-in for it. The engine keeps its own `Rc<VcState>` alive for at
    /// least as long as any `StateKernelCtxFields` built from it is in use
    /// (the same lifetime discipline `InstanceHandle`'s own `Rc<VcState>`
    /// clone already relies on) — this constructor takes `&std::rc::Rc<VcState>`
    /// directly (not `&StateEngine`) so that discipline is visible at the
    /// call site rather than hidden behind a second field access.
    ///
    /// `#[allow(dead_code)]` outside test builds: this wave has no CODEGEN/
    /// ENGINE consumer yet to call this from real kernel-invocation code
    /// (see this module's top doc comment) — it exists and is exercised by
    /// `vc_side_channel_pointer_round_trips_to_the_same_speculative_buffer`
    /// below, which is this task's own acceptance test for it, not dead API.
    #[allow(dead_code)]
    pub(crate) fn new(
        arena_resolution_cb: extern "C" fn(handle: SimHandle, net: SlotId),
        vc: &std::rc::Rc<VcState>,
    ) -> Self {
        StateKernelCtxFields {
            arena_resolution_cb,
            // `Rc::as_ptr` never bumps the strong count and never moves the
            // pointee — the raw pointer stays valid for exactly as long as
            // some `Rc<VcState>` clone (the engine's own, an `InstanceHandle`'s,
            // or both) keeps the allocation alive, which callers of this
            // constructor are required to uphold (documented above).
            vc_side_channel: std::rc::Rc::as_ptr(vc) as *mut VcState as *mut c_void,
        }
    }
}

/// ST-132: a marker trait, no methods. A `ProcessKernelFn`/`ProcessFn`
/// implementation that also implements `PureKernel` is asserting the
/// contract ST-132 requires: it is derivable purely from `(base, ctx)`, with
/// no other mutable state (no hidden thread-local/global) reachable from
/// inside it. This crate cannot *compel* a compiled kernel to implement this
/// trait (CODEGEN's own IR emitter, out of this wave's scope, would need a
/// `#[forbid(...)]`-style procedural check for a compiler-level guarantee) —
/// STATE's own enforcement mechanism this wave is the structural lint test
/// below: proving that nothing STATE itself exposes to a kernel author
/// *encourages* violating the contract (no STATE-owned API hands out a
/// thread-local or global mutable handle a kernel could close over instead
/// of going through `(base, ctx)`).
pub trait PureKernel {}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::state::instance::{StateEngine, TemplateRegistry};
    use crate::state::write::{SlotLocation, commit_write_at};
    use crate::state::{FourStateValue, SlotClass};
    use vj_abi::SimTime;

    fn fresh_engine() -> StateEngine {
        StateEngine::new(TemplateRegistry::new())
    }

    fn narrow_word(val: u64, unk: u64) -> u128 {
        (val as u128) | ((unk as u128) << 64)
    }

    fn t(units: i64) -> SimTime {
        SimTime {
            sim_units: units,
            delta: 0,
        }
    }

    extern "C" fn dummy_resolution_cb(_handle: SimHandle, _net: SlotId) {}

    /// ST-131: a compile-level proof — this struct literal fails to compile
    /// (not merely fails an assertion) if `StateKernelCtxFields` gains a
    /// third field, loses one of these two, or renames either one.
    #[test]
    fn state_kernel_ctx_fields_has_exactly_the_two_named_fields() {
        let fields = StateKernelCtxFields {
            arena_resolution_cb: dummy_resolution_cb,
            vc_side_channel: std::ptr::null_mut(),
        };
        assert_eq!(fields.vc_side_channel, std::ptr::null_mut());
        // The field is genuinely callable as the documented fn-pointer
        // shape (not merely a value that happens to type-check).
        (fields.arena_resolution_cb)(SimHandle(std::ptr::null_mut()), SlotId(0));
    }

    /// The forbidden patterns [`no_state_owned_api_exposes_a_thread_local_or_
    /// global_mutable_handle_to_a_kernel`] scans for, factored out so the
    /// documented-exception count check below exercises the exact same list.
    /// Built via concatenation (not one contiguous literal) so this file's
    /// own source text can never match itself, mirroring `accessor_impl.rs`'s
    /// existing `forbidden_global_patterns` convention.
    fn forbidden_mutable_global_patterns() -> Vec<String> {
        vec![["static", " mut"].concat(), ["lazy", "_static!"].concat()]
    }

    /// `production_code`/`find_matching_brace`: this module's own copy of
    /// the boundary-finder `accessor_impl.rs`'s tests module already
    /// established (private to that module, so not reusable from here) —
    /// isolates a scanned file's real, always-compiled production code from
    /// its trailing `#[cfg(test)] mod tests { ... }` block(s), so a test's
    /// own doc comment or fault-injection fixture text can never be mistaken
    /// for a real declaration. See `accessor_impl.rs`'s identically-named
    /// helpers for the full rationale (Task 10 r1/r2 review fixes) this copy
    /// carries over unchanged.
    fn production_code(src: &str) -> String {
        let marker = "#[cfg(test)]";
        let mut result = String::with_capacity(src.len());
        let mut cursor = 0usize;
        loop {
            let Some(rel_idx) = src[cursor..].find(marker) else {
                result.push_str(&src[cursor..]);
                break;
            };
            let idx = cursor + rel_idx;
            let after = &src[idx + marker.len()..];
            let after_trimmed = after.trim_start();
            if after_trimmed.starts_with("mod tests") {
                let trim_offset = after.len() - after_trimmed.len();
                let mod_tests_abs = idx + marker.len() + trim_offset;
                let after_keyword = &src[mod_tests_abs + "mod tests".len()..];
                let brace_abs = after_keyword
                    .find('{')
                    .map(|rel| mod_tests_abs + "mod tests".len() + rel);
                let close_abs = brace_abs.and_then(|b| find_matching_brace(src, b));
                match close_abs {
                    Some(close_abs) => {
                        result.push_str(&src[cursor..idx]);
                        cursor = close_abs + 1;
                        continue;
                    }
                    None => {
                        result.push_str(&src[cursor..idx]);
                        break;
                    }
                }
            }
            result.push_str(&src[cursor..idx + marker.len()]);
            cursor = idx + marker.len();
        }
        result
    }

    fn find_matching_brace(src: &str, open_idx: usize) -> Option<usize> {
        let bytes = src.as_bytes();
        debug_assert_eq!(bytes.get(open_idx), Some(&b'{'));
        let mut depth: i32 = 0;
        let mut i = open_idx;
        while i < bytes.len() {
            match bytes[i] {
                b'{' => {
                    depth += 1;
                    i += 1;
                }
                b'}' => {
                    depth -= 1;
                    if depth == 0 {
                        return Some(i);
                    }
                    i += 1;
                }
                b'/' if bytes.get(i + 1) == Some(&b'/') => {
                    while i < bytes.len() && bytes[i] != b'\n' {
                        i += 1;
                    }
                }
                b'/' if bytes.get(i + 1) == Some(&b'*') => {
                    i += 2;
                    let mut comment_depth = 1u32;
                    while i < bytes.len() && comment_depth > 0 {
                        if bytes[i] == b'/' && bytes.get(i + 1) == Some(&b'*') {
                            comment_depth += 1;
                            i += 2;
                        } else if bytes[i] == b'*' && bytes.get(i + 1) == Some(&b'/') {
                            comment_depth -= 1;
                            i += 2;
                        } else {
                            i += 1;
                        }
                    }
                }
                b'"' => {
                    i += 1;
                    while i < bytes.len() && bytes[i] != b'"' {
                        if bytes[i] == b'\\' {
                            i += 1;
                        }
                        i += 1;
                    }
                    i += 1;
                }
                b'\'' => {
                    const MAX_CHAR_LITERAL_LEN: usize = 12;
                    let window_end = (i + 1 + MAX_CHAR_LITERAL_LEN).min(bytes.len());
                    let mut j = i + 1;
                    let mut found_close = None;
                    while j < window_end {
                        if bytes[j] == b'\n' {
                            break;
                        }
                        if bytes[j] == b'\'' && j > i + 1 {
                            found_close = Some(j);
                            break;
                        }
                        j += 1;
                    }
                    match found_close {
                        Some(close) => i = close + 1,
                        None => i += 1,
                    }
                }
                _ => {
                    i += 1;
                }
            }
        }
        None
    }

    /// ST-132: the concrete, testable half of "no hidden mutable state
    /// outside (base, ctx)" that lives on STATE's side of the boundary — a
    /// source-text structural scan (same technique as Task 10's
    /// `no_public_function_returns_a_raw_absolute_pointer...` test) of every
    /// `.rs` file under `state/`:
    ///
    /// - `static mut`/`lazy_static!` must never appear in any production
    ///   code under `state/` — zero exceptions.
    /// - `thread_local!` must appear **exactly once** in all of `state/`'s
    ///   production code, and that one declaration must be `write.rs`'s
    ///   `WORKER_CACHE` — Task 8's documented, explicitly-thread-local
    ///   speculative-buffer *cache* (a thread's own cached `WorkerSlot`/
    ///   buffer pointer once registered via `register_worker_thread`; the
    ///   `worker_buffers: Vec<Box<RefCell<Vec<VcRecord>>>>` registry it is
    ///   backed by is an ordinary `VcState`/`StateEngine`-owned field, not a
    ///   second thread-local/global). This is intentional per-thread PDES
    ///   state, not kernel-visible mutable global state — the one allowed
    ///   exception, named explicitly here so a *third* `thread_local!`
    ///   anywhere else in `state/` fails this test. (The second, event_plan.rs's
    ///   design-scope PLAN, is read-only per-design syntax data.)
    #[test]
    fn no_state_owned_api_exposes_a_thread_local_or_global_mutable_handle_to_a_kernel() {
        let forbidden = forbidden_mutable_global_patterns();
        let state_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/state");
        let entries = std::fs::read_dir(&state_dir).expect("state/ directory is readable");

        let mut thread_local_hits: Vec<String> = Vec::new();

        for entry in entries {
            let entry = entry.expect("directory entry is readable");
            let path = entry.path();
            if path.extension().and_then(|e| e.to_str()) != Some("rs") {
                continue;
            }
            let contents = std::fs::read_to_string(&path)
                .unwrap_or_else(|e| panic!("{} must be readable: {e}", path.display()));
            let scanned = production_code(&contents);
            let file_name = path.file_name().unwrap().to_string_lossy().into_owned();

            for pattern in &forbidden {
                assert!(
                    !scanned.contains(pattern.as_str()),
                    "{file_name}'s production code must not contain a `{pattern}` -- \
                     no STATE-owned API may expose a hidden mutable global to a kernel"
                );
            }

            for _ in 0..scanned.matches("thread_local!").count() {
                thread_local_hits.push(file_name.clone());
            }
        }

        thread_local_hits.sort();
        assert_eq!(
            thread_local_hits,
            vec!["event_plan.rs".to_string(), "write.rs".to_string()],
            "exactly two `thread_local!` declarations must exist in state/'s production \
             code -- Task 8's documented WORKER_CACHE speculative-buffer cache in write.rs, \
             and event_plan.rs's read-only design-scope PLAN (the design-wide event pool \
             plan, installed per design by EventPlanScope like the other thread-local \
             design scopes; it holds no kernel handle) -- and nothing else; \
             found: {thread_local_hits:?}"
        );
    }

    /// Fault injection proving [`no_state_owned_api_exposes_a_thread_local_or_
    /// global_mutable_handle_to_a_kernel`]'s own machinery actually catches a
    /// violation, rather than passing vacuously on every input (a "gate that
    /// cannot fail"): a synthetic file shaped exactly like the forbidden
    /// second-`thread_local!`/`static mut` case must be flagged by the same
    /// `production_code` + pattern scan the real test uses.
    #[test]
    fn thread_local_scan_machinery_catches_a_synthetic_second_declaration() {
        let shaped = concat!(
            "pub struct Whatever;\n",
            "\n",
            "thread_local! {\n",
            "    static SNEAKY: std::cell::RefCell<u32> = const { std::cell::RefCell::new(0) };\n",
            "}\n",
            "\n",
            "#[cfg(test)]\n",
            "mod tests {\n",
            "    // real test code, excluded from the scan\n",
            "}\n",
        );
        let scanned = production_code(shaped);
        assert_eq!(
            scanned.matches("thread_local!").count(),
            1,
            "the synthetic fixture's own injected thread_local! must survive the \
             mod-tests-exclusion scan (it lives outside `mod tests`) so the real test's \
             exactly-one-total-across-the-crate check would trip on it"
        );

        let forbidden = forbidden_mutable_global_patterns();
        let static_mut_shaped = "pub static mut SNEAKY_GLOBAL: u32 = 0;\n";
        assert!(
            forbidden
                .iter()
                .any(|p| static_mut_shaped.contains(p.as_str())),
            "a reintroduced `static mut` must be caught by the forbidden-pattern list"
        );
        let lazy_static_shaped = "lazy_static! { static ref SNEAKY: u32 = 0; }\n";
        assert!(
            forbidden
                .iter()
                .any(|p| lazy_static_shaped.contains(p.as_str())),
            "a reintroduced `lazy_static!` must be caught by the forbidden-pattern list"
        );
    }

    /// ST-131: `vc_side_channel` genuinely names the same speculative buffer
    /// `StateEngine::drain_committed_changes` (Task 12) reads — not a decoy
    /// or a no-op stand-in. Builds `StateKernelCtxFields` from a live
    /// `StateEngine`, casts `vc_side_channel` back to `&VcState` (test-only —
    /// `VcState` itself is `pub(crate)`, so this is an ordinary same-crate
    /// access, not a special-cased accessor), appends a write through
    /// `commit_write_at` using that recovered reference directly (bypassing
    /// `engine.commit_write` entirely), then confirms the appended record is
    /// visible via `engine.drain_committed_changes` after `advance_gvt` —
    /// proving the opaque pointer round-trips to the one real buffer the
    /// engine itself also drains from.
    #[test]
    fn vc_side_channel_pointer_round_trips_to_the_same_speculative_buffer() {
        let mut engine = fresh_engine();
        // SAFETY: local, single-owned engine; no other live borrow of the
        // inline plane, single-threaded test.
        let word_offset = unsafe { engine.arena.alloc_inline_word() };
        let loc = SlotLocation {
            class: SlotClass::Inline,
            word_offset,
            bit_width: 8,
            elem_word_count: 1,
            row_stride_words: 0,
            num_rows: 0,
        };
        let cell = engine.alloc_cell(loc);

        let fields = StateKernelCtxFields::new(dummy_resolution_cb, &engine.vc);

        // Recover `&VcState` from the opaque pointer -- the exact cast a
        // real compiled-kernel call site would need to perform on its own
        // side of the ABI, done here purely to prove the pointer is genuine.
        //
        // SAFETY: `fields.vc_side_channel` was constructed immediately above
        // by `StateKernelCtxFields::new` from `Rc::as_ptr(&engine.vc)`;
        // `engine` (and therefore its `Rc<VcState>` allocation) is still
        // alive and un-mutated for the entire body of this test, so the
        // pointee is live and this reference does not outlive it.
        let vc_ref: &VcState = unsafe { &*(fields.vc_side_channel as *const VcState) };

        engine.set_current_time(t(5));
        let at = t(5);
        // Append directly through the recovered side-channel reference, not
        // through `engine.commit_write` -- this is the whole point of the
        // round-trip proof: the append must land in the same buffer
        // `drain_committed_changes` below reads.
        let result = commit_write_at(
            &engine.arena,
            vc_ref,
            loc,
            cell,
            FourStateValue::Narrow(narrow_word(0x2A, 0)),
            &mut (),
        );
        assert!(
            result.changed,
            "the write through the recovered side channel must register as a change"
        );

        engine.advance_gvt(at);
        let drained = engine.drain_committed_changes(at);
        assert_eq!(
            drained.len(),
            1,
            "exactly the one record appended through the side channel must be visible"
        );
        assert_eq!(drained[0].cell, cell);
        match &drained[0].new {
            FourStateValue::Narrow(w) => assert_eq!(*w, narrow_word(0x2A, 0)),
            FourStateValue::Wide { .. } => panic!("expected Narrow"),
        }

        // Negative control: before `advance_gvt` ran (GVT-gating, Task 12),
        // the record must not have been visible -- otherwise this test would
        // prove nothing about the side channel specifically, only that
        // `drain_committed_changes` works at all.
        let mut engine2 = fresh_engine();
        let word_offset2 = unsafe { engine2.arena.alloc_inline_word() };
        let loc2 = SlotLocation {
            class: SlotClass::Inline,
            word_offset: word_offset2,
            bit_width: 8,
            elem_word_count: 1,
            row_stride_words: 0,
            num_rows: 0,
        };
        let cell2 = engine2.alloc_cell(loc2);
        let fields2 = StateKernelCtxFields::new(dummy_resolution_cb, &engine2.vc);
        let vc_ref2: &VcState = unsafe { &*(fields2.vc_side_channel as *const VcState) };
        engine2.set_current_time(t(10));
        commit_write_at(
            &engine2.arena,
            vc_ref2,
            loc2,
            cell2,
            FourStateValue::Narrow(narrow_word(0x7, 0)),
            &mut (),
        );
        let too_early = engine2.drain_committed_changes(t(0));
        assert!(
            too_early.is_empty(),
            "before advance_gvt reaches the write's timestamp, drain_committed_changes must not see it"
        );
    }
}