Skip to content

Commit 1475d82

Browse files
mbbarbosastrub
authored andcommitted
feat(phl): support backward ecall on bdHoare/phoare goals
Extend backward `ecall` to bdHoare statement goals, so a bdHoare/phoare contract can be applied to the last call of a phoare goal — mirroring the existing hoare backward `ecall`. - ecPhlExists.ml: add `t_ecall_bdhoare_bwd` / `process_ecall_bdhoare`, and dispatch `FbdHoareS` in `process_ecall` (phoare added to the no-xhl error kinds). Program-variable contract arguments are handled via the existing ecall abstraction machinery; a trivial probability split routes the suffix call through `t_bdhoare_seq`, lifting the hoare prefix subgoal via `t_hoareS_conseq_bdhoare`. The split is sound only for lossless [= 1%r] goals and contracts, so both are checked up-front and out-of-scope uses are rejected with an explanatory message instead of failing deep inside t_call. - ecPhlConseq.mli: expose `t_hoareS_conseq_bdhoare`. - tests/phoare-ecall-bwd.ec: cover the lossless backward ecall plus the forward/side/non-lossless rejections.
1 parent d846247 commit 1475d82

3 files changed

Lines changed: 261 additions & 1 deletion

File tree

src/phl/ecPhlConseq.mli

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ val t_bdHoareF_conseq_bd : hoarecmp -> ss_inv -> FApi.backward
2323

2424
val t_hoareS_conseq_conj : ss_inv -> hs_inv -> ss_inv -> hs_inv -> FApi.backward
2525

26+
val t_hoareS_conseq_bdhoare : FApi.backward
27+
2628
(* -------------------------------------------------------------------- *)
2729
val t_equivF_conseq_nm : ts_inv -> ts_inv -> FApi.backward
2830
val t_equivS_conseq_nm : ts_inv -> ts_inv -> FApi.backward

src/phl/ecPhlExists.ml

Lines changed: 166 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,166 @@ let t_ecall_hoare_bwd ((cttpt, _) : proofterm * form) (tc : tcenv1) =
449449
EcPhlAuto.t_auto ?conv:None; (* Kill the conseq from the call rule *)
450450
] tc
451451

452+
(* -------------------------------------------------------------------- *)
453+
(* Backward ecall on bdHoare/phoare goals (ecall without ->>).
454+
*
455+
* Mirrors t_ecall_hoare_bwd but for a bdHoare goal and a bdHoare/phoare
456+
* contract. We abstract program-variable arguments of the contract pterm
457+
* into fresh local idents, dispatch to [t_call None ctt] (which routes
458+
* via [t_call]'s (FbdHoareF, FbdHoareS) arm to [t_bdhoare_call]), then
459+
* re-generalize over the abstracted idents in the residual goals.
460+
*
461+
* The prefix is split from the call with [t_bdhoare_seq] under a fixed
462+
* trivial probability split (f1=f2=1%r, g1=g2=0%r, pR=true). That split
463+
* only discharges the resulting bound side-conditions when the goal — and
464+
* the contract — are lossless [= 1%r]; both are checked up-front so that
465+
* out-of-scope goals are rejected cleanly rather than failing deep inside
466+
* [t_call]. The residual goal handed back to the user is the prefix, lifted
467+
* to a lossless bdHoare goal so that further ecalls keep dispatching here.
468+
*)
469+
let t_ecall_bdhoare_bwd ((cttpt, _) : proofterm * form) (tc : tcenv1) =
470+
let hyps = FApi.tc1_hyps tc in
471+
let env = EcEnv.LDecl.toenv hyps in
472+
let concl = destr_bdHoareS (FApi.tc1_goal tc) in
473+
let m = fst (concl.bhs_m) in
474+
let call, _ = pf_last_call !!tc concl.bhs_s in
475+
476+
(* The trivial probability split below (f1=f2=1, g1=g2=0) only discharges
477+
* the bound side-conditions when the goal is [= 1%r] (lossless). For any
478+
* other comparison or bound the construction would fail opaquely deep in
479+
* [t_call]/[apply]; reject up-front with an explanatory message instead. *)
480+
if concl.bhs_cmp <> FHeq || not (f_equal (bhs_bd concl).inv f_r1) then
481+
tc_error !!tc
482+
"backward ecall on phoare goals is only supported for lossless `= 1%%r' goals";
483+
484+
let pvs = PT.collect_pvars_from_pt cttpt in
485+
let ids, _, pvs_as_inv, subst = abstract_pvs hyps [m] pvs in
486+
487+
let cttpt =
488+
let pt_head, pt_args =
489+
match cttpt with
490+
| PTApply { pt_head; pt_args } -> (pt_head, pt_args)
491+
| _ -> assert false in
492+
let pt_args = List.map (PT.subst_pv_pt_arg env subst) pt_args in
493+
PTApply { pt_head; pt_args } in
494+
495+
let cttpt, ctt = EcLowGoal.LowApply.check `Elim cttpt (`Hyps (hyps, !!tc)) in
496+
497+
let ctt =
498+
EcReduction.h_red_opt EcReduction.full_red hyps ctt
499+
|> odfl ctt in
500+
501+
let ids_subst =
502+
List.fold_left2
503+
(fun s (id, _) pv -> EcSubst.add_flocal s id (inv_of_inv pv))
504+
EcSubst.empty ids pvs_as_inv in
505+
506+
let hf = destr_bdHoareF ctt in
507+
508+
(* The suffix call subgoal is synthesized with bound [= 1%r]; only a
509+
* lossless [= 1%r] contract applies to it. *)
510+
if hf.bhf_cmp <> FHeq || not (f_equal (bhf_bd hf).inv f_r1) then
511+
tc_error !!tc
512+
"backward ecall on phoare goals requires a lossless `= 1%%r' contract";
513+
514+
let fpre, fpost =
515+
(ss_inv_rebind (bhf_pr hf) m).inv, (ss_inv_rebind (bhf_po hf) m).inv
516+
in
517+
518+
let post =
519+
EcPhlCall.compute_hoare_call_post
520+
hyps m (fpre, POE.empty fpost) call
521+
(POE.empty (bhs_po concl).inv) in
522+
let post = EcSubst.subst_form ids_subst post in
523+
524+
(* Trivial probability split for FHeq with bound 1%r (lossless body):
525+
pR = true, f1=f2=1%r, g1=g2=0%r. Yields:
526+
- cond_phi : f_hoareS pre s1 post (user-level prefix, as hoare)
527+
- condf1 : pre s1 true 1%r (lossless prefix, trivial for det. code)
528+
- condf2 : phi s2 bhs_po 1%r (suffix bdhoare, target of t_call)
529+
- condg1 : pre s1 false 0%r (trivial)
530+
- condbd : 1*1+0*0 = bd (trivial when bd=1%r)
531+
- condnm : forall r1 r2 ... (trivial) *)
532+
let seq_info =
533+
let phi = { m; inv = post } in
534+
let pR = { m; inv = f_true } in
535+
let f1 = { m; inv = f_r1 } in
536+
let f2 = { m; inv = f_r1 } in
537+
let g1 = { m; inv = f_r0 } in
538+
let g2 = { m; inv = f_r0 } in
539+
(phi, pR, f1, f2, g1, g2)
540+
in
541+
542+
let tc = EcPhlSeq.t_bdhoare_seq (GapBefore cpos1_last) seq_info tc in
543+
(* Subgoal order from t_bdhoare_seq (with f1≠0, f2≠0, g1=0):
544+
[cond_phi; condf1; condf2; condg1; condbd; condnm]
545+
We apply the exists+intros+t_call pipeline to condf2 (the suffix).
546+
The intros chain leaves a single bdhoare goal on which t_call produces
547+
two subgoals (contract obligation + conseq); we then dispatch via
548+
t_seqsub. *)
549+
let condf2_tactic =
550+
FApi.t_seqs [
551+
t_hr_exists_intro_r pvs_as_inv;
552+
t_hr_exists_elim_r ~bound:(List.length ids);
553+
t_intros_i (List.fst ids);
554+
FApi.t_seqsub
555+
(EcPhlCall.t_call None ctt)
556+
[ EcLowGoal.Apply.t_apply_bwd_hi ~dpe:true cttpt;
557+
EcPhlAuto.t_auto ?conv:None; ];
558+
]
559+
in
560+
561+
(* t_bdhoare_seq_r auto-closes condnm via t_try, so the count is 5 (or 6 if
562+
that auto fails). Use t_onalli with an index-based dispatcher to be
563+
robust to either count. Order (0-indexed):
564+
0 -> cond_phi (HOARE prefix): lift to BDHOARE [pre ==> phi] FHeq 1%r
565+
via t_hoareS_conseq_bdhoare. This is sound under losslessness of
566+
the prefix — which the user proves as part of the residual goal.
567+
Subsequent ecalls then dispatch through our bdhoare arm and accept
568+
phoare contracts.
569+
2 -> condf2 (suffix call dance)
570+
others -> auto (trivial bound conditions). *)
571+
let tc =
572+
FApi.t_onalli
573+
(fun i ->
574+
if i = 0 then EcPhlConseq.t_hoareS_conseq_bdhoare
575+
else if i = 2 then condf2_tactic
576+
else EcPhlAuto.t_auto ?conv:None)
577+
tc
578+
in
579+
580+
tc
581+
582+
(* -------------------------------------------------------------------- *)
583+
let process_ecall_bdhoare
584+
(dir : APT.pdirection)
585+
(pterm : APT.pecall)
586+
(tc : tcenv1)
587+
=
588+
if dir <> `Backward then
589+
tc_error !!tc "forward ecall on bdHoare/phoare goals is not supported";
590+
591+
let (ctt_path, ctt_tvi, ctt_args) = pterm in
592+
let hyps = FApi.tc1_hyps tc in
593+
let concl = destr_bdHoareS (FApi.tc1_goal tc) in
594+
595+
(* Type-check contract (lemma) - apply it fully to find the bdHoare contract *)
596+
let ptenv = PT.ptenv_of_penv (LDecl.push_active_ss concl.bhs_m hyps) !!tc in
597+
let contract = PT.process_pterm ptenv (APT.FPNamed (ctt_path, ctt_tvi)) in
598+
let contract, _ = PT.process_pterm_args_app contract ctt_args in
599+
let contract = PT.apply_pterm_to_max_holes hyps contract in
600+
601+
assert (PT.can_concretize contract.PT.ptev_env);
602+
let contract = PT.concretize contract in
603+
604+
let call, _ = pf_last_call !!tc concl.bhs_s in
605+
606+
check_contract_type
607+
~phoare:true ~noexn:false ~loc:(L.loc ctt_path) ~name:(L.unloc ctt_path)
608+
!!tc hyps (`Single call) (snd contract);
609+
610+
t_ecall_bdhoare_bwd contract tc
611+
452612
(* -------------------------------------------------------------------- *)
453613
let process_ecall_hoare
454614
(dir : APT.pdirection)
@@ -626,7 +786,12 @@ let process_ecall
626786
tc_error !!tc "cannot provide a side for Hoare goals";
627787
process_ecall_hoare dir pterm tc
628788

789+
| FbdHoareS _ ->
790+
if Option.is_some oside then
791+
tc_error !!tc "cannot provide a side for bdHoare/phoare goals";
792+
process_ecall_bdhoare dir pterm tc
793+
629794
| FequivS _ ->
630795
process_ecall_equiv dir oside pterm tc
631796

632-
| _ -> tc_error_noXhl ~kinds:[`Hoare `Stmt; `Equiv `Stmt] !!tc
797+
| _ -> tc_error_noXhl ~kinds:[`Hoare `Stmt; `PHoare `Stmt; `Equiv `Stmt] !!tc

tests/phoare-ecall-bwd.ec

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
require import AllCore Distr DBool.
2+
3+
(* ================================================================== *)
4+
(* Backward ecall on bdHoare/phoare goals. *)
5+
(* *)
6+
(* Only lossless [= 1%r] goals with a lossless [= 1%r] contract are *)
7+
(* supported: the construction routes the suffix call through a *)
8+
(* trivial probability split that is sound only at bound 1%r. *)
9+
(* ================================================================== *)
10+
11+
module M = {
12+
proc f(x : int) : int = {
13+
return x + 1;
14+
}
15+
16+
proc g(x : int) : int = {
17+
var y, r : int;
18+
y <- x + 1;
19+
r <@ f(y);
20+
return r;
21+
}
22+
23+
proc h(x : int) : int = {
24+
var b : bool;
25+
b <$ dbool;
26+
return b ? 1 : 0;
27+
}
28+
29+
proc gh(x : int) : int = {
30+
var r : int;
31+
r <@ h(x);
32+
return r;
33+
}
34+
}.
35+
36+
(* ================================================================== *)
37+
(* Test 1: Backward ecall, lossless contract *)
38+
(* ================================================================== *)
39+
lemma f_spec : phoare[ M.f : x = 1 ==> res = 2 ] = 1%r.
40+
proof. by proc; auto. qed.
41+
42+
(* The call is the last instruction, with a non-empty deterministic
43+
prefix. ecall consumes the call and leaves the (lossless) prefix. *)
44+
lemma g_spec : phoare[ M.g : x = 0 ==> res = 2 ] = 1%r.
45+
proof.
46+
proc.
47+
ecall (f_spec).
48+
auto.
49+
qed.
50+
51+
(* ================================================================== *)
52+
(* Test 2: Forward ecall on phoare (negative test) *)
53+
(* ================================================================== *)
54+
lemma g_fwd : phoare[ M.g : x = 0 ==> res = 2 ] = 1%r.
55+
proof.
56+
proc.
57+
fail ecall ->> (f_spec).
58+
abort.
59+
60+
(* ================================================================== *)
61+
(* Test 3: side annotation on phoare (negative test) *)
62+
(* ================================================================== *)
63+
lemma g_side : phoare[ M.g : x = 0 ==> res = 2 ] = 1%r.
64+
proof.
65+
proc.
66+
fail ecall{1} (f_spec).
67+
abort.
68+
69+
(* ================================================================== *)
70+
(* Test 4: non-[= 1%r] goal is rejected up-front (negative test) *)
71+
(* ================================================================== *)
72+
lemma h_le : phoare[ M.h : true ==> res = 1 ] <= (1%r / 2%r).
73+
proof. admit. qed.
74+
75+
(* The goal bound is [<= 1/2], not [= 1%r]: the goal guard fires
76+
before the construction reaches t_call. *)
77+
lemma gh_le : phoare[ M.gh : true ==> res = 1 ] <= (1%r / 2%r).
78+
proof.
79+
proc.
80+
fail ecall (h_le).
81+
abort.
82+
83+
(* ================================================================== *)
84+
(* Test 5: non-[= 1%r] contract is rejected up-front (neg. test) *)
85+
(* ================================================================== *)
86+
87+
(* The goal is lossless [= 1%r] but the contract is [<= 1/2]: the
88+
contract guard fires rather than failing opaquely inside apply. *)
89+
lemma gh_eq : phoare[ M.gh : true ==> res = 1 ] = 1%r.
90+
proof.
91+
proc.
92+
fail ecall (h_le).
93+
abort.

0 commit comments

Comments
 (0)