vj-sim 0.2.0

Netlist, scheduler and process model of the vj SystemVerilog simulator.
Documentation
//! DETERMINISM::TIEBREAK (Task 20, ledger DT-017/DT-027/DT-028) — the one
//! fixed, documented tiebreak for same-`(time, region)` committed effects
//! from different processes, published here so `vj-harness`'s Gate 1
//! (`CommittedEffectSink`) has one named symbol to call/cite instead of
//! re-deriving the rule from reading `sched`/`determinism` source directly.
//!
//! **The policy (DT-017):** the total order is exactly
//! [`CanonicalEventKey`]'s already-derived `Ord` (Task 2/DT-010) — field
//! order `vtime, region_ordinal, reconv_iter, canonical_process_id,
//! local_send_seq`. This function does not reimplement that comparison; it
//! delegates to it, so there is exactly one comparison algorithm for
//! `CanonicalEventKey` values in the crate, never two that could silently
//! drift apart (the same "duplicate pure-function eval diverges" risk this
//! codebase has hit before). `local_send_seq` — assigned once, at
//! effect-creation time, by the controller that produced the key (Task 3's
//! `SequentialController::next_seq`, and any future WOCB controller the same
//! way) — is what makes the order total even when two effects share every
//! other field: two processes at the same `(time, region, reconv_iter)` are
//! already separated by `canonical_process_id`; two effects from the *same*
//! process at the same `(time, region, reconv_iter)` are separated by
//! `local_send_seq`.
//!
//! **DT-027 — Gate 1's hard dependency:** Gate 1 (`vj-harness`'s
//! thread-count-sweep bit-diff gate) is not proceeded around while this rule
//! is unpublished. This module's existence, its `pub` visibility, and its
//! passing tests below ARE the "ENGINE has published and documented its
//! fixed tiebreak rule" precondition DT-027 requires before Gate-1
//! implementation work may proceed — recorded as a hard, checked dependency,
//! not an informal aside.
//!
//! **DT-028 — the recorded CI policy decision:** the full `{1, 4, 8, 16,
//! 31}`-thread-count blake3 bit-diff sweep (SC-013's literal acceptance) is
//! **nightly-only** — it needs a real multi-threaded `DeterminismController`,
//! which does not exist this wave (`SequentialController` is the only
//! implementation shipped, `determinism::sequential::tests::
//! dt006_no_thread_spawn_or_rollback_code_outside_determinism_module` proves
//! it never spawns a thread). A cheap **Tier-A subset is PR-blocking**:
//! `{1, 4}` thread counts only, made up of two checks this crate already
//! ships — `sched::resume::tests::
//! resume_is_never_called_concurrently_for_the_same_process_id` (Task 12's
//! real 8-OS-thread `ResumeDriver` reentrancy stress test, the seam any
//! future multi-threaded controller dispatches through) plus a 2-controller
//! `committed_effect_tiebreak` ordering check (this module's own
//! `tiebreak_is_a_total_order_matching_canonical_event_key_ord` and
//! `tests/region_order_fixture.rs`'s `sequential_and_levelized_controllers_
//! converge_to_the_same_canonical_effect_order`, Task 22). This is a decision
//! *recorded* here because DT-028 asks for an explicit recorded policy, not
//! a default assumed silently; the actual CI YAML wiring the Tier-A subset
//! into PR checks and the full sweep into a nightly job is
//! `vj-harness`'s/the repo's own CI configuration, a different track,
//! out of this crate.

use crate::determinism::CanonicalEventKey;

/// The one fixed, documented tiebreak for same-`(time, region)` committed
/// effects (DT-017). Delegates to [`CanonicalEventKey`]'s derived `Ord`
/// (Task 2/DT-010's total order) rather than reimplementing comparison —
/// see this module's doc comment for why that matters.
pub fn committed_effect_tiebreak(
    a: &CanonicalEventKey,
    b: &CanonicalEventKey,
) -> std::cmp::Ordering {
    a.cmp(b)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::sched::SchedTime;
    use std::cmp::Ordering;

    fn key(
        vtime: u64,
        region_ordinal: u32,
        reconv_iter: u32,
        canonical_process_id: u32,
        local_send_seq: u64,
    ) -> CanonicalEventKey {
        CanonicalEventKey {
            vtime: SchedTime { ticks: vtime },
            region_ordinal,
            reconv_iter,
            canonical_process_id,
            local_send_seq,
        }
    }

    /// DT-017 — `committed_effect_tiebreak` must delegate to
    /// `CanonicalEventKey`'s own derived `Ord`, never reimplement comparison
    /// (drift risk: two physical implementations of the same ordering rule
    /// can silently diverge). A fixed table of pairs covers every
    /// field-precedence case so the test itself is deterministic (no RNG).
    #[test]
    fn tiebreak_is_a_total_order_matching_canonical_event_key_ord() {
        let pairs = [
            // differ only in vtime
            (key(1, 0, 0, 0, 0), key(2, 0, 0, 0, 0)),
            // same vtime, differ in region_ordinal
            (key(5, 0, 0, 0, 0), key(5, 1, 0, 0, 0)),
            // same vtime+region, differ in reconv_iter
            (key(5, 2, 0, 0, 0), key(5, 2, 1, 0, 0)),
            // same vtime+region+reconv_iter, differ in canonical_process_id
            (key(5, 2, 3, 2, 0), key(5, 2, 3, 5, 0)),
            // same everything but local_send_seq
            (key(5, 2, 3, 5, 0), key(5, 2, 3, 5, 1)),
            // exactly equal
            (key(9, 9, 9, 9, 9), key(9, 9, 9, 9, 9)),
            // reverse of an earlier case
            (key(2, 0, 0, 0, 0), key(1, 0, 0, 0, 0)),
        ];
        for (a, b) in pairs {
            assert_eq!(
                committed_effect_tiebreak(&a, &b),
                a.cmp(&b),
                "committed_effect_tiebreak({a:?}, {b:?}) must equal a.cmp(&b) -- \
                 it must delegate to CanonicalEventKey's derived Ord, not \
                 reimplement comparison"
            );
        }
    }

    /// DT-017's literal acceptance: "two processes writing in the same
    /// time/region with no data dependency always produce the same relative
    /// CommittedEffect order across repeated runs and thread counts". This
    /// crate cannot itself run multiple thread counts (Task 20's DT-028 doc
    /// comment above records why), so the real, checkable half here is that
    /// `committed_effect_tiebreak` is a pure function of its two by-value
    /// arguments with no interior-mutable/thread-local state it could read
    /// -- a `fn(&CanonicalEventKey, &CanonicalEventKey) -> Ordering` with no
    /// `&mut self`/global state cannot vary its answer by call count, thread,
    /// or wall-clock time by construction. The 100-repeat loop below is the
    /// executable half of that argument: same two keys, differing only in
    /// `canonical_process_id` (5 vs 2, so genuinely different processes with
    /// no dependency between them), same answer every time.
    #[test]
    fn same_time_region_different_process_no_dependency_has_a_fixed_relative_order_repeatable_across_runs()
     {
        let a = key(10, 1, 0, 5, 0);
        let b = key(10, 1, 0, 2, 0);
        let first = committed_effect_tiebreak(&a, &b);
        assert_ne!(
            first,
            Ordering::Equal,
            "the two keys differ in canonical_process_id and must not compare equal"
        );
        for _ in 0..100 {
            assert_eq!(
                committed_effect_tiebreak(&a, &b),
                first,
                "committed_effect_tiebreak must return the identical Ordering on \
                 every repeated call for the same pair of keys"
            );
            // also prove antisymmetry holds on every repeat, not just once
            assert_eq!(committed_effect_tiebreak(&b, &a), first.reverse());
        }
    }

    /// DT-027 — Gate 1 must not be considered satisfiable, and Gate-1
    /// implementation work must not proceed, until ENGINE has published and
    /// documented a fixed tiebreak rule. There is no "Gate 1 implementation"
    /// inside this crate to assert against (that is `vj-harness`'s own
    /// track); what this test actually checks is the hard, mechanical
    /// precondition DT-027 pins down: `committed_effect_tiebreak` exists,
    /// is `pub` (so it is reachable from `vj-harness` once that crate
    /// depends on `vj-sim`, which `docs/CONTRACTS.md` already records
    /// it does), and is callable with no side effects or hidden state. A
    /// future revision that makes this function private, deletes it, or
    /// requires taking an engine/controller instance to call would silently
    /// re-break the "published, harness-citable" precondition DT-027
    /// requires -- this test fails loudly if that happens, because the
    /// two calls below would no longer compile/pass unchanged.
    #[test]
    fn gate1_blocked_until_tiebreak_is_published_is_satisfied_by_this_module_existing() {
        // Reachable with the crate's own `pub use` path, exactly how
        // `vj-harness` would call it once it depends on `vj-sim`.
        let a = key(1, 0, 0, 0, 0);
        let b = key(1, 0, 0, 1, 0);
        let ordering: Ordering = crate::determinism::committed_effect_tiebreak(&a, &b);
        assert_eq!(
            ordering,
            Ordering::Less,
            "sanity: process 0 orders before process 1 at an otherwise-identical key"
        );
    }
}