fix(tla): stage primed arguments in the compiled + execute_branch action paths - #185
Merged
Conversation
…ion paths An action operator called with a primed variable as an *argument* — e.g. Nano's `CalculateHash(block, lastHash, lastHash')` where the override `CalculateHashImpl(blk, oldL, newL) == ... /\ newL = HashOf(blk) /\ ...` binds `newL` to `lastHash'` — is an action *by virtue of the call site*: the body clause `newL = h` is really the primed assignment `lastHash' = h`. Expressing that requires substituting the argument text into the body (macro semantics), so `newL` becomes the literal `lastHash'`. Two of the three action evaluators got this wrong. `eval_action_body_multi` (eval/action.rs) already substituted primed args, but: - `try_eval_compiled_guard_as_action` (compiled_eval.rs) only substituted when the body had *no* direct prime — Nano's `CalculateHashImpl` also stages `hashFunction'` directly, so it was classified transitively-action and took the value-binding path instead, binding `newLastHash` to the unstaged value of `lastHash'`. `newLastHash = h` then evaluated as a boolean guard, `lastHash'` was never staged, and a later conjunct reading it (`![node][lastHash']`) indexed with a bogus value — the action never fired. - `execute_branch` (action_exec.rs) substituted only when the body *primed a parameter* (`Lose(q) == q' = ...`), never when an *argument* was primed, with the same value-binding failure. Both now substitute the argument text whenever any argument is primed, matching the interpreted path and making all three action evaluators consistent. `arg_is_primed_expr` is lifted to `pub(crate)` and shared. Symptom: silent under-exploration (a false-safe soundness bug) for the "operator staging a prime through a primed argument, read by a later conjunct" shape — the memory-interface `Send(p,d,mem,mem')` pattern and Nano's hash-assignment pattern. Regression: `PrimedArgActionStaging` in the diff_tlc gate (CHOOSE-free so state counts pin exactly; pre-fix froze at 1 state / no violation, now 4 == TLC v2.19 with the reachable violation). Found while investigating the NanoBlockchain MCNanoSmall under-exploration; that spec has a separate, still-open residual (see the corpus-rebaseline log).
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.
Problem
An action operator called with a primed variable as an argument is an action by virtue of the call site. The canonical shape is NanoBlockchain's constant-operator override:
Here
newLaliaseslastHash', so the body clausenewL = his really the primed assignmentlastHash' = h. Expressing that needs textual argument substitution (macro semantics), turningnewLinto the literallastHash'.The interpreted
eval_action_body_multialready did this. The other two action evaluators did not:try_eval_compiled_guard_as_actionsubstituted only when the body had no direct prime.CalculateHashImplalso stageshashFunction'directly, so it was classified transitively-action and took the value-binding path — bindingnewLastHashto the unstaged value oflastHash'.newLastHash = hthen evaluated as a boolean guard,lastHash'was never staged, and the later![node][lastHash']indexed a bogus value → the action silently produced no successor.execute_branchsubstituted only when the body primed a parameter (Lose(q) == q' = ...), never when an argument was primed — same value-binding failure.Symptom: silent under-exploration (a false-safe soundness bug) for the "operator staging a prime through a primed argument, read by a later conjunct" shape — the Specifying-Systems
Send(p,d,mem,mem')memory-interface pattern and Nano's hash-assignment pattern.Fix
Both paths now substitute the argument text whenever any argument is primed, matching the interpreted path — all three action evaluators are now consistent.
arg_is_primed_expris lifted topub(crate)and shared.Validation (on spot)
PrimedArgActionStaging(CHOOSE-free so state counts pin exactly): pre-fix froze at 1 state / no violation; post-fix 4 states == TLC v2.19 with the reachable violation. Verified it genuinely regresses — neutering both fixes reproduces the 1-state freeze.scripts/diff_tlc.sh: 15/15.cargo test --release: 0 failures.Scope note
Found while investigating the NanoBlockchain
MCNanoSmallunder-exploration (1563 vs TLC 3003). This fix resolves the primed-argument-staging class but not MCNanoSmall itself, whose dominant blocker is a separate residual:ValidateOpenBlock's nested reads (ledger[block.source].block.type/.destination) evaluate over-restrictively, so ours rejects valid open blocks (relaxing them recovers ours to ~1163 ≈ TLC 1179). That residual is documented and parked for follow-up.