Skip to content

Commit 39d7752

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 76bf9e9 commit 39d7752

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
@@ -447,6 +447,166 @@ let t_ecall_hoare_bwd ((cttpt, _) : proofterm * form) (tc : tcenv1) =
447447
EcPhlAuto.t_auto ?conv:None; (* Kill the conseq from the call rule *)
448448
] tc
449449

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

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

630-
| _ -> tc_error_noXhl ~kinds:[`Hoare `Stmt; `Equiv `Stmt] !!tc
795+
| _ -> 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)