fix(tla): EnvironmentController explores fully — leading guard-disjunction split + instance primed-arg staging - #188
Merged
zoratu merged 1 commit intoJul 19, 2026
Conversation
…ction split + instance primed-arg staging EnvironmentController (Chandra-Toueg detector) explored only 36 distinct states vs TLC's ~129,630 after the panic was fixed in #187. Root cause was `localClock` never advancing: the ProcTick RECEIVE branch's `Detector!Receive(i, inDelivery')` produced no successor, so its nested `LocallyTick(i)` never ran, so every guard requiring `localClock > 0` (message send/predict) stayed disabled and the whole message-passing state space (the bulk of TLC's 130k) went unexplored. Two independent bugs, both needed: 1. Leading guard-disjunction with shared conjuncts. `split_action_body_disjuncts` split `/\ (\/ a \/ b) /\ LocallyTick(i) /\ UNCHANGED ...` into `[/\ a, /\ b /\ LocallyTick /\ UNCHANGED]` — it stripped the leading `/\` and split at the guard's `\/`, absorbing the shared trailing conjuncts into only the LAST `\/` branch. The first guard-disjunct then fired WITHOUT the shared `LocallyTick`/primed conjuncts, so the clock never advanced. The `/\`+`\/` strip now only fires when the disjunction is the body's sole top-level conjunct (`/\ \/ b1 \/ b2 \/ b3`, e.g. 2PC `RS`), detected by layout via the new `leading_conjunct_is_sole` (count `/\` lines shallower than the `\/` bullets — robust to `expr.trim()` dedenting the first line, unlike a `split_action_body_clauses` count, which over-counts a lone disjunction-conjunct). 2. Instance-action arguments that read a next-state variable. `expand_action_call_multi` value-bound call arguments against the bare `ctx`, so `Detector!Receive(i, inDelivery')` (with `inDelivery'` staged by the preceding `CommChan!Deliver(i)`) read `inDelivery'` as an unbound ModelValue; Receive's `\E m \in incomingMessages` then errored and it produced no successor. Evaluate arguments in the caller's staged context (a superset — unprimed arguments are unaffected). With both fixes EnvironmentController explores the full ~130k-state space (36 -> 116k+ and matching TLC's scale; it still hits the pre-existing pathological slow-tail near the end, tracked separately). Gate: diff_tlc 19/19 (adds ClockDetectorMC, a self-contained instance-action spec needing both fixes, 9 == TLC), cargo test --release 1486 passed / 0 failed. New unit test pins the RS-vs-Receive split distinction.
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.
After #187 fixed the panic, EnvironmentController (Chandra-Toueg detector,
specifications/detector_chan96) still explored only 36 distinct states vs TLC's ~129,630. Root cause:localClocknever advanced — theProcTickRECEIVE branch'sDetector!Receive(i, inDelivery')produced no successor, so its nestedLocallyTick(i)never ran, so every guard requiringlocalClock > 0(message send / predict) stayed disabled and the whole message-passing state space (the bulk of TLC's 130k) went unexplored. Two independent bugs, both needed — each isolated to a deterministic repro.1. Leading guard-disjunction with shared conjuncts
split_action_body_disjunctssplitinto
[/\ %P=0 /\ %S=0, /\ %P#0 /\ %S#0 /\ LocallyTick /\ UNCHANGED]— it stripped the leading/\and split at the guard's\/, absorbing the shared trailing conjuncts (LocallyTick,UNCHANGED) into only the last\/branch. The first guard-disjunct then fired without them, so the clock never advanced.The
/\+\/strip now only fires when the disjunction is the body's sole top-level conjunct (/\ \/ b1 \/ b2 \/ b3, e.g. 2PCRS— which must still split into 3). This is detected by layout via the newleading_conjunct_is_sole: count/\lines shallower than the\/bullets. Asplit_action_body_clausescount won't work — it over-counts a lone disjunction-conjunct (flattens the branches' inner/\, returning 6 forRS), andexpr.trim()dedents the first line so an absolute per-line column won't line up either; comparing against the\/bullet indent is robust to both.2. Instance-action arguments that read a next-state variable
expand_action_call_multivalue-bound call arguments against the barectx, soDetector!Receive(i, inDelivery')— withinDelivery'staged by the precedingCommChan!Deliver(i)— readinDelivery'as an unboundModelValue. Receive's\E m \in incomingMessagesthen errored and it produced no successor. Arguments now evaluate in the caller's staged context (a superset — unprimed arguments are unaffected).Result
With both fixes EnvironmentController explores the full state space: 36 → 116k+ distinct and matching TLC's ~129,630 scale. (It still hits a pre-existing pathological slow-tail near the very end — a separate perf issue, tracked with the ACP over-generation.)
Validation
scripts/diff_tlc.sh: 19/19 — addsClockDetectorMC, a self-contained instance-action spec that needs both fixes (a primed-argchan'[i]read + a callee body with a leading guard-disjunction), 9 distinct == TLC.cargo test --release: 1486 passed / 0 failed.split_action_body_disjuncts_keeps_leading_guard_disjunction_with_shared_conjunctspins the Receive-vs-RSsplit distinction (and the pre-existingRStest still passes).