Files
hsmc/verification/src/lib.rs
T
swaits d8bda344fd safety(verify): Creusot-verified validator for the path-table safety predicates
Closes the formal-verification side of the unsafe surface. The
const-eval block I added to hsmc-macros (commit nuux) catches any
chart whose emitted tables violate P1 + P2 at user `cargo check`
time — but until now its check predicate was a hand-written
expression with no machine-checked link to "P1 + P2 hold for the
unsafe blocks that follow".

This commit adds Pearlite-annotated functions in
verification/src/paths.rs that decide P1 + P2 for arbitrary
__PATH_RANGE / __PATH_DATA / __TERMINATE_* / __DEFAULT_CHILD inputs.
Each function carries `#[ensures(... forall ...)]` stating the
universal predicate it computes; Creusot discharges that the body
matches. The const-eval block evaluates the *same expressions* on
each chart's actual emitted tables — pinned by
correspondence_paths.rs::the_const_eval_block_predicate_matches_the_validator
which compares both directly on representative and adversarial
inputs.

End-to-end safety chain:

  - hsmc-macros emits __PATH_RANGE / etc. for every chart.
  - Const-eval re-runs the predicate on the chart's actual tables
    at build time. Build fails on any violation.
  - The validator's predicate is in turn machine-proven (Creusot
    `forall<i: Int>`) to match P1 + P2.

Net: every chart that compiles has had its tables verified by a
function whose correctness is formally proven. Operationally
equivalent to a universal "no chart can fail" proof for end users
— every chart they compile is verified.

Files:
- verification/src/paths.rs (new): five validators
  (check_path_range_invariant, check_terminate_range_invariant,
  check_state_ids_in_bounds, check_default_child_in_bounds,
  check_path_tables_sound) with full Pearlite contracts. 10 unit
  tests pin executable behavior.
- verification/tests/correspondence_paths.rs (new): 9 tests
  including the predicate-equality test above.
- verification/INVARIANTS.md: new "Path tables" section with five
  spec rows + aggregate. The previous "Phase 4: transition() codegen
  invariants" out-of-scope item is reclassified Phase 5 (universal
  proof of compute_transition_paths itself) — bigger than needed
  for the end-user safety guarantee.
- docs/004: rewrites the "Creusot deductive proofs" check with the
  new architecture and explains why per-instance verification at
  `cargo check` is operationally equivalent to universal proof.

Run:
  cargo test -p hsmc-verification    # 10 unit + 9 correspondence
  just verify                        # discharge Creusot VCs
  just safety                        # full unsafe audit in one shot
2026-05-04 15:29:24 -06:00

49 lines
2.1 KiB
Rust

//! Deductive proofs for the `hsmc` runtime data structures.
//!
//! This crate hosts Creusot/Pearlite contracts on simplified mirrors of
//! `hsmc::__private::EventQueue` and `hsmc::__private::TimerTable`. The
//! mirrors are byte-for-byte equivalent to the runtime types' observable
//! behavior; the difference is that the mirrors carry full
//! `#[requires] / #[ensures]` annotations, so a Creusot run discharges
//! VCs (verification conditions) for them.
//!
//! ## Why mirror, not annotate hsmc directly?
//!
//! Two reasons:
//!
//! 1. **Cleanliness.** `hsmc` itself stays free of `creusot-contracts`
//! — production users don't have to think about a verifier dep.
//! 2. **Tractability.** The runtime types are layered on `heapless`
//! types whose Pearlite models would have to be written first.
//! A simplified mirror with `&mut [_; CAP]` storage avoids the
//! indirection and makes the proofs readable.
//!
//! The correspondence is enforced by `tests/correspondence.rs`, which
//! drives both the mirror AND the real `hsmc` runtime type with the same
//! operation sequences and asserts equal observable results. If the
//! mirror drifts away from the runtime, the correspondence test fails
//! before the proofs become wrong.
//!
//! ## Status
//!
//! See `INVARIANTS.md` for the spec-rule → proof mapping. Initial focus
//! (in priority order):
//!
//! 1. `EventQueue` — FIFO; bounded; pop-after-push returns the pushed
//! value. (S6 in the semantics doc.)
//! 2. `TimerTable::start` — at most one entry per `(state, trigger)`
//! pair; capacity respected. (S5.1)
//! 3. `TimerTable::pop_expired` — deepest expired entry first; ties
//! broken by insertion index; entry removed; `None` iff none expired.
//! (S5.2)
//! 4. (later) `transition()` path validity — codegen-emitted; needs
//! macro changes to thread Creusot attributes through.
//! 5. (later) `step()` end-to-end determinism.
// Verification is a dev-time concern — no need for no_std here. We
// use `std::vec::Vec` (via creusot-std's specs) directly.
pub mod event_queue;
pub mod paths;
pub mod timer_table;