Skip to content

Commit 0f5fb20

Browse files
Yiping106283claude
andcommitted
fix(phl): check the bound of the upper-bound while rule on all memories
Summary: the pHL `while` tactic (invariant only, `<=`) accepted `phoare[M.p : true ==> false] <= (-1%r)` for `while (true) {}` (upstream #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 #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>
1 parent 884dad7 commit 0f5fb20

3 files changed

Lines changed: 78 additions & 10 deletions

File tree

‎examples/PRG.ec‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -551,7 +551,11 @@ section.
551551
rewrite
552552
(@BIA.big_cat_int (qF + size P.logP{hr} + 1) (_ + List.size _))
553553
?BIA.big_int1 /#.
554-
by skip; progress=> /#.
554+
+ by skip; progress=> /#.
555+
+ by move=> &hr [#] _ _ _ ->.
556+
move=> &hr; case: (Bad P.logP{hr} F.m{hr})=> //=.
557+
apply/divr_ge0; 2:smt(Support.card_gt0).
558+
by apply/le_fromint/Bigint.sumr_ge0_seq=> a /mem_range; smt(ge0_qF size_ge0).
555559
qed.
556560
557561
lemma conclusion &m:

‎src/phl/ecPhlWhile.ml‎

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -185,19 +185,39 @@ let t_bdhoare_while_rev_r inv tc =
185185
f_imp while_jgmt unfolded_while_jgmt
186186
in
187187

188-
(* 2. Sub-goal *)
189-
let rem_concl =
190-
let modi = s_write env lp_body in
188+
(* 2. Sub-goal: the prefix establishes the invariant *)
189+
let rem_concl = f_hoareS mt b_pre rem_s (POE.lift inv) in
190+
191+
(* 3. Sub-goal: on exit with the post-condition, the bound is 1.
192+
4. Sub-goal: the bound is non-negative.
193+
194+
Both are conditions on the bound that justify the transformation, not
195+
conditions on the behaviour of [rem_s]: they are needed for every memory
196+
the loop may start from (3, 4) and, since a non-terminating run of
197+
[rem_s] contributes probability 0 to the conclusion, for every memory
198+
satisfying the pre-condition (4). They are therefore emitted quantified
199+
over all memories and NOT inside the (partial-correctness) post-condition
200+
of sub-goal 2, which only has to hold on terminating runs of [rem_s].
201+
Moreover a pHL judgment is false as soon as its bound is negative in
202+
some memory (whether or not that memory satisfies the pre-condition),
203+
so (4) is required unconditionally. *)
204+
let exit_concl =
191205
let term_post = map_ss_inv2 f_imp
192206
(map_ss_inv2 f_and inv (map_ss_inv2 f_and (map_ss_inv1 f_not lp_guard) b_post))
193207
(map_ss_inv2 f_eq bound {m;inv=f_r1}) in
194-
let term_post = generalize_mod_ss_inv env modi term_post in
195-
let term_post = map_ss_inv2 f_and inv term_post in
196-
let post = { hsi_m = term_post.m; hsi_inv = POE.empty term_post.inv; } in
197-
f_hoareS mt b_pre rem_s post
208+
EcSubst.f_forall_mems_ss_inv mem term_post
209+
in
210+
211+
let nonneg_concl =
212+
EcSubst.f_forall_mems_ss_inv mem
213+
(map_ss_inv2 f_real_le {m;inv=f_r0} bound)
198214
in
199215

200-
FApi.xmutate1_hyps tc `While [(hyps', body_concl); (hyps, rem_concl)]
216+
FApi.xmutate1_hyps tc `While
217+
[(hyps', body_concl );
218+
(hyps , rem_concl );
219+
(hyps , exit_concl );
220+
(hyps , nonneg_concl)]
201221

202222
(* -------------------------------------------------------------------- *)
203223
(* Rule for = or >= *)
@@ -580,7 +600,12 @@ let process_while side winfos tc =
580600
t_bdhoare_while_rev_geq phi vrnt k eps tc
581601
| None, None ->
582602
let _, phi = TTC.tc1_process_Xhl_formula tc phi in
583-
t_bdhoare_while_rev phi tc
603+
(* [t_bdhoare_while_rev] emits the bound side-conditions (exit
604+
bound, non-negativity) as the last two goals; try to close them
605+
automatically so trivial cases stay effort-free. *)
606+
FApi.t_onalli
607+
(function 2 | 3 -> FApi.t_try t_trivial | _ -> t_id)
608+
(t_bdhoare_while_rev phi tc)
584609

585610
| None, Some _ ->
586611
tc_error !!tc "invalid arguments"

‎tests/phoare-while-neg-bound.ec‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
(* pHL `while` with an invariant only, upper bound (upstream #1102): the
2+
conditions on the bound (`inv /\ !e /\ post => bd = 1%r`, `0%r <= bd`)
3+
must be goals quantified over all memories, not part of the
4+
post-condition of the hoare judgment on the statements preceding the
5+
loop, which a diverging prefix or a never-established invariant
6+
discharges vacuously. Under #1105 a pHL judgment with a bound that is
7+
negative in some memory is false, so the non-negativity goal is
8+
unconditional. Here `0%r <= -1%r` must remain, unprovable. *)
9+
require import AllCore Real.
10+
11+
module M = { proc p() = { while (true) {} } }.
12+
13+
lemma bad : phoare[M.p : true ==> false] <= (-1%r).
14+
proof.
15+
proc.
16+
while (true).
17+
+ auto.
18+
+ done.
19+
(* remaining goal: forall &hr, 0%r <= -1%r *)
20+
move=> &hr.
21+
fail (by smt()).
22+
abort.
23+
24+
(* Invariant `false`, "established" by a diverging prefix: only the
25+
non-negativity of the bound rejects it. *)
26+
module N = { proc p() = { while (true) {} while (true) {} } }.
27+
28+
lemma bad' : phoare[N.p : true ==> false] <= (-1%r).
29+
proof.
30+
proc.
31+
while false.
32+
+ auto.
33+
+ while true.
34+
+ done.
35+
done.
36+
(* remaining goal: forall &hr, 0%r <= -1%r *)
37+
move=> &hr.
38+
fail (by smt()).
39+
abort.

0 commit comments

Comments
 (0)