fix(phl): check the bound of the upper-bound while rule on all memories (on #1105) - #1130
Open
Yiping106283 wants to merge 1 commit into
Open
Conversation
This was referenced Sep 11, 2026
Yiping106283
force-pushed
the
fix-phoare-while-le-bound
branch
from
September 13, 2026 02:56
26900c0 to
dbf7bac
Compare
while rule on all memorieswhile rule on all memories (on #1105)
…ries
Summary: the pHL `while` tactic (invariant only, `<=`) accepted
`phoare[M.p : true ==> false] <= (-1%r)` for `while (true) {}` (upstream
EasyCrypt#1102), and likewise with the invariant `false` and a diverging prefix.
Root cause (src/phl/ecPhlWhile.ml, `t_bdhoare_while_rev_r`): the rule is a
fixpoint induction whose base case (the loop diverges: probability 0) and
exit case need `0%r <= bd`, and whose exit case needs
`inv /\ !e /\ post => bd = 1%r`. The exit condition was emitted inside the
post-condition of the hoare judgment on the prefix, which only has to hold
on its terminating runs, and `0%r <= bd` was not required at all, so a
diverging loop -- or a diverging prefix with the invariant `false` -- proved
any bound.
Fix: the prefix goal becomes `hoare[s : pre ==> inv]`; the exit condition
`forall &hr, inv /\ !e /\ post => bd = 1%r` and the non-negativity
`forall &hr, 0%r <= bd` are emitted by the rule as separate goals quantified
over all memories (always, in this order, after the body and prefix goals).
Based on EasyCrypt#1105: under its semantics the bound must be non-negative in every
memory, so the goal is unconditional (restricting it to `pre \/ inv` would
still let the rule prove a judgment whose bound is negative outside them).
The `while` tactic (`process_while`) tries `t_trivial` on the two of them
so trivial cases stay effort-free. examples/PRG.ec closes the two relocated
goals.
Test: tests/phoare-while-neg-bound.ec (invariants `true` and `false`). The
remaining goal `forall &hr, 0%r <= -1%r` is introduced with `move=> &hr`
(which fails when the goal is not emitted) and asserted unprovable with
`fail (by smt())`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Yiping106283
force-pushed
the
fix-phoare-while-le-bound
branch
from
September 13, 2026 03:00
dbf7bac to
395c252
Compare
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.
Fixes #1102. Based on #1105 (
negative-phoare-false, head bb75dba); this replaces the earlier version of this PR, which was based onmain.Summary
The pHL
whiletactic (invariant only,<=) acceptedphoare[M.p : true ==> false] <= (-1%r)forwhile (true) {}, and likewise with the invariantfalseand a diverging prefix (the variant noted by @oskgo in #1119). #1105 does not reach this: the rule never passes throughconseq,byprorexfalso, so with bb75dba the reproducer is still accepted. The conditions on the bound are now emitted by the rule as separate goals quantified over all memories, and thewhiletactic triest_trivialon them.Under #1105's semantics a pHL judgement is false as soon as its bound is negative in some memory, whether or not that memory satisfies the pre-condition; the non-negativity side goal is therefore unconditional (
forall &hr, 0%r <= bd) because a goal restricted topre \/ inv, as in #1130, would still let the rule prove a judgement whose bound is negative outside those memories.A distinct bug of the same rule, the bound being written by the statements preceding the loop, is not fixed here (its goals hold for that example); see #1124 and the companion PR #1131 (stacked version to follow).
Root cause (
src/phl/ecPhlWhile.ml,t_bdhoare_while_rev_r)The rule is a fixpoint induction whose base case (the loop diverges: probability 0) and exit case need
0%r <= bd, and whose exit case needsinv /\ !e /\ post => bd = 1%r. The exit condition was emitted inside the post-condition of the hoare judgment on the prefix, which only has to hold on its terminating runs, and0%r <= bdwas not required at all, so a diverging loop -- or a diverging prefix with the invariantfalse-- proved any bound.Fix
The prefix goal becomes
hoare[s : pre ==> inv]; the exit conditionforall &hr, inv /\ !e /\ post => bd = 1%rand the non-negativityforall &hr, 0%r <= bdare emitted by the rule as separate goals quantified over all memories (always, in this order, after the body and prefix goals). Thewhiletactic (process_while) triest_trivialon the two of them so trivial cases stay effort-free.The exit condition is kept as
bd = 1%r(the condition the rule always had, merely relocated); the exact need is1%r <= bd, and it can be relaxed to that at zero cost if preferred.Impact
while invon a<=judgement now yields four goals instead of two (body, prefix, exit bound, non-negativity); the last two are closed automatically when trivial (literal bounds such as1%r,0%rwithpost = false, or0%r <= bdin context). Awhilewhose old prefix goal was closed byauto/skipwith the exit condition inside the post-condition now closes the relocated goals separately.examples/PRG.eccloses the two relocated goals (the non-negativity one withge0_qF, since the goal no longer carriescard (fdom F.m) <= qF). No library file is affected. Overlap with Change pHL to prevent negative probabilities #1105's own script changes: in the same PRG.ec proof, Change pHL to prevent negative probabilities #1105'sconseqgoals inside the loop body already show0%r <= (sumid ..)%r / card%r(twice), and the relocatedwhilegoal shows it once more for the whole loop; these are distinct goals of distinct rules (thewhileone is needed even when the body is closed withoutconseq), so nothing is discharged twice on one goal, but the three-line argument is repeated and could be factored into ahaveif preferred. No other theory or example is touched by both.Test
tests/phoare-while-neg-bound.ec(invariantstrueandfalse): the remaining goalforall &hr, 0%r <= -1%ris introduced withmove=> &hr(which fails with "all goals are closed" on the Change pHL to prevent negative probabilities #1105 head, where the lemma goes through) and asserted unprovable withfail (by smt()).make unit96/96,make stdlib128/128,make examples49/49 (ECJOBS=3, local prover set). Reproducers: pHL seq permits negative probabilities #1100 and pHL call permits negative probabilities #1101 are rejected by Change pHL to prevent negative probabilities #1105, pHL while permits negative probabilities #1102 by this commit; the pHLrnd Esoundness #1119rndreproducer is still accepted (separate PR).Note on CI: the external-project checks fail here for the same reason as on #1105 itself — CI looks for a
merge-<this branch>branch on each downstream repository and otherwise checks out their default branch, which does not have the #1105 script changes yet.