feat(modelchecker): next_states() builtin for one-step ENABLED assert…#365
Merged
Conversation
…ions
Adds one-step lookahead to always assertions: next_states() returns the
list of successor transitions from the state under check. Each entry is
a struct:
transition(name, kind, role_ref, role_id, action_name, state)
- name: flat label ("Customer#0.PlaceOrder", "thread-0",
"channel-0-message-1")
- kind: "action" | "thread" | "channel"
- role_ref: role instance label ("Customer#0"), None for non-role
transitions
- role_id: the role's identity value (same as __id__) — compares by
VALUE, so n.role_id == w.__id__ scopes a check to a role instance.
Role objects themselves compare by pointer and never match across
clones, so the Role object is deliberately not exposed.
- action_name: bare action name. Named action_name/role_ref (not
action/role) because those are fizz keywords and cannot appear in
attribute position.
- state: successor heap vars at the first yield point — same struct
shape as before/after in transition assertions.
This expresses TLA+ ENABLED-style safety properties with arbitrary
quantifier nesting — e.g. "every item in the trash is restorable":
always assertion EveryTrashItemRestorable:
for k in items:
if items[k] == "trash":
if len([n for n in next_states() if n.state.items[k] == "active"]) == 0:
return False
return True
The forall-item-exists-successor shape is not expressible with
transition assertions, and a global-reachability `exists` formulation
cannot catch ordering-restricted designs (stack-like trash where only
the most recent deletion is restorable) — pinned by the 09-07 example.
Semantics:
- The probe (probeNextStates) IGNORES exploration bounds — max_actions
depth, max_concurrent_actions, per-action caps. Enabledness is a
property of the state, not the exploration schedule; without this,
assertions would false-fail at the depth frontier and at states
where in-flight non-atomic actions exhaust the concurrency cap
(pinned by 09-08).
- Probe forks are throwaway: never attached, queued, or
invariant-checked (no recursion). lib-level allocation counters
(role refs, channel ids) are snapshotted/restored so discarded probe
execution cannot shift ref numbering of real exploration.
- Crash variants are excluded from the successor set.
- Non-atomic actions: successor is the FIRST yield point; use name
filtering for "can this serial action start".
- Intermediate-fork dedup happens AFTER Execute (mirrors processNode):
sibling oneof forks share the parent's state until the choice
binding runs; deduping at push time would collapse them (pinned by
09-06).
- Lazy + memoized; zero cost for specs that don't call it. Verified
BFS, DFS, --experimental_processed_queue, --experimental_no_graph,
and simulation. Unavailable in composition joins.
Also exposes id components to specs via new attrs on ModelValue
(inherited by SymmetricValue): id.name -> "Worker", id.index -> 0.
Previously the only option was parsing str(id) ("Worker0").
Docs: LANGUAGE_REFERENCE next_states() section; GOTCHAS #14 (role
objects compare by pointer across states — compare __id__ values;
also applies to transition assertions' before/after) and #15 (keywords
cannot be attribute names; multi-line comprehensions do not parse).
Reference examples: 09-06 (unrestricted restore, PASSES), 09-07 (stack
trash, FAILS with assertion name), 09-08 (concurrency-cap false
positive guard), 09-09 (role_id discrimination + id attrs).
Co-Authored-By: Claude Fable 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.
…ions
Adds one-step lookahead to always assertions: next_states() returns the list of successor transitions from the state under check. Each entry is a struct:
This expresses TLA+ ENABLED-style safety properties with arbitrary quantifier nesting — e.g. "every item in the trash is restorable":
The forall-item-exists-successor shape is not expressible with transition assertions, and a global-reachability
existsformulation cannot catch ordering-restricted designs (stack-like trash where only the most recent deletion is restorable) — pinned by the 09-07 example.Semantics:
Also exposes id components to specs via new attrs on ModelValue (inherited by SymmetricValue): id.name -> "Worker", id.index -> 0. Previously the only option was parsing str(id) ("Worker0").
Docs: LANGUAGE_REFERENCE next_states() section; GOTCHAS #14 (role objects compare by pointer across states — compare id values; also applies to transition assertions' before/after) and #15 (keywords cannot be attribute names; multi-line comprehensions do not parse).
Reference examples: 09-06 (unrestricted restore, PASSES), 09-07 (stack trash, FAILS with assertion name), 09-08 (concurrency-cap false positive guard), 09-09 (role_id discrimination + id attrs).