vj-sim 0.2.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
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
//! Task 19 — `RescheduleReq` layout proposal & the `EventRegion`/`Region`
//! reconciliation note (ledger SC-109, SC-113).
//!
//! `vj_abi::eval::RescheduleReq` is the real, ABI-frozen, reserved-opaque
//! type (`opaque: [u8; RESCHEDULE_REQ_SIZE]`, `RESCHEDULE_REQ_SIZE == 32`) —
//! it lives in `vj-abi`, owned by the **abi** track, developing
//! concurrently in a sibling worktree this session cannot see or safely
//! edit. SC-113 asks the **scheduler** subsystem to supply the concrete
//! field layout; this module delivers that as a documented Rust-shape
//! *proposal* for the abi track to adopt in their own file, not a live edit
//! to `vj-abi`. Accordingly this whole module is `#[allow(dead_code)]`,
//! never constructed by real (non-test) code this wave, and exports nothing
//! consumed elsewhere in this crate (`sched/mod.rs` declares it with a plain
//! `mod reschedule_req_proposal;`, not `pub use reschedule_req_proposal::*;`)
//! — it exists to be read.
//!
//! ## SC-109 reconciliation
//!
//! `docs/design/verification.md`'s original `EventRegion` sketch —
//! `{ Active, Inactive, NBA, Observed, Reactive, Postponed, Cleanup }`, 7
//! variants — is superseded. This is not re-decided here: it was already
//! settled by `ARCHITECTURE.md` §5/§7 item 2, and the real implementation is
//! `vj_abi::Region`'s existing 5-variant `#[repr(u32)]` enum (`Active`,
//! `Inactive`, `Nba`, `Reactive`, `Postponed`). `Observed` is reserved
//! (unused) ABI discriminant space for later SVA regions; `Cleanup` is
//! dropped entirely for v1. `CommittedEffect`/ordering keys use
//! `vj_abi::Region`'s 5 variants, full stop — [`super::mod@self`]'s
//! sibling test `region_has_exactly_five_variants_no_preponed_observed_re_inactive_re_nba`
//! (Task 1, `sched/mod.rs`) is the executable proof of that; this module's
//! own `sc109_event_region_uses_the_abi_five_variant_set` test below repeats
//! the same assertions so SC-109 has its own passing, labeled witness rather
//! than only a shared one.
#![allow(dead_code)]

use vj_abi::SimHandle;

use crate::process::ArenaRange;
use crate::sched::{JoinKind, SimTick};
use crate::state::{EdgeKind, StateCellId};

/// `RescheduleReqLayoutProposal::kind == KIND_DELAY` — payload is `a`/`b`
/// (the `Suspend::Delay(u64)` tick count, low/high `u32` halves).
pub const KIND_DELAY: u8 = 0;
/// `RescheduleReqLayoutProposal::kind == KIND_EDGE` — payload is
/// `edge_kind`/`edge_signal` (a `Suspend::WaitEvents` set with exactly one
/// `EventTerm::Edge` term).
pub const KIND_EDGE: u8 = 1;
/// `RescheduleReqLayoutProposal::kind == KIND_EDGE_LIST` — payload is `a`/`b`
/// as an `(ArenaRange.base, ArenaRange.count)` pair (a `Suspend::WaitEvents`
/// set with more than one term).
pub const KIND_EDGE_LIST: u8 = 2;
/// `RescheduleReqLayoutProposal::kind == KIND_FORK_JOIN` — payload is
/// `fork_join_kind` plus `a`/`b` as an `(ArenaRange.base, ArenaRange.count)`
/// pair over the pending `SpawnSpec`s (`Suspend::ForkJoin`).
pub const KIND_FORK_JOIN: u8 = 3;
/// `RescheduleReqLayoutProposal::kind == KIND_DONE` — no payload
/// (`Suspend::Done`).
pub const KIND_DONE: u8 = 4;
/// **Task 6 (CG-171):** `RescheduleReqLayoutProposal::kind == KIND_REPEAT_EDGE`
/// — payload is `edge_kind`/`edge_signal` (same fields `KIND_EDGE` uses for
/// its own single-edge term) plus `a`/`b` as the repeat COUNT's low/high
/// `u32` halves (same split `KIND_DELAY`'s own `delay_ticks` uses) — a
/// `Suspend::WaitEvents` set with exactly one `EventTerm::Repeat{count,
/// inner: Edge{cell, edge}}` term. Closes CG-081's `RepeatedEventControl`
/// (`repeat(n) @(posedge clk)`, when reachable -- see `process::cfg::
/// SuspendPoint`'s own doc comment for why it never is via a `Timed`
/// statement in this wave) and CG-171's revised `CycleDelay` (`##N` written
/// inside a process that ALSO itself syntactically sights a posedge/negedge
/// edge -- the common `always_ff @(posedge clk) ... ##N ...` idiom; NOT the
/// `default clocking`-bound shape, see `process::cfg::SuspendPoint::
/// CycleDelay`'s own doc comment for the verified, narrower scope this
/// closes): both resolve to the identical wire shape and the identical
/// scheduler-side `EventTerm::Repeat`-over-`Edge` machinery (SC-052/SC-053,
/// already built and tested) -- a single-cell, single-edge repeat wait fits
/// the frozen 24-byte budget inline, with no out-of-line table needed at
/// all (unlike `KIND_EDGE_LIST`'s genuinely multi-signal shape).
/// **Deviation from the plan's literal separate `KIND_REPEAT_EDGE=5`/
/// `KIND_CYCLE_DELAY=6` pair, carrying a `ClockDomainId` for `CycleDelay`
/// specifically:** that design needs a live WOCB clock-domain-inference
/// pipeline (`analysis::analyze()`) threaded from `vj::run`'s
/// elaboration all the way to `sched::Engine`, which does not exist
/// anywhere in this tree today (`analyze()` is never called from `run.rs`/
/// `aot.rs`/`pipeline.rs`) and would need exactly the `run.rs`-touching
/// change this wave's binding rules forbid risking (wave-B-runtime is
/// concurrently landing there). This tag instead reuses `CycleDelay`'s
/// ALREADY-real, codegen-time-resolved, syntactically-inferred single clock
/// (`process::cfg` already tracked and ambiguity-checked this before this
/// task; Task 6 only threads it through as real payload instead of
/// discarding it) -- for the shape it resolves, this satisfies CG-171's
/// literal acceptance text ("a real Suspend terminator against the inferred
/// ClockDomain ... converted cycles->ticks by the scheduler at runtime ...
/// never a hard LoweringError and never a plain #N reinterpretation")
/// without inventing the unbuilt pipeline, and the
/// cycles->ticks conversion genuinely still happens at scheduler RUNTIME
/// (`RepeatCounters`/`EventTerm::Repeat` counts live edges, never a cached
/// tick offset) -- never moved back into codegen, the exact inversion the
/// plan's own CG-171 revision (itself a review-fix supersession of an
/// earlier, wronger draft) was written to rule out.
pub const KIND_REPEAT_EDGE: u8 = 5;

/// `wait fork;` (LRM 9.6.1, SC-125): suspend until every child this process
/// forked has finished. Payload-free -- the scheduler already tracks the
/// children (`ProcessDescriptor::fork_children`), so decodes to
/// `Suspend::WaitFork`.
pub const KIND_WAIT_FORK: u8 = 6;

/// `process::await()` / `process::suspend()` (LRM 9.7, classes plan Task
/// 13): the process just queued a `ProcessRequest` and yields so the engine
/// can apply it. Payload-free; decodes to `Suspend::ByRequest`.
pub const KIND_BY_REQUEST: u8 = 7;

/// Field-layout proposal for `vj_abi::eval::RescheduleReq`'s currently
/// reserved-opaque `opaque: [u8; RESCHEDULE_REQ_SIZE]` payload (SC-113), sized
/// to fit inside the frozen `RESCHEDULE_REQ_SIZE == 32` bytes (ARCHITECTURE
/// §7 item 3 / `vj-abi::eval`'s own doc comment: "Only
/// `VJ_ABI_VERSION` bumps may change `RESCHEDULE_REQ_SIZE`").
///
/// **Review fix carried over from the plan:** an earlier draft of this field
/// list — one dedicated field per `Suspend`/`WaitSpecStorage` variant need,
/// declared in the "natural" order with ordinary `#[repr(C)]` alignment
/// padding — sizes to 56 bytes, silently blowing the frozen 32-byte budget
/// the moment the abi track tried to adopt it. This corrected, packed layout
/// exploits that the payload fields are mutually exclusive per `kind` (a
/// tagged union, never all populated at once) and reuses two generic `u32`
/// slots (`a`/`b`) across variants: `size_of::<RescheduleReqLayoutProposal>()
/// == 24` (`kind`/`edge_kind`/`fork_join_kind`/`_reserved` pack into bytes
/// 0-3, `a`/`b` into bytes 4-11, `edge_signal` at the next 8-byte-aligned
/// offset, bytes 16-23 — pinned by
/// `layout_proposal_is_repr_c_and_copy` below), leaving 8 of the 32 reserved
/// bytes free for a future field without a version bump. `delay_ticks`
/// splits into `a`/`b` (low/high `u32` halves of the `u64` tick count)
/// rather than getting its own dedicated `u64` field, which is what buys
/// back the 32 bytes the naive layout overspent; a `u64` tick count that
/// large is not a realistic simulation length, but the split keeps the type
/// honest about the full range `SimTick`/`Suspend::Delay(u64)` allow (see
/// `delay_round_trips_through_a_b_split` below).
///
/// The `iff: Option<PredicateFn>` half of `WaitSpecStorage::Edge`/
/// `WaitSpec::Edge` is deliberately **not** represented here: `PredicateFn`'s
/// calling convention is itself an open, unresolved joint decision
/// (`docs/CONTRACTS.md` open question 4), so this proposal covers exactly
/// what `Suspend`'s four variants need to be reconstructed by the host
/// driver and leaves `iff` cross-ABI representation to whichever future task
/// resolves that open question — not invented here as a side effect of a
/// layout exercise.
#[repr(C)]
#[derive(Clone, Copy, Debug)]
#[allow(dead_code)]
pub struct RescheduleReqLayoutProposal {
    /// One of `KIND_DELAY`/`KIND_EDGE`/`KIND_EDGE_LIST`/`KIND_FORK_JOIN`/
    /// `KIND_DONE`/`KIND_REPEAT_EDGE`. Decoded (never matched raw) via
    /// [`Self::kind_enum`].
    pub kind: u8,
    /// Valid iff `kind == KIND_EDGE` or `kind == KIND_REPEAT_EDGE`: the
    /// `EdgeKind` discriminant (`EdgeKind as u8`; `EdgeKind` has 4 fieldless
    /// variants, fits).
    pub edge_kind: u8,
    /// Valid iff `kind == KIND_FORK_JOIN`: the `JoinKind` discriminant
    /// (`JoinKind as u8`; `JoinKind` has 3 fieldless variants, fits).
    pub fork_join_kind: u8,
    /// Explicit pad byte, must be `0`; free for a future `kind` value.
    pub _reserved: u8,
    /// `KIND_DELAY`: low 32 bits of `delay_ticks` (`SimTick`).
    /// `KIND_EDGE_LIST`/`KIND_FORK_JOIN`: `ArenaRange.base`.
    /// `KIND_REPEAT_EDGE`: low 32 bits of the repeat count (`u64`).
    pub a: u32,
    /// `KIND_DELAY`: high 32 bits of `delay_ticks`.
    /// `KIND_EDGE_LIST`/`KIND_FORK_JOIN`: `ArenaRange.count`.
    /// `KIND_REPEAT_EDGE`: high 32 bits of the repeat count.
    pub b: u32,
    /// Valid iff `kind == KIND_EDGE` or `kind == KIND_REPEAT_EDGE`: the
    /// `StateCellId` being waited on.
    pub edge_signal: u64,
}

const _: () = assert!(core::mem::size_of::<RescheduleReqLayoutProposal>() == 24);
const _: () = assert!(core::mem::align_of::<RescheduleReqLayoutProposal>() == 8);

/// The `kind` byte held a value outside `KIND_DELAY..=KIND_DONE` — a
/// programmer/ABI-mismatch error, never silently mapped to a default
/// description.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct InvalidKindByte(pub u8);

/// The decoded (typed) form of [`RescheduleReqLayoutProposal::kind`], used so
/// [`RescheduleReqLayoutProposal::describe`] can match exhaustively with no
/// wildcard arm — a 6th `Suspend`-shaped kind here would fail that match to
/// compile, the same guarantee `region_ordinal`'s exhaustive match on
/// `Region` gives (Task 1).
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum ProposalKind {
    Delay,
    Edge,
    EdgeList,
    ForkJoin,
    Done,
    /// Task 6 (CG-081/CG-171).
    RepeatEdge,
    WaitFork,
    /// Classes plan Task 13.
    ByRequest,
}

impl RescheduleReqLayoutProposal {
    /// Task 11 (A2-part2; found, not assigned -- see [`Self::to_bytes`]'s
    /// own doc comment for the full story of the bug this and that method
    /// together fix): every field is still explicitly set to a real value
    /// by each constructor below, deliberately in full (never relying on
    /// `..Default::default()`/a partial base) even though [`Self::to_bytes`]
    /// is what now actually determines the wire encoding -- keeping this
    /// struct's own field values fully real and inspectable (via `{:?}`,
    /// under a debugger, ...) independent of the byte-level encoding below.
    ///
    /// Delay payload: `Suspend::Delay(ticks)` (`ticks` split low/high into
    /// `a`/`b` — see the struct doc comment).
    pub fn from_delay(ticks: SimTick) -> Self {
        Self {
            kind: KIND_DELAY,
            edge_kind: 0,
            fork_join_kind: 0,
            _reserved: 0,
            a: ticks as u32,
            b: (ticks >> 32) as u32,
            edge_signal: 0,
        }
    }

    /// Single-edge-term payload: a `Suspend::WaitEvents` set with exactly one
    /// `EventTerm::Edge { cell, edge, .. }` term.
    pub fn from_single_edge(edge: EdgeKind, signal: StateCellId) -> Self {
        Self {
            kind: KIND_EDGE,
            edge_kind: edge as u8,
            fork_join_kind: 0,
            _reserved: 0,
            a: 0,
            b: 0,
            edge_signal: signal.0,
        }
    }

    /// **Task 7 (AB-096/CG-082) — `wait(expr)`'s guard-function ABI.**
    ///
    /// AB-096 asks for a documented mechanism by which a suspended
    /// `wait(expr)` process can expose a re-pollable condition to the
    /// scheduler "without the scheduler understanding SV expression
    /// semantics" (`docs/design/codegen.md` §9's `SvWakeSpec.dynamic_eval:
    /// Option<extern "C" fn(*const u8) -> bool>` sketch).
    ///
    /// **Deviation from the plan's literal "add a dedicated `guard_fn`
    /// struct field" text, keeping the exact specified round-trip
    /// behavior:** the frozen `RESCHEDULE_REQ_SIZE == 32` budget (only an
    /// `VJ_ABI_VERSION` bump may change it, not owned by this track) has
    /// no room for a NINTH, pointer-sized (8-byte) field. This struct
    /// already occupies 24 of those bytes, and `super::kernel_adapter`
    /// separately spends 4 more on `next_resume_pc` (every real suspend
    /// site needs it to resume at all) -- leaving a genuinely free 4 bytes,
    /// not 8 (an earlier draft of this task assumed a full 8 spare bytes
    /// without accounting for `next_resume_pc`'s own share; this is the
    /// corrected accounting, the same kind of self-correction this file's
    /// own `KIND_REPEAT_EDGE` doc comment already records for a prior
    /// task). This constructor instead reuses `a`/`b` -- unused (always
    /// `0`/`0`) for a plain, guard-free `KIND_EDGE` wait, exactly the
    /// "previously-free reserved bytes" this task is meant to spend -- to
    /// carry a real `extern "C" fn(SimHandle) -> bool` pointer's bit
    /// pattern, split low/high `u32` (the SAME `a`/`b` convention
    /// [`Self::from_delay`]/[`Self::from_repeat_edge`] already use for
    /// their own 64-bit payloads), instead of adding a field that would not
    /// fit. This keeps `size_of::<RescheduleReqLayoutProposal>()` at
    /// exactly 24 bytes -- genuinely UNCHANGED, not merely "still small
    /// enough" -- which is what
    /// [`tests::guard_fn_does_not_grow_the_proposal_past_24_bytes`] proves.
    /// A real (non-null) `extern "C" fn` pointer is never the all-zero bit
    /// pattern, so `a == 0 && b == 0` unambiguously means "no guard," no
    /// separate flag bit needed -- [`Self::guard_fn`] is the decode side.
    ///
    /// **`KIND_REPEAT_EDGE` is NOT extended with a guard in this pass**,
    /// despite the plan's own text naming it too ("a repeated event can
    /// also carry an `iff`, LRM-legal"): `KIND_REPEAT_EDGE`'s own `a`/`b`
    /// pair already carries the repeat count (Task 6, CG-081/CG-171) --
    /// there is no unused 8-byte slot left to reuse there, and inventing
    /// one would need either shrinking the count below the full `u64`
    /// range `Suspend::Repeat`'s own count field supports (a real behavior
    /// change, not a packing trick) or growing past the frozen budget. No
    /// fixture in this task's own list exercises `repeat(n) @(...) iff
    /// cond`, so this is a named, documented scope limit, not a silent gap.
    pub fn from_single_edge_with_guard(
        edge: EdgeKind,
        signal: StateCellId,
        guard_fn: extern "C" fn(SimHandle) -> bool,
    ) -> Self {
        let ptr = guard_fn as usize as u64;
        Self {
            kind: KIND_EDGE,
            edge_kind: edge as u8,
            fork_join_kind: 0,
            _reserved: 0,
            a: ptr as u32,
            b: (ptr >> 32) as u32,
            edge_signal: signal.0,
        }
    }

    /// The decode side of [`Self::from_single_edge_with_guard`]: `Some` iff
    /// `kind == KIND_EDGE` and `a`/`b` together encode a non-null pointer
    /// (a plain, guard-free [`Self::from_single_edge`] leaves both `0`).
    /// Deliberately scoped to `KIND_EDGE` only -- `a`/`b` mean something
    /// else entirely for every other kind (a delay tick count, an
    /// `ArenaRange`, a repeat count), so decoding them as a pointer for any
    /// kind but `KIND_EDGE` would silently misinterpret real payload bytes
    /// as a bogus function address.
    pub fn guard_fn(&self) -> Option<extern "C" fn(SimHandle) -> bool> {
        if self.kind != KIND_EDGE {
            return None;
        }
        let ptr = (u64::from(self.a)) | (u64::from(self.b) << 32);
        if ptr == 0 {
            return None;
        }
        // SAFETY: the only producer of a nonzero `a`/`b` pair under
        // `kind == KIND_EDGE` anywhere in this tree is
        // `Self::from_single_edge_with_guard`, which derives `ptr` from a
        // real `extern "C" fn(SimHandle) -> bool` value via an ordinary
        // fn-pointer-to-integer cast (`guard_fn as usize as u64`) -- the
        // exact inverse operation. `usize`/`u64` round-trip losslessly on
        // every platform this workspace targets (64-bit only, `rust-
        // toolchain.toml`), so `ptr as usize` is the original address bit-
        // for-bit, and transmuting a `usize` of a function-pointer's own
        // ABI-correct size/representation back to that exact function
        // pointer type is the documented, sound way to reconstruct it.
        Some(unsafe {
            core::mem::transmute::<usize, extern "C" fn(SimHandle) -> bool>(ptr as usize)
        })
    }

    /// **Task 6 (CG-081/CG-171):** repeat-edge payload -- a `Suspend::
    /// WaitEvents` set with exactly one `EventTerm::Repeat{count, inner:
    /// Edge{cell, edge, iff: None}}` term (`count` split low/high into `a`/
    /// `b`, same split `from_delay`'s own `delay_ticks` uses). See
    /// `KIND_REPEAT_EDGE`'s own doc comment for the full reasoning.
    pub fn from_repeat_edge(count: u64, edge: EdgeKind, signal: StateCellId) -> Self {
        Self {
            kind: KIND_REPEAT_EDGE,
            edge_kind: edge as u8,
            fork_join_kind: 0,
            _reserved: 0,
            a: count as u32,
            b: (count >> 32) as u32,
            edge_signal: signal.0,
        }
    }

    /// Multi-term payload: a `Suspend::WaitEvents` set with more than one
    /// term, referenced out-of-line via `range` (the elaboration-time
    /// edge-term table `ArenaRange`, same shape as
    /// `WaitSpecStorage::EdgeList`).
    pub fn from_edge_list(range: ArenaRange) -> Self {
        Self {
            kind: KIND_EDGE_LIST,
            edge_kind: 0,
            fork_join_kind: 0,
            _reserved: 0,
            a: range.base,
            b: range.count,
            edge_signal: 0,
        }
    }

    /// `Suspend::ForkJoin(join_kind, spawns)` payload: `spawns` referenced
    /// out-of-line via `range` (the dynamic fork-child table `ArenaRange`,
    /// same shape as `ForkFrame::children`).
    pub fn from_fork_join(join_kind: JoinKind, range: ArenaRange) -> Self {
        Self {
            kind: KIND_FORK_JOIN,
            edge_kind: 0,
            fork_join_kind: join_kind as u8,
            _reserved: 0,
            a: range.base,
            b: range.count,
            edge_signal: 0,
        }
    }

    /// `Suspend::Done` payload: none.
    pub fn done() -> Self {
        Self {
            kind: KIND_DONE,
            edge_kind: 0,
            fork_join_kind: 0,
            _reserved: 0,
            a: 0,
            b: 0,
            edge_signal: 0,
        }
    }

    /// `wait fork;` -- see [`KIND_WAIT_FORK`].
    pub fn wait_fork() -> Self {
        Self {
            kind: KIND_WAIT_FORK,
            ..Self::done()
        }
    }

    /// `p.await()` / `p.suspend()` -- see [`KIND_BY_REQUEST`].
    pub fn by_request() -> Self {
        Self {
            kind: KIND_BY_REQUEST,
            ..Self::done()
        }
    }

    /// Task 11 (A2-part2; found, not assigned): encodes `self` into the real
    /// `[u8; 24]` wire form field-by-field, at each field's own documented
    /// offset (never via `mem::transmute::<Self, [u8; 24]>`, which this
    /// method REPLACES -- see below for why). Bytes 12..16 -- the gap a
    /// `#[repr(C)]` layout of this struct leaves between `b: u32` (ending at
    /// offset 12) and `edge_signal: u64` (starting at offset 16, its own
    /// 8-byte alignment requirement per `align_of::<Self>() == 8`) -- are
    /// explicitly `0` here, never left to whatever byte happened to occupy
    /// that gap in `self`'s own in-memory representation.
    ///
    /// **Why this exists (a real bug this task found, not assigned, while
    /// exercising this crate's real lowering path through its own tests --
    /// full story, since the first fix attempt here was itself wrong):**
    /// `kernel_adapter::build_eval_result` used to build `self` via an
    /// ordinary `Self { kind: ..., a: ..., ... }` struct literal (every
    /// NAMED field set to a real value) and then `mem::transmute::<Self,
    /// [u8; 24]>` it directly. A struct literal only pins the NAMED
    /// fields' bytes, never the compiler-inserted padding between them --
    /// transmuting to a type with no padding of its own (`[u8; 24]`) then
    /// reads that gap as a real, concrete byte sequence, whatever
    /// indeterminate bit pattern happened to already be on the stack at
    /// that call site. `vj-codegen::process_lower` freezes the result
    /// directly into a compile-time LLVM IR constant (the literal byte
    /// array a `store [36 x i8] c"..."` emits), so two otherwise-identical
    /// compiles of the same design at different moments (different stack
    /// contents) could legitimately emit different IR text for a
    /// `KIND_DONE`/`KIND_DELAY`/`KIND_EDGE` suspend constant -- caught live
    /// by this task's own
    /// `parallel_lowering_produces_byte_identical_ir_to_sequential_lowering`
    /// test, which lowers the same design twice and found the two runs'
    /// otherwise-identical IR disagreeing at exactly this padding's byte
    /// offset.
    ///
    /// **First attempted fix (kept here as a documented dead end, not
    /// repeated): rebuilding `self` from a `mem::zeroed()` base via `Self {
    /// kind: ..., ..Self::zeroed() }` struct-update syntax** -- this does
    /// NOT work: Rust's struct-update syntax is defined field-by-field (each
    /// omitted field's VALUE is copied from the base), never as a whole-
    /// struct byte-level copy, so it carries no guarantee that the
    /// RESULT's own padding bytes come from the base's (zeroed) padding
    /// rather than being freshly indeterminate again at the new
    /// construction site -- verified live: this task's own test still
    /// observed differing bytes at the same offset after that fix, from
    /// two calls in the very same process. The only fix that actually
    /// closes the gap is to never transmute a padded struct to bytes at
    /// all -- this method, called from `build_eval_result` in place of the
    /// old `mem::transmute`, writes every one of the 24 output bytes
    /// itself (explicit `0` for bytes 12..16), so there is no padding left
    /// to be indeterminate.
    pub fn to_bytes(self) -> [u8; 24] {
        let mut out = [0u8; 24];
        out[0] = self.kind;
        out[1] = self.edge_kind;
        out[2] = self.fork_join_kind;
        out[3] = self._reserved;
        out[4..8].copy_from_slice(&self.a.to_le_bytes());
        out[8..12].copy_from_slice(&self.b.to_le_bytes());
        // out[12..16] intentionally left explicit `0`: the alignment gap
        // `#[repr(C)]` would otherwise leave between `b` and `edge_signal`
        // -- see this method's own doc comment.
        out[16..24].copy_from_slice(&self.edge_signal.to_le_bytes());
        out
    }

    /// Decode the raw `kind` byte to its typed form. `Err` (never a silent
    /// default) for any byte outside `KIND_DELAY..=KIND_DONE` — the loud,
    /// typed-error path a corrupted/future-versioned wire value must take
    /// instead of being read as some other kind by accident.
    fn kind_enum(&self) -> Result<ProposalKind, InvalidKindByte> {
        match self.kind {
            KIND_DELAY => Ok(ProposalKind::Delay),
            KIND_EDGE => Ok(ProposalKind::Edge),
            KIND_EDGE_LIST => Ok(ProposalKind::EdgeList),
            KIND_FORK_JOIN => Ok(ProposalKind::ForkJoin),
            KIND_DONE => Ok(ProposalKind::Done),
            KIND_REPEAT_EDGE => Ok(ProposalKind::RepeatEdge),
            KIND_WAIT_FORK => Ok(ProposalKind::WaitFork),
            KIND_BY_REQUEST => Ok(ProposalKind::ByRequest),
            other => Err(InvalidKindByte(other)),
        }
    }

    /// Exhaustive, no-wildcard description of the decoded `kind` — proves no
    /// `Suspend` variant this proposal represents is left undescribable by
    /// the layout. Returns [`InvalidKindByte`] (never panics/defaults) if
    /// `kind` is out of range.
    pub fn describe(&self) -> Result<&'static str, InvalidKindByte> {
        Ok(match self.kind_enum()? {
            ProposalKind::Delay => "Delay",
            ProposalKind::Edge => "Edge",
            ProposalKind::EdgeList => "EdgeList",
            ProposalKind::ForkJoin => "ForkJoin",
            ProposalKind::Done => "Done",
            ProposalKind::RepeatEdge => "RepeatEdge",
            ProposalKind::WaitFork => "WaitFork",
            ProposalKind::ByRequest => "ByRequest",
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::process::ArenaRange;
    use crate::sched::JoinKind;
    use crate::state::{EdgeKind, StateCellId};

    #[test]
    fn layout_proposal_is_repr_c_and_copy() {
        fn assert_copy<T: Copy>() {}
        assert_copy::<RescheduleReqLayoutProposal>();
        assert_eq!(core::mem::size_of::<RescheduleReqLayoutProposal>(), 24);
        assert_eq!(core::mem::align_of::<RescheduleReqLayoutProposal>(), 8);
    }

    #[test]
    fn layout_proposal_fits_the_frozen_reschedule_req_budget() {
        // SC-113, review fix — the actual bug this task's redesign fixes:
        // the original 56-byte draft would have failed this assertion
        // outright.
        assert!(
            core::mem::size_of::<RescheduleReqLayoutProposal>()
                <= vj_abi::eval::RESCHEDULE_REQ_SIZE
        );
    }

    #[test]
    fn layout_proposal_covers_every_suspend_variant() {
        // Suspend::Delay
        let delay = RescheduleReqLayoutProposal::from_delay(42);
        assert_eq!(delay.kind, KIND_DELAY);
        assert_eq!(delay.describe().unwrap(), "Delay");

        // Suspend::WaitEvents with a single Edge term
        let edge = RescheduleReqLayoutProposal::from_single_edge(EdgeKind::Posedge, StateCellId(7));
        assert_eq!(edge.kind, KIND_EDGE);
        assert_eq!(edge.edge_kind, EdgeKind::Posedge as u8);
        assert_eq!(edge.edge_signal, 7);
        assert_eq!(edge.describe().unwrap(), "Edge");

        // Suspend::WaitEvents with more than one term
        let edge_list =
            RescheduleReqLayoutProposal::from_edge_list(ArenaRange { base: 3, count: 5 });
        assert_eq!(edge_list.kind, KIND_EDGE_LIST);
        assert_eq!(edge_list.a, 3);
        assert_eq!(edge_list.b, 5);
        assert_eq!(edge_list.describe().unwrap(), "EdgeList");

        // Suspend::ForkJoin
        let fork_join = RescheduleReqLayoutProposal::from_fork_join(
            JoinKind::All,
            ArenaRange { base: 10, count: 2 },
        );
        assert_eq!(fork_join.kind, KIND_FORK_JOIN);
        assert_eq!(fork_join.fork_join_kind, JoinKind::All as u8);
        assert_eq!(fork_join.a, 10);
        assert_eq!(fork_join.b, 2);
        assert_eq!(fork_join.describe().unwrap(), "ForkJoin");

        // Suspend::Done
        let done = RescheduleReqLayoutProposal::done();
        assert_eq!(done.kind, KIND_DONE);
        assert_eq!(done.describe().unwrap(), "Done");

        // Task 6 (CG-081/CG-171): Suspend::WaitEvents with a single
        // Repeat{count, inner: Edge} term.
        let repeat_edge =
            RescheduleReqLayoutProposal::from_repeat_edge(3, EdgeKind::Posedge, StateCellId(9));
        assert_eq!(repeat_edge.kind, KIND_REPEAT_EDGE);
        assert_eq!(repeat_edge.edge_kind, EdgeKind::Posedge as u8);
        assert_eq!(repeat_edge.edge_signal, 9);
        assert_eq!(repeat_edge.a, 3);
        assert_eq!(repeat_edge.b, 0);
        assert_eq!(repeat_edge.describe().unwrap(), "RepeatEdge");

        // Suspend::WaitFork and Classes plan Task 13's Suspend::ByRequest:
        // distinct wire kinds, neither aliased to Done.
        let wait_fork = RescheduleReqLayoutProposal::wait_fork();
        assert_eq!(wait_fork.kind, KIND_WAIT_FORK);
        assert_eq!(wait_fork.describe().unwrap(), "WaitFork");
        let by_request = RescheduleReqLayoutProposal::by_request();
        assert_eq!(by_request.kind, KIND_BY_REQUEST);
        assert_ne!(by_request.kind, KIND_DONE);
        assert_ne!(by_request.kind, KIND_WAIT_FORK);
        assert_eq!(by_request.describe().unwrap(), "ByRequest");
    }

    /// Task 6: mirrors `delay_round_trips_through_a_b_split` for the repeat
    /// count -- proves the `a`/`b` split covers the same full `u64` range
    /// `Suspend::Delay`'s own tick count does.
    #[test]
    fn repeat_edge_count_round_trips_through_a_b_split() {
        let near_max: u64 = u64::MAX - 1;
        let proposal = RescheduleReqLayoutProposal::from_repeat_edge(
            near_max,
            EdgeKind::Negedge,
            StateCellId(1),
        );
        let reconstructed = ((proposal.b as u64) << 32) | (proposal.a as u64);
        assert_eq!(reconstructed, near_max);
    }

    /// A real, top-level `extern "C" fn` for the guard round-trip tests
    /// below -- never a closure (a closure has no stable, ABI-crossable
    /// function pointer of the `extern "C" fn(SimHandle) -> bool` shape).
    extern "C" fn test_guard_fn(_h: vj_abi::SimHandle) -> bool {
        true
    }

    /// Task 7 (AB-096): [`RescheduleReqLayoutProposal::from_single_edge_with_guard`]
    /// round-trips a REAL function pointer through `a`/`b` bit-for-bit, and
    /// a plain, guard-free [`RescheduleReqLayoutProposal::from_single_edge`]
    /// still decodes to `None` (never a spurious `Some` from stray a/b
    /// bytes).
    #[test]
    fn guard_fn_round_trips_through_a_b_split() {
        let with_guard = RescheduleReqLayoutProposal::from_single_edge_with_guard(
            EdgeKind::Posedge,
            StateCellId(3),
            test_guard_fn,
        );
        let decoded = with_guard
            .guard_fn()
            .expect("a guard-carrying KIND_EDGE proposal must decode Some");
        assert_eq!(
            decoded as *const () as usize, test_guard_fn as *const () as usize,
            "the decoded function pointer must be byte-identical to the original"
        );

        let without_guard =
            RescheduleReqLayoutProposal::from_single_edge(EdgeKind::Posedge, StateCellId(3));
        assert!(
            without_guard.guard_fn().is_none(),
            "a plain, guard-free KIND_EDGE proposal (a == b == 0) must decode to None"
        );
    }

    /// Task 7 (AB-096): `guard_fn()` must never misinterpret another kind's
    /// own `a`/`b` payload (a delay tick count, here) as a bogus function
    /// pointer -- scoped to `KIND_EDGE` only.
    #[test]
    fn guard_fn_is_none_for_every_kind_other_than_edge() {
        let delay = RescheduleReqLayoutProposal::from_delay(u64::MAX - 1);
        assert!(
            delay.guard_fn().is_none(),
            "KIND_DELAY's own a/b (a real, nonzero tick-count split) must never decode as a \
             guard function pointer"
        );
    }

    /// Task 7 (AB-096): the whole point of reusing `a`/`b` instead of
    /// adding a ninth field -- the struct's own `size_of` must stay
    /// EXACTLY what it was before this task, proving no ABI-budget
    /// violation was introduced.
    #[test]
    fn guard_fn_does_not_grow_the_proposal_past_24_bytes() {
        assert_eq!(
            core::mem::size_of::<RescheduleReqLayoutProposal>(),
            24,
            "adding guard_fn support must not grow RescheduleReqLayoutProposal -- it reuses \
             the existing a/b slot, it does not add a new field"
        );
    }

    #[test]
    fn describe_reports_a_typed_error_for_an_out_of_range_kind_byte() {
        let mut corrupt = RescheduleReqLayoutProposal::done();
        corrupt.kind = 200;
        assert_eq!(corrupt.describe(), Err(InvalidKindByte(200)));
    }

    #[test]
    fn delay_round_trips_through_a_b_split() {
        let near_max: SimTick = u64::MAX - 1;
        let proposal = RescheduleReqLayoutProposal::from_delay(near_max);
        let reconstructed = ((proposal.b as u64) << 32) | (proposal.a as u64);
        assert_eq!(reconstructed, near_max);
    }

    #[test]
    fn sc109_event_region_uses_the_abi_five_variant_set() {
        use vj_abi::Region;

        assert_eq!(core::mem::size_of::<Region>(), 4);

        let all = [
            Region::Active,
            Region::Inactive,
            Region::Nba,
            Region::Reactive,
            Region::Postponed,
        ];
        assert_eq!(all.len(), 5);
        for r in all {
            let ordinal = crate::sched::region_ordinal(r);
            assert!((0..=4).contains(&ordinal));
        }
        let mut ordinals: Vec<u32> = all
            .iter()
            .map(|&r| crate::sched::region_ordinal(r))
            .collect();
        ordinals.sort_unstable();
        assert_eq!(ordinals, vec![0, 1, 2, 3, 4], "no gap in the 5-variant set");
    }
}