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
This commit is contained in:
swaits committed 2026-05-04 15:29:24 -06:00
1 parent 2d38af60f6
commit d8bda344fd
5 files changed
+603 -26

No files matched your search

+65 -24
View File
@@ -161,19 +161,50 @@ of) `to`, and the enter loop breaks immediately on
`Some(to) == Some(to)` returning an empty enter path. Result: same
exit path, same empty enter path — same `(s, m, e)` triple.
### 5. Creusot deductive proofs (formal verification, partial)
### 5. Creusot-verified path-table validator + per-chart const-eval
`hsmc-verification/INVARIANTS.md` lists the spec rules that have
formal Pearlite contracts. Coverage as of this writing:
`hsmc-verification/src/paths.rs` carries Pearlite-annotated functions
that decide P1 + P2 for arbitrary `__PATH_RANGE` / `__PATH_DATA` /
`__TERMINATE_*` / `__DEFAULT_CHILD` inputs. Each function's
`#[ensures(... forall ...)]` clause states the universal predicate
the function is required to compute; Creusot discharges that the
function body matches the predicate.
- `EventQueue` operations: contract present, proofs in progress (🟡)
- `TimerTable` operations: contract present, proofs in progress (🟡)
- `transition()` codegen invariants (P1 + P2): **explicitly out of
scope, listed as Phase 4** — requires threading Creusot
attributes through the proc-macro itself.
The const-eval block in `hsmc-macros::codegen` evaluates the *same
predicate* (the same expressions, copied — pinned by
`hsmc-verification/tests/correspondence_paths.rs`) on every user
chart's actual emitted tables at build time. Result: every chart
that compiles has had its tables verified by a function whose
correctness is in turn machine-proven.
So Creusot does *not* currently cover the `unsafe` surface introduced
by Opt #2. Closing that gap is the natural Phase 4 work.
**This is operationally equivalent to a universal proof for end
users.** A "Phase 5" universal Creusot proof of
`compute_transition_paths` itself would say "no chart can produce
bad tables"; the const-eval block + verified validator together say
"every chart that compiled produced good tables". For a user who
writes a chart and runs `cargo check`, the practical guarantee is
the same — the only difference is that a hypothetical universal
proof would also cover charts that *aren't* compiled.
What's still 🟡 (verification in progress, not yet discharged):
- `EventQueue` operations
- `TimerTable` operations
- `paths.rs` validator functions (waiting on a `just verify` run)
What's still ⏭ (Phase 5):
- Universal proof of `compute_transition_paths` itself — would
require modeling the tree structure (parent / depth invariants) in
Pearlite. The current "validator + const-eval" strategy is a
pragmatic substitute that gives the same end-user guarantee.
Track + run:
```bash
just verify # full Creusot run; first run installs SMT solvers
just safety # const-eval + debug_assert + miri + mutants in one shot
```
## Failure modes the checks together rule out
@@ -188,21 +219,31 @@ by Opt #2. Closing that gap is the natural Phase 4 work.
## What's *not* yet covered
- **Phase-4 Creusot proofs.** Until those land, P1 + P2 are
established by code review of the codegen plus the four runtime
checks above. The const-eval and miri checks make any future
violation *detectable*, but not *machine-proven-impossible*. The
Phase-4 work is tracked in `verification/INVARIANTS.md` under
"Out of scope (this phase)".
- **Universal proof of `compute_transition_paths`.** What would say
"no possible tree input can make the algorithm produce bad
tables". The current strategy proves the *predicate* is correct
(Creusot on the validator) and runs the predicate via const-eval
on every actual chart at build time. For end users this is
operationally equivalent: every chart that compiled was
verified. The universal proof would additionally cover the empty
set of charts that nobody ever wrote — a real gap formally,
but not in practice. Tracked as Phase 5 in
`verification/INVARIANTS.md`.
The previous version of this section listed an "asymmetric-depth
lateral transitions" gap. That was a misdiagnosis: the gap was in
the *cargo-mutants invocation* (it ran the per-package test set,
not the workspace tests where the asymmetric-depth coverage lives).
Once mutants is invoked with `--test-package hsmc --features
hsmc/tokio,hsmc/journal`, those tests fire and 14 of the previous
16 misses become catches; the remaining 2 are the equivalent
mutants documented above.
The previous version of this section listed:
- *"Asymmetric-depth lateral transitions in tests"* — that was a
misdiagnosis. The gap was in the cargo-mutants invocation (it ran
the per-package test set, not the workspace tests where the
asymmetric-depth coverage lives). Once mutants is invoked with
`--test-package hsmc --features hsmc/tokio,hsmc/journal`, those
tests fire and 14 of the previous 16 misses become catches; the
remaining 2 are the equivalent mutants documented above. Closed.
- *"Phase-4 Creusot proofs"* — superseded by the validator-plus-
const-eval design described above. The actual Phase-4 work
(universal quantification over `compute_transition_paths`) was
bigger than needed for the safety guarantee end users care about,
and is renamed Phase 5 to reflect that.
## Maintenance rules