fix(phl): swap with a code path must rebuild the enclosing program - #1134
Merged
Conversation
strub
approved these changes
Sep 11, 2026
strub
force-pushed
the
fix-swap-codepath-context
branch
from
September 11, 2026 15:40
ecf8bdd to
ff26ff5
Compare
Summary `swap 1.:[1 .. 1] 1` (a range prefixed by a code path: then-branch of instruction 1; `1?:` else-branch; loop bodies likewise) replaced the whole program of the goal by the addressed block after swapping inside it. The enclosing `if`/`while` and the sibling branches vanished, which made false hoare/phoare/equiv judgments provable (`auto`, `by sim`). Fixes EasyCrypt#1122. Root cause src/phl/ecPhlSwap.ml, LowInternal.swap_stmt. Since f0827a1 ("Extend code-position handling with gap/range semantics") the range carries a path, and swap_stmt follows it with normalize_cgap_range, which returns the sub-statement reached by the path. The swap was performed on that sub-statement and the result returned as is; t_swap_r then installed it with hl_set_stmt as the full program. The normalised path was discarded and nothing zipped the context back, whereas `proc change`/`proc rewrite` (src/phl/ecPhlRewrite.ml) open a zipper with EcMatching.Zipper.zipper_and_split_of_cgap_range and rebuild the program with Zipper.zip, and `outline` goes through Zipper.map_range (routing from 3808ca8 / 8debf27). Fix Open a zipper at the addressed block with EcMatching.Zipper.zipper_and_split_of_cgap_range (as `proc change` and `proc rewrite` do), resolve the offset and perform the swap on the block rebuilt from z_head/z_tail, and zip the swapped block back with EcMatching.Zipper.zip. check_swap still runs on the two swapped fragments of the inner block. A top-level range (empty path) yields a ZTop zipper, so the previous behaviour is unchanged there. zipper_and_split_of_cgap_range normalised start and fin separately and never checked start <= fin: a reversed range such as [3 .. 1] split into an empty middle fragment and was silently accepted (`proc change [3 .. 1] : { }` and `proc rewrite [3 .. 1] /=` were no-ops; `swap` would have inherited that, whereas its old normalize_cgap_range path rejected it). It now normalises through Position.normalize_cgap1_range, which raises InvalidCPos on a reversed range. Consequently `proc change`, `proc rewrite`, `outline` and module updates (`M with { proc f [[3 .. 1] ~ ...] }`) reject reversed ranges too. t_change_stmt (`proc change`) and process_outline did not catch InvalidCPos from the primitive (an out-of-range position was already an anomaly there); they now report "invalid code position". An empty range ([2 .. 1]) is still accepted. A path into an instruction that is not an if/while/match now gives "invalid range" instead of an assertion failure in normalize_brsel. The manual now says that a code position may carry a code path (branch, loop body, match arm). Test tests/swap-codepath-context.ec: regression test (`fail` idiom); the then/else/while-body/equiv goals that were closable after a path swap can no longer be closed by `auto`/`by sim`, and reversed ranges are rejected at top level and behind a path. tests/swap-codepath.ec: positive; in-branch, else-branch, while-body, phoare and `swap{1}` equiv swaps now work and the resulting goal is the whole program (closed with `auto` on a pre/post that only the untouched branch satisfies, and with `sim` against the pre-swapped program). tests/procchange.ec, procrewrite*.ec and outline.ec pass on the patched binary. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
strub
force-pushed
the
fix-swap-codepath-context
branch
from
September 11, 2026 15:40
ff26ff5 to
0d01353
Compare
strub
enabled auto-merge
September 11, 2026 15:40
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.
Summary
Since f0827a1,
swapaccepts a range prefixed by a code path (1.:[1 .. 1]: then-branch of instruction 1;1?:: else-branch; loop bodies). After swapping inside the addressed block it installed that inner block as the whole program of the goal: the enclosingif/whileand every sibling branch disappeared, so falsehoare/phoare/equivjudgments became provable (auto,by sim), includinglemma bad : falseviabyphoare. This PR makes the path work as intended: the swap is performed inside the addressed block and the enclosing program is rebuilt.Fixes #1122.
Root cause
src/phl/ecPhlSwap.ml,LowInternal.swap_stmt. f0827a1 ("Extend code-position handling with gap/range semantics") changedswap_kind.intervalto acodegap_range, which carries a path, and madeswap_stmtfollow it withnormalize_cgap_range. That function returns the sub-statement reached by the path; the swap was performed on it andstmt (hd @ s2 @ s1 @ tl)of that sub-statement was returned, whicht_swap_rinstalled withhl_set_stmtas the full program. The normalised path was discarded and nothing zipped the context back, whereasproc change/proc rewrite(src/phl/ecPhlRewrite.ml) open a zipper withEcMatching.Zipper.zipper_and_split_of_cgap_rangeand rebuild the program withZipper.zip, andoutlinegoes throughZipper.map_range(routing that dates from 3808ca8 / 8debf27).Fix
swap_stmtnow opens a zipper at the addressed block withEcMatching.Zipper.zipper_and_split_of_cgap_range(asproc changeandproc rewritedo), resolves the offset and performs the swap on the block rebuilt fromz_head/z_tail, and returnsEcMatching.Zipper.zipof the zipper with the swapped block as its tail.check_swap(raise / read-write independence) still runs on the two swapped fragments of the inner block. For a top-level range (empty path) the zipper isZTop, so the behaviour there is unchanged.zipper_and_split_of_cgap_rangenormalisedstartandfinseparately and never checkedstart <= fin; a reversed range such as[3 .. 1]then split into an empty middle fragment and was silently accepted (proc change [3 .. 1] : { }andproc rewrite [3 .. 1] /=were no-ops on main, andswapwould have inherited that). It now goes throughPosition.normalize_cgap1_range, which raisesInvalidCPoson a reversed range, exactly as the oldnormalize_cgap_rangepath ofswapdid.proc change(t_change_stmt) andoutlinedid not catchInvalidCPosfrom the primitive (an out-of-range position was already reported as ananomalyon main); they now report "invalid code position" / "Outline: invalid code position".doc/tactics/swap.rstnow says that a code position may carry a code path (branch, loop body, match arm).Impact
swap,swap{i}with a path now yield the original program with the two fragments exchanged inside the addressed branch/loop body/match arm, in all program logics.swapandinterleave(which builds an empty path) are unaffected.swap("invalid range"), and is now also rejected byproc change,proc rewrite,outlineand module updates (M with { proc f [[3 .. 1] ~ ...] }) with "invalid code position" (all callers ofzipper_and_split_of_cgap_range); on main they silently did nothing to the program. An empty range ([2 .. 1], i.e. the gap before 2 = the gap after 1) is still accepted, as before.proc change/outlinewith an out-of-range position now give "invalid code position" instead of ananomaly: InvalidCPos.swappath into an instruction that is not anif/while/match(e.g.swap 1.:[1 .. 1] 1on an assignment) now gives "invalid range" instead of ananomaly(Assertion failed src/ecMatching.ml:374) on main.Test
tests/swap-codepath-context.ec(regression test,fail (...)/abortidiom): afterswap 1.:[1 .. 1] 1,1?:, a while-body path andswap{1}in anequiv, the bogus goals can no longer be closed byauto/by sim; reversed ranges[3 .. 1]are rejected at top level and behind a1.:/1?:path. Fails on unpatched main at the firstfail("this command is expected to fail"), passes with the fix.tests/swap-codepath.ec(positive): legitimate swaps of two independent assignments inside a then-branch, an else-branch, a while body, in aphoaregoal and withswap{1}inequivgoals; the resulting goal is the whole program (closed byautowith a pre/post that only the untouched branch satisfies, and bysimagainst the pre-swapped program). Fails on unpatched main, passes with the fix.lemma fake,lemma bad : false, and the else/while/equiv variants) is rejected with the fix.tests/procchange.ec,tests/procrewrite.ec,tests/procrewrite-block.ec,tests/procrewrite-simpl.ec,tests/outline.ecpass individually on the patched binary (none uses a reversed range).dune build --profile=ciclean;make unit107/107.make stdlibandmake exampleswere run: the only failures aretheories/crypto/assumptions/DHIES.ec:987(prover timeout under load; passes standalone on both the patched and the unpatched binary) andexamples/ChaChaPoly/chacha_poly.ec:2525(identical on unpatched main).