Skip to content

Commit f453b34

Browse files
cknittclaude
andcommitted
Add try..finally
Why: JavaScript code cleans up with finally blocks: after an await, around break and continue, and in generators, where cancellation is only seen by finally. ReScript had no way to express them. `try` takes `finally { ... }` after its body or after its catch. `finally` stays an identifier: after a catch it only starts a finally block when a brace follows. The block is a statement and must not break or continue out of it. Supersedes #7903. AST modified (Pexp_try): Example: let f = g => try g() catch { | Not_found => 0 } finally { Console.log("done") } Before (diagnostic): `finally` was not valid after a catch. After (parse): expression Pexp_try expression Pexp_apply expression Pexp_ident "g" … [ <case> pattern Ppat_construct "Not_found" expression Pexp_constant PConst_int (0,None) ] Some expression Pexp_apply expression Pexp_ident "Console.log" … Lambda added (Ltryfinally): After (lambda): (try (try (apply g/1002 unit) with raw_exn/1062 …) finally (seq (js_call[console.log] "done") unit)) JS added (try..catch..finally): After (js): function f(g) { try { return g(); } catch (raw_exn) { let exn = Primitive_exceptions.internalToException(raw_exn); if (exn.RE_EXN_ID === "Not_found") { return 0; } throw exn; } finally { console.log("done"); } } Diagnostic added (break out of finally): Example: while true { try { () } finally { break } } After (diagnostic): `break` cannot leave a `finally` block. Only a loop inside the block can be broken out of. Review: Why a separate Ltryfinally instead of an optional finally on Ltrywith? Ltrywith keeps its meaning in every pass, a try without catch needs no exception ident, and try..catch..finally is an Ltryfinally around an Ltrywith that the JS backend prints as one statement. Review: Why is the catch handler no longer in tail position? With a finally, a self tail call in the handler would become a loop `continue` that runs the finally before the recursive call instead of after it. Review: Why reject break and continue in finally? JavaScript allows them, but they silently drop a pending exception or return value. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fc7LEuyEfynjaKCVEBPmTF Signed-off-by: Christoph Knittel <ck@cca.io>
1 parent d8db3c6 commit f453b34

66 files changed

Lines changed: 1347 additions & 114 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎analysis/reanalyze/src/arnold.ml‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -996,11 +996,13 @@ module Compile = struct
996996
expression ~ctx e)
997997
|> Command.unordered_sequence
998998
| Texp_assert _ -> Command.nothing
999-
| Texp_try (e, cases) ->
999+
| Texp_try (e, cases, finally) -> (
10001000
let c_e = e |> expression ~ctx in
10011001
let c_cases = cases |> List.map (case ~ctx) |> Command.nondet in
10021002
let open Command in
1003-
c_e +++ c_cases
1003+
match finally with
1004+
| None -> c_e +++ c_cases
1005+
| Some finally -> c_e +++ c_cases +++ expression ~ctx finally)
10041006
| Texp_variant (_label, e_opt) -> e_opt |> expression_opt ~ctx
10051007
| Texp_while _ ->
10061008
not_implemented "Texp_while";

‎analysis/reanalyze/src/exception.ml‎

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -394,18 +394,23 @@ let traverse_ast ~file ~values_builder ~checks_builder () =
394394
kind = Throws;
395395
}
396396
:: !current_events
397-
| Texp_try (e, cases) ->
398-
let exceptions =
399-
cases
400-
|> List.map (fun case -> case.Typedtree.c_lhs.pat_desc)
401-
|> exceptions_of_patterns
402-
in
403-
let old_events = !current_events in
404-
current_events := [];
405-
e |> iter_expr self;
406-
current_events :=
407-
{Event.exceptions; loc; kind = Catches !current_events} :: old_events;
408-
cases |> iter_cases self
397+
| Texp_try (e, cases, finally) ->
398+
(* Catches with no exceptions means a catch-all, so a try without catch
399+
cases must not record one. *)
400+
if cases <> [] then (
401+
let exceptions =
402+
cases
403+
|> List.map (fun case -> case.Typedtree.c_lhs.pat_desc)
404+
|> exceptions_of_patterns
405+
in
406+
let old_events = !current_events in
407+
current_events := [];
408+
e |> iter_expr self;
409+
current_events :=
410+
{Event.exceptions; loc; kind = Catches !current_events} :: old_events)
411+
else e |> iter_expr self;
412+
cases |> iter_cases self;
413+
finally |> iter_expr_opt self
409414
| _ -> super.expr self expr |> ignore);
410415
(if is_does_no_throw then
411416
let nested_events = !current_events in

‎analysis/reanalyze/src/side_effects.ml‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,10 @@ let rec expr_no_side_effects (expr : Typedtree.expression) =
4949
partial = Total && e |> expr_no_side_effects
5050
&& cases |> List.for_all case_no_side_effects
5151
| Texp_letmodule _ -> false
52-
| Texp_try (e, cases) ->
53-
e |> expr_no_side_effects && cases |> List.for_all case_no_side_effects
52+
| Texp_try (e, cases, finally) ->
53+
e |> expr_no_side_effects
54+
&& cases |> List.for_all case_no_side_effects
55+
&& finally |> expr_opt_no_side_effects
5456
| Texp_tuple el -> el |> List.for_all expr_no_side_effects
5557
| Texp_variant (_lbl, eo) -> eo |> expr_opt_no_side_effects
5658
| Texp_field (e, _lid, _ld) -> e |> expr_no_side_effects

‎changes/try-finally.feature.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Add `try ... finally { ... }` and `try ... catch { ... } finally { ... }`, compiled to JavaScript `finally` blocks.

‎compiler/core/js_dump.ml‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1564,10 +1564,10 @@ and statement_desc top cxt f (s : J.statement_desc) : cxt =
15641564
match fin with
15651565
| None -> cxt
15661566
| Some b ->
1567-
P.group f 1 (fun _ ->
1568-
P.string f L.finally;
1569-
P.space f;
1570-
brace_block cxt f b))
1567+
P.space f;
1568+
P.string f L.finally;
1569+
P.space f;
1570+
brace_block cxt f b)
15711571

15721572
and function_body ?directive (cxt : cxt) f ~return_unit (b : J.block) : unit =
15731573
(match directive with

‎compiler/core/lam_analysis.ml‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ let rec no_side_effects (lam : Lambda.t) : bool =
121121
*)
122122
| Ltrywith (body, _exn, handler) ->
123123
no_side_effects body && no_side_effects handler
124+
| Ltryfinally (body, finally) ->
125+
no_side_effects body && no_side_effects finally
124126
| Lifthenelse (a, b, c) ->
125127
no_side_effects a && no_side_effects b && no_side_effects c
126128
| Lsequence (a, b) -> no_side_effects a && no_side_effects b
@@ -210,7 +212,7 @@ let size_upto ~limit (lam : Lambda.t) =
210212
add 1;
211213
List.iter size ls
212214
| Lstaticcatch _ -> add too_big
213-
| Ltrywith _ -> add too_big
215+
| Ltrywith _ | Ltryfinally _ -> add too_big
214216
| Lifthenelse (l1, l2, l3) ->
215217
add 1;
216218
size l1;

‎compiler/core/lam_arity_analysis.ml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ let rec get_arity (meta : Lam_stats.t) (lam : Lambda.t) : Lam_arity.t =
120120
| Some v -> all_lambdas meta (v :: Ext_list.map sw snd))
121121
| Lstaticcatch (_, _, handler) -> get_arity meta handler
122122
| Ltrywith (l1, _, l2) -> all_lambdas meta [l1; l2]
123+
| Ltryfinally (body, _) -> get_arity meta body
123124
| Lifthenelse (_, l2, l3) -> all_lambdas meta [l2; l3]
124125
| Lsequence (_, l2) -> get_arity meta l2
125126
| Lstaticraise _ (* since it will not be in tail position *) -> Lam_arity.na

‎compiler/core/lam_bounded_vars.ml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,10 @@ let rewrite (map : _ Hash_ident.t) (lam : Lambda.t) : Lambda.t =
154154
let v = rebind v in
155155
let l2 = aux l2 in
156156
Lambda.try_ l1 v l2
157+
| Ltryfinally (l1, l2) ->
158+
let l1 = aux l1 in
159+
let l2 = aux l2 in
160+
Lambda.try_finally l1 l2
157161
| Lifthenelse (l1, l2, l3) ->
158162
let l1 = aux l1 in
159163
let l2 = aux l2 in

‎compiler/core/lam_check.ml‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ let check ~file ~pass lam =
9292
| Lstaticcatch (e1, (j, _vars), e2) ->
9393
check_staticfails e1 (Set_int.add j cxt);
9494
check_staticfails e2 cxt
95-
| Ltrywith (e1, _exn, e2) ->
95+
| Ltrywith (e1, _, e2) | Ltryfinally (e1, e2) ->
9696
check_staticfails e1 cxt;
9797
check_staticfails e2 cxt
9898
| Lifthenelse (e1, e2, e3) -> check_list [e1; e2; e3] cxt
@@ -142,6 +142,9 @@ let check ~file ~pass lam =
142142
iter e1;
143143
def exn;
144144
iter e2
145+
| Ltryfinally (e1, e2) ->
146+
iter e1;
147+
iter e2
145148
| Lifthenelse (e1, e2, e3) ->
146149
iter e1;
147150
iter e2;

‎compiler/core/lam_closure.ml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ let free_variables (export_idents : Set_ident.t) (params : stats Map_ident.t)
117117
iter sink_pos e1;
118118
local_add_list vars;
119119
iter sink_pos e2
120-
| Ltrywith (e1, _exn, e2) ->
120+
| Ltrywith (e1, _, e2) | Ltryfinally (e1, e2) ->
121121
iter top e1;
122122
iter sink_pos e2
123123
| Lifthenelse (e1, e2, e3) ->

0 commit comments

Comments
 (0)