vj 0.2.0

vj: a SystemVerilog (IEEE 1800-2023) simulator built on slang and LLVM.
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
// Hand-written, ABI-conformant test-kernel fixture (Task 10 of
// docs/superpowers/plans/2026-09-23-wave-A-abi.md; ledger AB-003, AB-004,
// AB-074..AB-078). NOT compiled as part of the cargo workspace -- this is a
// source TEMPLATE that `tests/loader.rs` reads, substitutes two tokens
// into, and compiles on the fly via `rustc --crate-type=cdylib` into a
// temporary `.so`.
//
// Substituted tokens (plain string replacement, never `format!`, so this
// file's own braces need no escaping):
//   __ABI_VERSION__ -- replaced with a literal u32 (the value
//                       `sim_abi_version()` returns).
//   __ABORT__       -- replaced with either `std::process::abort();` (every
//                       entry point/accessor EXCEPT `sim_abi_version` traps
//                       the whole process immediately when called) or the
//                       empty string (the fixture behaves normally). This
//                       is what makes
//                       `refuses_mismatched_abi_version_before_touching_any_other_symbol`
//                       and
//                       `loader_calls_abi_version_immediately_after_open_before_any_other_symbol`
//                       able to actually FAIL (not just assert) if the
//                       loader ever calls the wrong thing at the wrong
//                       time.
//
// Does not depend on the `vj-abi` crate at all: only the ABI's binary
// LAYOUT matters across a `dlopen`/`libloading` boundary, never Rust
// nominal-type identity, so every type below is a local, `#[repr(C)]`/
// `#[repr(transparent)]`-compatible mirror of the real `vj_abi` shape
// it stands in for (exactly how a real codegen-emitted kernel -- which
// emits raw LLVM IR, never Rust source depending on this crate -- already
// has to work).

#[repr(C)]
pub struct EvalResultRaw {
    pub outcome: u32,
    pub reschedule: [u8; 32],
}

fn completed() -> EvalResultRaw {
    EvalResultRaw {
        outcome: 0, // EvalOutcome::Completed
        reschedule: [0u8; 32],
    }
}

#[repr(C)]
pub struct SimTimeRaw {
    pub sim_units: i64,
    pub delta: u32,
}

#[repr(C)]
pub struct WideViewRaw {
    pub aval: *mut u64,
    pub bval: *mut u64,
    pub word_count: u32,
}

// Task 13 (docs/superpowers/plans/2026-09-23-wave-A-abi.md; ledger AB-064,
// AB-065, AB-066, AB-067, AB-068) extends Task 10's original "one narrow
// scalar cell only" fixture with three more narrow cells and a distinct
// NBA staging buffer, so the entry-point BEHAVIORAL tests in
// `tests/entry_points_behavior.rs` have real, distinguishable state to
// observe -- never aliasing the same storage for two different meanings
// (an earlier draft of this fixture had every slot alias one `CELL`,
// which let several of those tests pass for the wrong reason; each slot
// below is its own, separately zeroed/read/written `u128`).
//
// `sv_get_scalar`/`sv_set_scalar`'s `slot` parameter selects among:
//   slot 0 -- `CELL`    : Task 10's original counter cell, incremented by
//                         the one process in `VJ_PROCESS_TABLE` when
//                         dispatched (directly, or -- Task 13 -- via
//                         `sim_eval_region(Region::Active, ..)` when
//                         `REQUEST_CELL` is nonzero).
//   slot 1 -- `IN_CELL` : the comb process's input ("in").
//   slot 2 -- `OUT_CELL`: the comb process's output ("out"); `sim_settle`
//                         copies `IN_CELL` into it directly (a fixed point
//                         in one pass, since `out` never feeds back into
//                         `in`). Also the NBA-commit target: `sim_eval_
//                         region(Region::Nba, ..)` stages `IN_CELL`'s
//                         current value into `NBA_STAGE` (an `out <= in`
//                         nonblocking write) without touching `OUT_CELL`
//                         itself; only `sim_commit_nba` applies it.
//   slot 3 -- `REQUEST_CELL`: gates whether `sim_eval_region(Region::
//                         Active, ..)` dispatches the counter process at
//                         all (AB-067's region-scoping proof).
//   slot 4 -- `DELAY_CELL`: Task 14 (AB-071) -- marks whether the fixture's
//                         one delay-suspended process has run. That
//                         process becomes runnable only once the kernel's
//                         own notion of "now" (`CURRENT_TIME`, advanced
//                         exclusively by `sim_step_time`) has reached its
//                         fixed due time (`DELAY_DUE_TIME`, sim_units=10);
//                         `sim_eval_region(Region::Active, ..)` is what
//                         actually dispatches it once due, same as the
//                         request-gated counter process above, but gated
//                         on elapsed time instead of an explicit request
//                         flag. Runs at most once (`DELAY_RAN` guards
//                         re-firing on a later Region::Active evaluation
//                         after its due time has already passed).
//   slot 5 -- `FINAL_CELL`: Task 14 (AB-072) -- incremented by `sim_final`
//                         exactly once, no matter how many times
//                         `sim_final` itself is called (`FINAL_RAN` guards
//                         the second call).
static mut CELL: u128 = 0;
static mut IN_CELL: u128 = 0;
static mut OUT_CELL: u128 = 0;
static mut REQUEST_CELL: u128 = 0;
// NBA staging buffer (AB-068), kept structurally distinct from `OUT_CELL`
// (the committed cell it eventually writes into): `NBA_PENDING` disting-
// uishes "no write staged" from "a write of value 0 is staged", since 0 is
// a legitimate staged value.
static mut NBA_STAGE: u128 = 0;
static mut NBA_PENDING: bool = false;
// Task 14 (AB-071): the kernel's own notion of "now", advanced only by
// `sim_step_time` -- never touched anywhere else. The one delay-suspended
// process becomes runnable once `CURRENT_TIME >= DELAY_DUE_TIME`.
static mut CURRENT_TIME: i64 = 0;
const DELAY_DUE_TIME: i64 = 10;
static mut DELAY_CELL: u128 = 0;
static mut DELAY_RAN: bool = false;
// Task 14 (AB-072): the final-block counter cell and its "already ran"
// guard -- a second `sim_final` call must not re-increment it.
static mut FINAL_CELL: u128 = 0;
static mut FINAL_RAN: bool = false;

#[unsafe(no_mangle)]
pub extern "C" fn sim_abi_version() -> u32 {
    __ABI_VERSION__
}

#[unsafe(no_mangle)]
pub extern "C" fn sim_init(_design_id: u32) -> *mut core::ffi::c_void {
    __ABORT__
    // SAFETY: this fixture is single-threaded and never re-entrant --
    // every caller in `tests/loader.rs`/`tests/entry_points_behavior.rs`
    // calls into one `LoadedKernel` at a time via its own `&mut self`
    // discipline.
    unsafe {
        CELL = 0;
        IN_CELL = 0;
        OUT_CELL = 0;
        REQUEST_CELL = 0;
        NBA_STAGE = 0;
        NBA_PENDING = false;
        CURRENT_TIME = 0;
        DELAY_CELL = 0;
        DELAY_RAN = false;
        FINAL_CELL = 0;
        FINAL_RAN = false;
    }
    // A real `SimHandle` payload would point at real per-design state; this
    // fixture has none, so it hands back a real, non-null, well-aligned
    // dangling-but-valid-for-ptr-identity `u8` pointer -- nothing ever
    // dereferences it, only compares/threads it through as an opaque
    // handle.
    core::ptr::NonNull::<u8>::dangling().as_ptr().cast()
}

#[unsafe(no_mangle)]
pub extern "C" fn sim_settle(_h: *mut core::ffi::c_void) -> EvalResultRaw {
    __ABORT__
    // AB-066: drive all initial-time activity to quiescence in this one
    // call. The fixture's only comb process is "out = in"; since `out`
    // never feeds back into `in`, copying once already IS the fixed
    // point -- no loop is needed to justify returning `Completed` rather
    // than `MoreWorkInRegion`.
    // SAFETY: see sim_init's SAFETY note.
    unsafe {
        OUT_CELL = IN_CELL;
    }
    completed()
}

#[unsafe(no_mangle)]
pub extern "C" fn sim_eval_region(
    _h: *mut core::ffi::c_void,
    _region: u32,
    _t: SimTimeRaw,
) -> EvalResultRaw {
    __ABORT__
    // Region discriminants mirror `vj_abi::Region`'s frozen repr(u32)
    // layout (Active=0, Inactive=1, Nba=2, Reactive=3, Postponed=4) --
    // matched exhaustively with a loud panic on anything else, never a
    // silent no-op on an out-of-range value (this fixture's own
    // correctness posture, not a "kind enum" match, but held to the same
    // standard).
    match _region {
        0 => {
            // Region::Active: the counter process (Task 10's
            // `test_kernel_process`, dispatched by real table lookup, not
            // a hand-inlined increment) runs only when REQUEST_CELL is
            // nonzero -- AB-067's region-scoping proof.
            // SAFETY: see sim_init's SAFETY note.
            let request = unsafe { REQUEST_CELL };
            if request != 0 {
                return VJ_PROCESS_TABLE[0](_h, core::ptr::null_mut(), 0);
            }
            // Task 14 (AB-071): the delay-suspended process becomes
            // runnable once `sim_step_time` has advanced CURRENT_TIME to
            // (or past) its fixed due time, and runs at most once (guarded
            // by DELAY_RAN, so re-evaluating Region::Active after it has
            // already fired does not re-fire it).
            // SAFETY: see sim_init's SAFETY note.
            unsafe {
                if CURRENT_TIME >= DELAY_DUE_TIME && !DELAY_RAN {
                    DELAY_CELL = DELAY_CELL.wrapping_add(1);
                    DELAY_RAN = true;
                }
            }
        }
        1 => {
            // Region::Inactive: nothing in this fixture is Inactive-
            // runnable.
        }
        2 => {
            // Region::Nba: stage `out <= in` (AB-068) -- writes the
            // STAGING buffer only, never OUT_CELL itself. Visible only
            // after a subsequent sim_commit_nba call.
            // SAFETY: see sim_init's SAFETY note.
            unsafe {
                NBA_STAGE = IN_CELL;
                NBA_PENDING = true;
            }
        }
        3 => {
            // Region::Reactive: nothing in this fixture is
            // Reactive-runnable.
        }
        4 => {
            // Region::Postponed: nothing in this fixture is
            // Postponed-runnable.
        }
        other => panic!("test_kernel: sim_eval_region called with unknown region {other}"),
    }
    completed()
}

#[unsafe(no_mangle)]
pub extern "C" fn sim_commit_nba(_h: *mut core::ffi::c_void, _t: SimTimeRaw) -> EvalResultRaw {
    __ABORT__
    // AB-068: apply the staged NBA update queue -- a distinct call from
    // whatever staged it in sim_eval_region(Region::Nba, ..) above.
    // SAFETY: see sim_init's SAFETY note.
    unsafe {
        if NBA_PENDING {
            OUT_CELL = NBA_STAGE;
            NBA_PENDING = false;
        }
    }
    completed()
}

#[unsafe(no_mangle)]
pub extern "C" fn sim_step_time(_h: *mut core::ffi::c_void, _t: SimTimeRaw) -> EvalResultRaw {
    __ABORT__
    // AB-071: advance the kernel's own notion of "now" -- the ONLY place
    // CURRENT_TIME is ever written. A subsequent sim_eval_region(Active,
    // ..) call is what actually makes the due delay-suspended process
    // runnable; this entry point only does the time-wheel bookkeeping.
    // SAFETY: see sim_init's SAFETY note.
    unsafe {
        CURRENT_TIME = _t.sim_units;
    }
    completed()
}

#[unsafe(no_mangle)]
pub extern "C" fn sim_save(_h: *mut core::ffi::c_void) -> *mut core::ffi::c_void {
    __ABORT__
    core::ptr::null_mut()
}

#[unsafe(no_mangle)]
pub extern "C" fn sim_restore(_h: *mut core::ffi::c_void, _checkpoint: *mut core::ffi::c_void) {
    __ABORT__
}

#[unsafe(no_mangle)]
pub extern "C" fn sim_final(_h: *mut core::ffi::c_void) {
    __ABORT__
    // AB-072: run every final block exactly once, no matter how many times
    // sim_final itself is called -- FINAL_RAN guards the second call.
    // SAFETY: see sim_init's SAFETY note.
    unsafe {
        if !FINAL_RAN {
            FINAL_CELL = FINAL_CELL.wrapping_add(1);
            FINAL_RAN = true;
        }
    }
}

#[unsafe(no_mangle)]
pub extern "C" fn sim_process_count(_h: *mut core::ffi::c_void) -> u32 {
    __ABORT__
    // AB-073: computed from the real process table's length, never a
    // hand-typed literal -- so this stays correct if the table ever grows,
    // and so `tests/entry_points_behavior.rs`'s
    // `sim_process_count_matches_process_table_length` test can actually
    // catch a future regression (e.g. someone re-hardcoding this) by
    // comparing against the independent `__test_process_table_len`
    // companion accessor below.
    VJ_PROCESS_TABLE.len() as u32
}

#[unsafe(no_mangle)]
pub extern "C" fn sv_get_scalar(_h: *mut core::ffi::c_void, _slot: u32) -> u128 {
    __ABORT__
    // SAFETY: see sim_init's SAFETY note -- single-threaded, non-reentrant
    // fixture.
    unsafe {
        match _slot {
            0 => CELL,
            1 => IN_CELL,
            2 => OUT_CELL,
            3 => REQUEST_CELL,
            4 => DELAY_CELL,
            5 => FINAL_CELL,
            other => panic!("test_kernel: sv_get_scalar called with unknown slot {other}"),
        }
    }
}

#[unsafe(no_mangle)]
pub extern "C" fn sv_set_scalar(_h: *mut core::ffi::c_void, _slot: u32, v: u128) {
    __ABORT__
    // SAFETY: see sim_init's SAFETY note.
    unsafe {
        match _slot {
            0 => CELL = v,
            1 => IN_CELL = v,
            2 => OUT_CELL = v,
            3 => REQUEST_CELL = v,
            other => panic!("test_kernel: sv_set_scalar called with unknown slot {other}"),
        }
    }
}

// The remaining four wide/memory-row accessors are trivial, never-called
// stubs for this fixture -- Task 10's own spec ("sv_get_wide/sv_set_wide/
// sv_get_mem_word/sv_set_mem_word can be trivial unreachable!() stubs for
// this fixture, documented as such"). No test in `tests/loader.rs` ever
// resolves or calls these.
#[unsafe(no_mangle)]
pub extern "C" fn sv_get_wide(_h: *mut core::ffi::c_void, _slot: u32) -> WideViewRaw {
    unreachable!("sv_get_wide is a trivial, never-called stub in this fixture")
}

#[unsafe(no_mangle)]
pub extern "C" fn sv_set_wide(_h: *mut core::ffi::c_void, _slot: u32, _v: WideViewRaw) {
    unreachable!("sv_set_wide is a trivial, never-called stub in this fixture")
}

#[unsafe(no_mangle)]
pub extern "C" fn sv_get_mem_word(
    _h: *mut core::ffi::c_void,
    _slot: u32,
    _row: u64,
) -> WideViewRaw {
    unreachable!("sv_get_mem_word is a trivial, never-called stub in this fixture")
}

#[unsafe(no_mangle)]
pub extern "C" fn sv_set_mem_word(
    _h: *mut core::ffi::c_void,
    _slot: u32,
    _row: u64,
    _v: WideViewRaw,
) {
    unreachable!("sv_set_mem_word is a trivial, never-called stub in this fixture")
}

// The 1-entry process table: dispatches (by index, matching
// `vj_abi::ProcessFn`'s frozen shape) to a process that increments the
// fixture's one cell by 1.
extern "C" fn test_kernel_process(
    _h: *mut core::ffi::c_void,
    _local: *mut u8,
    _resume_pc: u32,
) -> EvalResultRaw {
    // SAFETY: see sim_init's SAFETY note.
    unsafe {
        CELL = CELL.wrapping_add(1);
    }
    completed()
}

#[unsafe(no_mangle)]
pub static VJ_PROCESS_TABLE: [extern "C" fn(*mut core::ffi::c_void, *mut u8, u32) -> EvalResultRaw; 1] =
    [test_kernel_process];

// Task 14 (AB-073): a test-only companion accessor -- NOT part of the
// vj ABI (no `vj_abi` type or symbol constant names it), never
// exported by a real compiled kernel, and never gated by `__ABORT__`
// (it is not one of the entry points the abort-everything-but-version
// negative fixture exercises). Its only purpose is to let
// `tests/entry_points_behavior.rs`'s
// `sim_process_count_matches_process_table_length` test read
// VJ_PROCESS_TABLE's real length through a path that is INDEPENDENT
// of `sim_process_count`'s own implementation, so the test is a genuine
// two-source agreement check rather than comparing a function to itself.
#[unsafe(no_mangle)]
pub extern "C" fn __test_process_table_len() -> u32 {
    VJ_PROCESS_TABLE.len() as u32
}