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
The earlier safety-audit run reported 16/53 missed mutants in
compute_transition_paths and concluded the LCA depth-walk wasn't
covered by tests. Wrong diagnosis. cargo-mutants infers the
test-package from the file under mutation, so without an explicit
override it ran only the hsmc-macros per-package test set
(essentially trybuild fixtures). All the asymmetric-depth lateral
coverage in det_lateral.rs et al. is in the hsmc/ workspace tests
and was sitting on the bench.
With `--test-package hsmc --features hsmc/tokio,hsmc/journal`:
codegen.rs safety fns: 51/53 (14 caught + 37 unviable + 2 missed)
hsmc/src/lib.rs: 48/48 (45 caught + 3 unviable + 0 missed)
total: 99/101 = 98%
The 2 remaining are equivalent mutants — transformations that
provably produce identical __PATH_RANGE / __PATH_DATA output and
so cannot be distinguished by any behavior test:
1. `da > db` → `da >= db` in the LCA depth-equalizer:
the mutated loop walks `a` one extra step in the equality case;
the next loop (`while db > da`) compensates symmetrically.
parent(root) = None short-circuits any walk past root. Same
LCA, same paths. Verified by hand on lateral, up-transition,
and asymmetric-depth (4↔2) inputs.
2. `from != to && is_strict_ancestor(...)` → `from == to && ...`:
becomes always-false (a state isn't its own strict ancestor),
so the up-transition branch is dead code. Up-transitions then
fall through to the lateral branch, which computes
lca(from, to) = to (target is ancestor of source), walks
source up to (exclusive of) to, and produces an empty enter
path. Same paths.
Both are documented in docs/004 § "The two equivalent mutants".
Updates:
- justfile `just safety`: add the correct flags so future runs
reproduce 99/101 instead of the misleading 37/53.
- docs/004: corrected mutants section, equivalent-mutant analysis,
removed the false "asymmetric-depth test gap" claim.
After Opt #2 added six bounded `get_unchecked` blocks per chart, the
project's two "no unsafe" claims (README §"At a glance" + spec §11
rule 12) were stale, and there was no single place answering "how
do I know the unsafe is correct?".
This commit closes both gaps:
- New: docs/004. unsafe-safety-contract.md
Inventories every unsafe site, names the predicate, names what
establishes it (codegen-time guarantee), and lists the five
layered checks (const-eval, debug_assert, miri, mutants, Creusot)
with current status. Documents what's *not* yet covered (the
16-of-53 missed mutants in the LCA depth walk are behavior, not
safety; transition() codegen invariants are still Phase-4 for
Creusot). Includes a maintenance checklist for future unsafe.
- README points at the doc and gives an honest one-paragraph
summary instead of "no unsafe".
- Spec rule 12 rewritten from "no unsafe in generated code" to
"bounded unsafe in generated code, none in user code" with a
pointer to the audit.
- justfile gains `just safety` — runs const-eval (cargo check),
debug_assert tests, miri, and mutants in one shot. Mirrors the
doc's audit matrix so the doc never goes stale silently.