Skip to content

Commit 33b335d

Browse files
committed
Implement defer construct for resource cleanup
Add 'defer pre { handler }; body' construct that executes handler whenever execution leaves body (both normal fall-through and goto/return). - Syntax: Tm_Defer with handler_pre (slprop), handler, and body - Parser: 'defer' keyword with slprop pre and braced handler block - ElimGoto: preserve defer blocks, conditionalize body but not handler - Extraction: defer extracted as sequential composition (body then handler)
1 parent 5130951 commit 33b335d

20 files changed

Lines changed: 229 additions & 6 deletions
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
(*
2+
Copyright 2026 Microsoft Research
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*)
16+
17+
module Pulse.Checker.Defer
18+
19+
open Pulse.Syntax
20+
open Pulse.Typing
21+
open Pulse.Checker.Pure
22+
open Pulse.Checker.Base
23+
open Pulse.Checker.Prover
24+
25+
module T = FStar.Tactics.V2
26+
module P = Pulse.Syntax.Printer
27+
module RU = Pulse.RuntimeUtils
28+
29+
#push-options "--z3rlimit_factor 10 --fuel 0 --ifuel 0"
30+
let check
31+
(g:env)
32+
(pre:term)
33+
(post_hint:post_hint_opt g)
34+
(res_ppname:ppname)
35+
(t:st_term { Tm_Defer? t.term })
36+
(check:check_t)
37+
: T.Tac (checker_result_t g pre post_hint)
38+
= allow_invert post_hint;
39+
match post_hint with
40+
| NoHint | TypeHint _ ->
41+
fail g (Some t.range)
42+
"defer requires an annotated post-condition"
43+
44+
| PostHint post ->
45+
let g = push_context "check_defer" t.range g in
46+
let Tm_Defer { handler_pre=cpre; handler; body } = t.term in
47+
// Check handler_pre is a valid slprop
48+
let cpre = check_slprop g cpre in
49+
// Extend env: push BindingPost so goto labels see cpre
50+
let g' = push_post g cpre in
51+
// Extend post_hint: body's postcondition must include cpre
52+
let body_post : post_hint_for_env g' =
53+
{ post with post = tm_star post.post cpre } in
54+
// Check body with extended env and post_hint
55+
let (| body', c_body |) = apply_checker_result_k (check g' pre (PostHint body_post) res_ppname body) res_ppname in
56+
// The body's postcondition should be (post ** cpre)
57+
// Now check the handler with cpre as (part of) precondition
58+
let post_hint_for_handler: post_hint_for_env g = {
59+
g;
60+
effect_annot = body_post.effect_annot;
61+
ret_ty = tm_unit;
62+
u = u0;
63+
post = tm_emp;
64+
} in
65+
let (| handler, c_handler |) = apply_checker_result_k (check g cpre (PostHint post_hint_for_handler) ppname_default handler) ppname_default in
66+
// After body, context is (post ** cpre); handler consumes cpre
67+
// Overall defer comp has the original post (without cpre)
68+
let t = wtag (Some (ctag_of_comp_st c_body)) (Tm_Defer { handler_pre = cpre; handler; body=body' }) in
69+
let c = C_ST { u=comp_u c_body; res=comp_res c_body; pre; post=post.post } in
70+
let c'' = match_comp_res_with_post_hint t c post_hint in
71+
prove_post_hint
72+
(try_frame_pre false #g (|t, c''|) res_ppname)
73+
post_hint
74+
t.range
75+
#pop-options
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
(*
2+
Copyright 2026 Microsoft Research
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*)
16+
17+
module Pulse.Checker.Defer
18+
19+
module T = FStar.Tactics.V2
20+
21+
open Pulse.Syntax
22+
open Pulse.Typing
23+
open Pulse.Checker.Base
24+
25+
val check
26+
(g:env)
27+
(pre:term)
28+
(post_hint:post_hint_opt g)
29+
(res_ppname:ppname)
30+
(t:st_term { Tm_Defer? t.term })
31+
(check:check_t)
32+
: T.Tac (checker_result_t g pre post_hint)

src/checker/Pulse.Checker.fst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ module Admit = Pulse.Checker.Admit
4444
module Return = Pulse.Checker.Return
4545
module Rewrite = Pulse.Checker.Rewrite
4646
module ForwardJumpLabel = Pulse.Checker.ForwardJumpLabel
47+
module Defer = Pulse.Checker.Defer
4748
module Goto = Pulse.Checker.Goto
4849
module PCP = Pulse.Checker.Pure
4950

@@ -455,6 +456,9 @@ let rec check
455456

456457
| Tm_Goto _ ->
457458
Goto.check g pre post_hint res_ppname t
459+
460+
| Tm_Defer _ ->
461+
Defer.check g pre post_hint res_ppname t check
458462
in
459463

460464
let (| x, g1, t, pre', k |) = r in

src/checker/Pulse.ElimGoto.fst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,16 @@ let rec conditionalize (g: env) (t: st_term) (cond: cond_params) : T.Tac (option
302302
| None -> body)
303303
| _ -> None
304304
)
305+
| Tm_Defer { handler_pre; handler; body } -> (
306+
match conditionalize g body cond, conditionalize g handler cond with
307+
| None, None -> None
308+
| Some body, None ->
309+
Some { t with term = Tm_Defer { handler_pre; handler = add_write_if_necessary g handler cond; body } }
310+
| None, Some handler ->
311+
Some { t with term = Tm_Defer { handler_pre; handler; body = add_write_if_necessary g body cond } }
312+
| Some body, Some handler ->
313+
Some { t with term = Tm_Defer { handler_pre; handler; body } }
314+
)
305315

306316
let rec elim_gotos (g: env) (t: st_term) : T.Tac st_term =
307317
match t.term with
@@ -372,6 +382,10 @@ let rec elim_gotos (g: env) (t: st_term) : T.Tac st_term =
372382
{ t with term = Tm_WithLocalArray { binder; initializer; length; body } }
373383
| Tm_ProofHintWithBinders { hint_type; binders; t } ->
374384
T.fail "elim_gotos: Unexpected constructor: ProofHintWithBinders should have been desugared away"
385+
| Tm_Defer { handler_pre; handler; body } ->
386+
let handler = elim_gotos g handler in
387+
let body = elim_gotos g body in
388+
{ t with term = Tm_Defer { handler_pre; handler; body } }
375389
| Tm_ForwardJumpLabel { lbl; body; post } ->
376390
let cond_var = fresh g in
377391
let g' = push_goto g cond_var lbl (goto_comp_of_block_comp post) in

src/checker/Pulse.Extract.Main.fst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,9 @@ let rec simplify_st_term (g:env) (e:st_term) : T.Tac st_term =
174174
| Tm_Goto _
175175
| Tm_ProofHintWithBinders _ -> e
176176

177+
| Tm_Defer { handler_pre; handler; body } ->
178+
ret (Tm_Defer { handler_pre; handler = simplify_st_term g handler; body = simplify_st_term g body })
179+
177180
| Tm_Abs { b; q; ascription; body } ->
178181
ret (Tm_Abs { b; q; ascription; body = with_open b body })
179182

@@ -335,6 +338,9 @@ let rec erase_ghost_subterms (g:env) (p:st_term) : T.Tac st_term =
335338
ret (Tm_ForwardJumpLabel { lbl; body = e; post })
336339

337340
| Tm_Goto _ -> p
341+
342+
| Tm_Defer { handler_pre; handler; body } ->
343+
ret (Tm_Defer { handler_pre; handler = erase_ghost_subterms g handler; body = erase_ghost_subterms g body })
338344

339345
end
340346

@@ -552,6 +558,12 @@ let rec extract_dv g (p:st_term) : T.Tac R.term =
552558
}) (close_term body x)), R.Q_Explicit;
553559
]
554560

561+
| Tm_Defer { handler_pre; handler; body } ->
562+
let body = extract_dv g body in
563+
let handler = extract_dv g handler in
564+
let b = R.pack_binder { sort = ECL.unit_ty; ppname = T.seal "_"; attrs = []; qual = R.Q_Explicit } in
565+
ECL.mk_let b body handler
566+
555567
end
556568

557569
and extract_dv_branch g (b:Pulse.Syntax.Base.branch) : T.Tac R.branch =

src/checker/Pulse.Syntax.Base.fst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,10 @@ let rec eq_st_term (t1 t2:st_term)
282282
| Tm_Goto { lbl=l1; arg=a1 },
283283
Tm_Goto { lbl=l2; arg=a2 } ->
284284
eq_tm l1 l2 && eq_tm a1 a2
285+
286+
| Tm_Defer { handler_pre=p1; handler=h1; body=b1 },
287+
Tm_Defer { handler_pre=p2; handler=h2; body=b2 } ->
288+
eq_tm p1 p2 && eq_st_term h1 h2 && eq_st_term b1 b2
285289

286290
| _ -> false
287291

src/checker/Pulse.Syntax.Base.fsti

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,11 @@ type st_term' =
312312
lbl: term; // either var or named
313313
arg: term;
314314
}
315+
| Tm_Defer {
316+
handler_pre: slprop;
317+
handler: st_term;
318+
body: st_term;
319+
}
315320
and st_term = {
316321
term : st_term';
317322
range : range;

src/checker/Pulse.Syntax.Builder.fst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ let tm_admit ctag u typ post = Tm_Admit { ctag; u; typ; post }
5454
let tm_pragma_with_options o b = Tm_PragmaWithOptions { options=o; body=b }
5555
let tm_forward_jump_label body lbl post = Tm_ForwardJumpLabel { body; lbl; post }
5656
let tm_goto lbl arg = Tm_Goto { lbl; arg }
57+
let tm_defer handler_pre handler body = Tm_Defer { handler_pre; handler; body }
5758
let with_range t r = { term = t; range = r; effect_tag = default_effect_hint; source=Sealed.seal true; seq_lhs=Sealed.seal false; }
5859
let tm_assert_with_binders bs p t = Tm_ProofHintWithBinders { hint_type=ASSERT { p; elaborated=false }; binders=bs; t }
5960
let mk_assert_hint_type p = ASSERT { p; elaborated=false }

src/checker/Pulse.Syntax.Naming.fst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,11 @@ let rec close_open_inverse_st' (t:st_term)
233233
| Tm_Goto { lbl; arg } ->
234234
close_open_inverse' lbl x i;
235235
close_open_inverse' arg x i
236+
237+
| Tm_Defer { handler_pre; handler; body } ->
238+
close_open_inverse' handler_pre x i;
239+
close_open_inverse_st' handler x i;
240+
close_open_inverse_st' body x i
236241
#pop-options
237242

238243
let close_open_inverse (t:term) (x:var { ~(x `Set.mem` freevars t) } )

src/checker/Pulse.Syntax.Naming.fsti

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,9 @@ let rec freevars_st (t:st_term)
168168
| Tm_Goto { lbl; arg } ->
169169
freevars lbl ++ freevars arg
170170

171+
| Tm_Defer { handler_pre; handler; body } ->
172+
freevars handler_pre ++ freevars_st handler ++ freevars_st body
173+
171174
and freevars_branches (t:list branch) : Set.set var =
172175
match t with
173176
| [] -> empty
@@ -356,6 +359,9 @@ let rec ln_st' (t:st_term) (i:int)
356359
| Tm_Goto { lbl; arg } ->
357360
ln' lbl i && ln' arg i
358361

362+
| Tm_Defer { handler_pre; handler; body } ->
363+
ln' handler_pre i && ln_st' handler i && ln_st' body i
364+
359365
and ln_branch' (b : branch) (i:int) : Tot bool (decreases b) =
360366
ln_pattern' b.pat i &&
361367
ln_st' b.e (i + pattern_shift_n b.pat)
@@ -609,6 +615,9 @@ let rec subst_st_term (t:st_term) (ss:subst)
609615
| Tm_Goto { lbl; arg } ->
610616
Tm_Goto { lbl=subst_term lbl ss; arg=subst_term arg ss }
611617

618+
| Tm_Defer { handler_pre; handler; body } ->
619+
Tm_Defer { handler_pre=subst_term handler_pre ss; handler=subst_st_term handler ss; body=subst_st_term body ss }
620+
612621
in
613622
{ t with term = t' }
614623

0 commit comments

Comments
 (0)