tla-export: bound message and step fields from their guards; Set::range; guarded disjunctions (tla_export step 4) - #53
Open
kiranandcode wants to merge 4 commits into
Open
kiranandcode wants to merge 4 commits into
kiranandcode wants to merge 4 commits into
Conversation
…tten oracle
Exporting toydb's src/raft/safety.rs and model-checking it at tla/Raft.cfg's
bounds turned up four gaps, each fixed generally:
- Set::range / range_inclusive (and FiniteRange::range_set) over integers
print as `lo..hi-1` / `lo..hi`; they were refused as an uninterpreted trait
method, which took inv_hosts, inv_lterms and inv_commits out of the .cfg.
- A binder guarded by `S.contains(Ctor { f: x, .. })` (nested constructors
too) ranges over `{m.f : m \in {m \in S : m.tag = "Ctor"}}`; it was a
Dom_<type> hole per binder, 44 of them in safety.rs's invariants.
- A disjunction of two state predicates outside what Init reaches prints as
`IF a THEN TRUE ELSE b`: TLC branches on `\/` in an action, so
`b == 0 || log[b - 1].term == bt` indexed log[0] and stopped TLC.
- `exists|step: Step| next_step(pre, post, step)` over a step enum whose
match arms each call one transition is one `\E` per variant, each field
bounded from that transition's guard (`0 <= i < pre.n`, `net.contains(..)`,
`b <= e <= h.log.len()`); fields only a type bounds keep the 2^10 cap.
safety.rs's 41 step-field holes become 3 (the payload of a proposed
command, the ghost ack map, and t_bump_term's new term), and TLC no longer
builds the union of every variant's values in every state (about 10x).
With these the export reproduces tla/Raft.tla's 597,764 distinct states and
diameter 18 with no conjunct violated. The new test checks a small model of
the same shapes with TLC (144 states, as a breadth-first search finds).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… range_inclusive holds hi
The per-variant expansion of `exists|step: Step|` read a variant's field
bounds from the first unguarded constructor arm naming the variant, even
when an earlier arm (guarded, a wildcard, an or-pattern) caught the variant
first or the arm refuted a field (`R { a: true, b }`). Values a later arm
admits were then dropped with no hole or refusal. Now only the first arm
that could match the variant is read, and only when it is unguarded, that
variant's constructor, and binds each field plainly or with `_`; otherwise
the fields fall back to their type or a hole.
`Set::range_inclusive(lo, hi)` printed as `lo..hi`, but vstd's is
`range_set(lo, hi).insert(hi)`, which holds `hi` even below `lo`: now
`(lo..hi \cup {hi})`.
Tests: the refutable, guarded and wildcard arms (9 states; 4 before), an
Init disjunction keeping its `\/` (2 initial states) beside range_inclusive,
and a constructor-field bound through a nested constructor, a tuple and a
map's keys, with an invariant TLC must find violated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ep-field domains never read the step The `IF a THEN TRUE ELSE b` rewrite of a state-predicate disjunction broke TLC assigning a primed variable through a helper's parameter: `pick(post.x)` with `pick(v) = v == 0 || v == 1` became `IF v = 0 ...` on an unassigned `x'`. A call passing a post-reading argument to a non-state parameter now goes to the operator's `_postarg` variant (the OpKey carries the bit), whose parameters read the post state, so its disjunctions keep `\/`; a parameter a reduced application (`bind_args`) binds to a post-reading value does too. The record variant stays one operator per recursive function and reads its parameters that way always, as main did. A step field's domain read off its arm's guard sits outside the `LET` that binds the step, but the step binder and the callee's scrutinee parameter were not unbound, so `b < size(step)` printed `0..size(step) - 1` there. They are now unbound, as is every local a destructuring or valueless `let` binds (its names printed raw before, possibly as a state variable). Tests: `tla_export_keeps_a_disjunction_over_the_post_state` (a helper given `post.x`, a `let` of `post.z` and a match on `post.o`: 9 states) and `tla_export_bounds_a_step_field_whose_guard_reads_the_step` (callee and inline match: 7 states). The toyDB export is byte for byte unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ads it A disjunction of that closure's applications keeps its \/ so TLC can assign through it, instead of becoming IF .. THEN TRUE ELSE .., which stopped TLC on an undefined primed variable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Step 4 of
plans/tla_export.md: export toyDB's Raft model (src/raft/safety.rs) and model-check it against the hand-written oracletla/Raft.tla. The comparison itself is in BasisResearch/toydb (tla/EXPORT.md, branchkg/raft-export). This PR holds the exporter fixes it needed. Each fix is general; nothing keys on toyDB.What the first export of safety.rs got wrong, and the fix
Set::<int>::range(0, n)refused (an uninterpretedFiniteRange::range_set)node_ids, soinv_hosts,inv_ltermsandinv_commitswere left out of the .cfgSet::range/range_inclusive/range_setover integers print aslo..hi-1/lo..hi; other element types are refusedforall|c, t, clog| net.contains(Msg::Campaign { c, term: t, clog }) ==> ..gave oneDom_<type>hole per binderS.contains(..)/M.contains_key(..)guard ranges over{m.f : m \in {m \in S : m.tag = "Ctor"}}b == 0 || (b <= h.log.len() && h.log[b - 1].term == bt)printed as\/\/in an action, evaluatesh.log[0]and stops (the oracle hand-wrote anIFfor exactly this)letor match binding), outside what Init reaches, prints asIF a THEN TRUE ELSE bexists|step: TStep| next_step(pre, post, step)bounded as the union of every variant's valuesexistsover a datatype that the body (or the function it passes the binder to) matches on is one\Eper variant. Each field is bounded from the guard of the transition its arm calls (0 <= i < pre.n,net.contains(Msg::X { .. }),b <= e <= h.log.len()), including the callee'slets that read only bound fields. A field that only its type bounds keeps the 2^10 cap and the split-into-holes rule ofbound_variantsAfter these fixes, safety.rs exports with 0 refusals and 3 holes: the payload of a proposed command, the ghost ack map
q, andt_bump_term's new term. None of the three is bounded anywhere in the model. With them filled fromtla/Raft.cfg's bounds, TLC finds 597,764 distinct states, diameter 18, no conjunct violated, the same as the oracle. The only difference is states generated (8,796,586 vs 7,324,630), which equals the oracle's own count once its hand-added action guards (its (A3)) are removed.Deviations from the plan
S.contains(x), map domains and ranges. Constructor-field membership and per-variant step expansion are additions. Without them TLC could not check the Raft export at the oracle's bounds (the unbounded holes multiply per state).IFrewrite is applied only outside the operators Init reaches, because in Init a\/may be what assigns the state. An operator Init and Next share keeps its\/, as before this PR.Test plan
tla_export_bounds_message_fields_and_step_fields_from_their_guards: a small model with a node range, message-field quantifiers, the guarded index disjunction and a step enum dispatched to transitions. It asserts no holes and no refusals, checks the printed shapes, and runs SANY plus TLC (144 distinct states, which an independent breadth-first search of the same system also gives).tla_export_caps_a_domain_read_off_a_typeupdated to the per-variant\Eshape. Its holes, and its TLC count of 22, are unchanged._(else its type or a hole);Set::range_inclusive(lo, hi)prints as(lo..hi \cup {hi}), as vstd holdshieven belowlo. New tests:tla_export_bounds_a_step_field_only_from_an_arm_taking_every_value(refutable, guarded and wildcard arms: 9 states, 4 before the fix),tla_export_keeps_a_disjunction_init_reaches(2 initial states, plus range_inclusive),tla_export_bounds_a_nested_tuple_and_map_key_constructor_field.pick(post.x)) goes to its_postargoperator, whose disjunctions keep\/so TLC can assign through them; parameters of reduced applications likewise; the record variant of a recursive function stays one operator and keeps\/, as on main. A step field's domain never reads the step binder, the callee's scrutinee or a destructuredletlocal. New tests:tla_export_keeps_a_disjunction_over_the_post_state(helper,letand match: 9 states),tla_export_bounds_a_step_field_whose_guard_reads_the_step(callee and inline match: 7 states).postreads the post state, so a disjunction of its applications keeps\/. Before this fix it became anIFand TLC stopped on an undefinedx'. New testtla_export_keeps_a_disjunction_over_a_closure_reading_the_post_state(7 states; it fails without the fix).tla_exportsuite withTLA2TOOLS_JARset: 69 passed. toyDB's export regenerates byte for byte.cargo fmt -- --check;cargo clippy -p vir -p rust_verify_test --all-targets -- -D warnings.tla/export/export.shregenerates the committed export byte for byte; TLC runs of the main,deep_lcand witness configurations, plus the per-conjunct CTI probes (see toydbtla/EXPORT.md).PRs #50 (model tools) and #51 (liveness) also edit
vir/src/tla.rs; expect textual conflicts inquant/Exporterfields when they merge.🤖 Generated with Claude Code