diff --git a/analysis/reanalyze/src/arnold.ml b/analysis/reanalyze/src/arnold.ml index 6b078c99c0c..b67b1a9fb08 100644 --- a/analysis/reanalyze/src/arnold.ml +++ b/analysis/reanalyze/src/arnold.ml @@ -996,11 +996,13 @@ module Compile = struct expression ~ctx e) |> Command.unordered_sequence | Texp_assert _ -> Command.nothing - | Texp_try (e, cases) -> + | Texp_try (e, cases, finally) -> ( let c_e = e |> expression ~ctx in let c_cases = cases |> List.map (case ~ctx) |> Command.nondet in let open Command in - c_e +++ c_cases + match finally with + | None -> c_e +++ c_cases + | Some finally -> c_e +++ c_cases +++ expression ~ctx finally) | Texp_variant (_label, e_opt) -> e_opt |> expression_opt ~ctx | Texp_while _ -> not_implemented "Texp_while"; diff --git a/analysis/reanalyze/src/exception.ml b/analysis/reanalyze/src/exception.ml index 42cec666861..13fdc5d4b7e 100644 --- a/analysis/reanalyze/src/exception.ml +++ b/analysis/reanalyze/src/exception.ml @@ -394,18 +394,23 @@ let traverse_ast ~file ~values_builder ~checks_builder () = kind = Throws; } :: !current_events - | Texp_try (e, cases) -> - let exceptions = - cases - |> List.map (fun case -> case.Typedtree.c_lhs.pat_desc) - |> exceptions_of_patterns - in - let old_events = !current_events in - current_events := []; - e |> iter_expr self; - current_events := - {Event.exceptions; loc; kind = Catches !current_events} :: old_events; - cases |> iter_cases self + | Texp_try (e, cases, finally) -> + (* Catches with no exceptions means a catch-all, so a try without catch + cases must not record one. *) + if cases <> [] then ( + let exceptions = + cases + |> List.map (fun case -> case.Typedtree.c_lhs.pat_desc) + |> exceptions_of_patterns + in + let old_events = !current_events in + current_events := []; + e |> iter_expr self; + current_events := + {Event.exceptions; loc; kind = Catches !current_events} :: old_events) + else e |> iter_expr self; + cases |> iter_cases self; + finally |> iter_expr_opt self | _ -> super.expr self expr |> ignore); (if is_does_no_throw then let nested_events = !current_events in diff --git a/analysis/reanalyze/src/side_effects.ml b/analysis/reanalyze/src/side_effects.ml index 45b38e3f1ac..05a1d04b2b2 100644 --- a/analysis/reanalyze/src/side_effects.ml +++ b/analysis/reanalyze/src/side_effects.ml @@ -49,8 +49,10 @@ let rec expr_no_side_effects (expr : Typedtree.expression) = partial = Total && e |> expr_no_side_effects && cases |> List.for_all case_no_side_effects | Texp_letmodule _ -> false - | Texp_try (e, cases) -> - e |> expr_no_side_effects && cases |> List.for_all case_no_side_effects + | Texp_try (e, cases, finally) -> + e |> expr_no_side_effects + && cases |> List.for_all case_no_side_effects + && finally |> expr_opt_no_side_effects | Texp_tuple el -> el |> List.for_all expr_no_side_effects | Texp_variant (_lbl, eo) -> eo |> expr_opt_no_side_effects | Texp_field (e, _lid, _ld) -> e |> expr_no_side_effects diff --git a/changes/try-finally.feature.md b/changes/try-finally.feature.md new file mode 100644 index 00000000000..34de9971f74 --- /dev/null +++ b/changes/try-finally.feature.md @@ -0,0 +1 @@ +- Add `try ... finally { ... }` and `try ... catch { ... } finally { ... }`, compiled to JavaScript `finally` blocks. diff --git a/compiler/core/js_dump.ml b/compiler/core/js_dump.ml index e0d3e3ae511..80b09fb7725 100644 --- a/compiler/core/js_dump.ml +++ b/compiler/core/js_dump.ml @@ -1564,10 +1564,10 @@ and statement_desc top cxt f (s : J.statement_desc) : cxt = match fin with | None -> cxt | Some b -> - P.group f 1 (fun _ -> - P.string f L.finally; - P.space f; - brace_block cxt f b)) + P.space f; + P.string f L.finally; + P.space f; + brace_block cxt f b) and function_body ?directive (cxt : cxt) f ~return_unit (b : J.block) : unit = (match directive with diff --git a/compiler/core/lam_analysis.ml b/compiler/core/lam_analysis.ml index 272b62d60e8..43cd70bc898 100644 --- a/compiler/core/lam_analysis.ml +++ b/compiler/core/lam_analysis.ml @@ -121,6 +121,8 @@ let rec no_side_effects (lam : Lambda.t) : bool = *) | Ltrywith (body, _exn, handler) -> no_side_effects body && no_side_effects handler + | Ltryfinally (body, finally) -> + no_side_effects body && no_side_effects finally | Lifthenelse (a, b, c) -> no_side_effects a && no_side_effects b && no_side_effects c | Lsequence (a, b) -> no_side_effects a && no_side_effects b @@ -210,7 +212,7 @@ let size_upto ~limit (lam : Lambda.t) = add 1; List.iter size ls | Lstaticcatch _ -> add too_big - | Ltrywith _ -> add too_big + | Ltrywith _ | Ltryfinally _ -> add too_big | Lifthenelse (l1, l2, l3) -> add 1; size l1; diff --git a/compiler/core/lam_arity_analysis.ml b/compiler/core/lam_arity_analysis.ml index bd1179c23d9..8747722ba49 100644 --- a/compiler/core/lam_arity_analysis.ml +++ b/compiler/core/lam_arity_analysis.ml @@ -120,6 +120,7 @@ let rec get_arity (meta : Lam_stats.t) (lam : Lambda.t) : Lam_arity.t = | Some v -> all_lambdas meta (v :: Ext_list.map sw snd)) | Lstaticcatch (_, _, handler) -> get_arity meta handler | Ltrywith (l1, _, l2) -> all_lambdas meta [l1; l2] + | Ltryfinally (body, _) -> get_arity meta body | Lifthenelse (_, l2, l3) -> all_lambdas meta [l2; l3] | Lsequence (_, l2) -> get_arity meta l2 | Lstaticraise _ (* since it will not be in tail position *) -> Lam_arity.na diff --git a/compiler/core/lam_bounded_vars.ml b/compiler/core/lam_bounded_vars.ml index 0ee2e101450..8349591fe83 100644 --- a/compiler/core/lam_bounded_vars.ml +++ b/compiler/core/lam_bounded_vars.ml @@ -154,6 +154,10 @@ let rewrite (map : _ Hash_ident.t) (lam : Lambda.t) : Lambda.t = let v = rebind v in let l2 = aux l2 in Lambda.try_ l1 v l2 + | Ltryfinally (l1, l2) -> + let l1 = aux l1 in + let l2 = aux l2 in + Lambda.try_finally l1 l2 | Lifthenelse (l1, l2, l3) -> let l1 = aux l1 in let l2 = aux l2 in diff --git a/compiler/core/lam_check.ml b/compiler/core/lam_check.ml index fabd75ee9a0..afb18909c0f 100644 --- a/compiler/core/lam_check.ml +++ b/compiler/core/lam_check.ml @@ -92,7 +92,7 @@ let check ~file ~pass lam = | Lstaticcatch (e1, (j, _vars), e2) -> check_staticfails e1 (Set_int.add j cxt); check_staticfails e2 cxt - | Ltrywith (e1, _exn, e2) -> + | Ltrywith (e1, _, e2) | Ltryfinally (e1, e2) -> check_staticfails e1 cxt; check_staticfails e2 cxt | Lifthenelse (e1, e2, e3) -> check_list [e1; e2; e3] cxt @@ -142,6 +142,9 @@ let check ~file ~pass lam = iter e1; def exn; iter e2 + | Ltryfinally (e1, e2) -> + iter e1; + iter e2 | Lifthenelse (e1, e2, e3) -> iter e1; iter e2; diff --git a/compiler/core/lam_closure.ml b/compiler/core/lam_closure.ml index 93f3b81fae4..97bd1b47a7e 100644 --- a/compiler/core/lam_closure.ml +++ b/compiler/core/lam_closure.ml @@ -117,7 +117,7 @@ let free_variables (export_idents : Set_ident.t) (params : stats Map_ident.t) iter sink_pos e1; local_add_list vars; iter sink_pos e2 - | Ltrywith (e1, _exn, e2) -> + | Ltrywith (e1, _, e2) | Ltryfinally (e1, e2) -> iter top e1; iter sink_pos e2 | Lifthenelse (e1, e2, e3) -> diff --git a/compiler/core/lam_compile.ml b/compiler/core/lam_compile.ml index db8d1df05d4..574637509fd 100644 --- a/compiler/core/lam_compile.ml +++ b/compiler/core/lam_compile.ml @@ -42,7 +42,8 @@ let rec source_loc_of_lam (lam : Lambda.t) = | None -> source_loc_of_lam body) | Lletrec (_, body) | Lsequence (_, body) -> source_loc_of_lam body | Lifthenelse (_, then_, _) -> source_loc_of_lam then_ - | Lstaticcatch (body, _, _) | Ltrywith (body, _, _) -> source_loc_of_lam body + | Lstaticcatch (body, _, _) | Ltrywith (body, _, _) | Ltryfinally (body, _) -> + source_loc_of_lam body | Lstringswitch (_, cases, default) -> ( match cases with | (_, body) :: _ -> source_loc_of_lam body @@ -1345,16 +1346,37 @@ let compile output_prefix = } ]} *) - and compile_trywith lam id catch (lambda_cxt : Lam_compile_context.t) = + and compile_try lam (catch : (Ident.t * Lambda.t) option) + (finally : Lambda.t option) (lambda_cxt : Lam_compile_context.t) = let aux (with_context : Lam_compile_context.t) (body_context : Lam_compile_context.t) = (* should_return is passed down - #1701, try should prevent tailcall *) + #1701, try should prevent tailcall. + A finally block runs after the catch handler, so the handler is not + in tail position either. *) + let with_context = + match finally with + | None -> with_context + | Some _ -> body_context + in [ S.try_ (Js_output.output_as_block (compile_lambda body_context lam)) - ~with_: - (id, Js_output.output_as_block (compile_lambda with_context catch)); + ?with_: + (Option.map + (fun (id, catch) -> + ( id, + Js_output.output_as_block (compile_lambda with_context catch) + )) + catch) + ?finally: + (Option.map + (fun finally -> + Js_output.output_as_block + (compile_lambda + {lambda_cxt with continuation = EffectCall Not_tail} + finally)) + finally); ] in match lambda_cxt.continuation with @@ -1944,8 +1966,11 @@ let compile output_prefix = compile_for_await_of id iterable body lambda_cxt | Lassign (id, lambda) -> compile_assign id lambda lambda_cxt | Ltrywith (lam, id, catch) -> - (* generate documentation *) - compile_trywith lam id catch lambda_cxt + compile_try lam (Some (id, catch)) None lambda_cxt + | Ltryfinally (Ltrywith (lam, id, catch), finally) -> + compile_try lam (Some (id, catch)) (Some finally) lambda_cxt + | Ltryfinally (lam, finally) -> + compile_try lam None (Some finally) lambda_cxt in (compile_recursive_lets, compile_lambda) diff --git a/compiler/core/lam_exit_count.ml b/compiler/core/lam_exit_count.ml index f8868da8f81..8b10e75e4ea 100644 --- a/compiler/core/lam_exit_count.ml +++ b/compiler/core/lam_exit_count.ml @@ -92,7 +92,7 @@ let count_helper (lam : Lambda.t) : collection option = count l; Ext_list.iter_snd sw.sw_consts count; Ext_list.iter_snd sw.sw_blocks count - | Ltrywith (l1, _v, l2) -> + | Ltrywith (l1, _, l2) | Ltryfinally (l1, l2) -> count l1; count l2 | Lifthenelse (l1, l2, l3) -> diff --git a/compiler/core/lam_hit.ml b/compiler/core/lam_hit.ml index fdc610d1ac8..0f728215034 100644 --- a/compiler/core/lam_hit.ml +++ b/compiler/core/lam_hit.ml @@ -38,7 +38,7 @@ let hit_variables (fv : Set_ident.t) (l : t) : bool = | Lvar id -> hit_var id | Lassign (id, e) -> hit_var id || hit e | Lstaticcatch (e1, (_, _vars), e2) -> hit e1 || hit e2 - | Ltrywith (e1, _exn, e2) -> hit e1 || hit e2 + | Ltrywith (e1, _, e2) | Ltryfinally (e1, e2) -> hit e1 || hit e2 | Lfunction {body; params = _} -> hit body | Llet (_str, _id, arg, body) -> hit arg || hit body | Lletrec (decl, body) -> hit body || hit_list_snd decl @@ -75,7 +75,7 @@ let hit_variable (fv : Ident.t) (l : t) : bool = | Lvar id -> hit_var id | Lassign (id, e) -> hit_var id || hit e | Lstaticcatch (e1, (_, _vars), e2) -> hit e1 || hit e2 - | Ltrywith (e1, _exn, e2) -> hit e1 || hit e2 + | Ltrywith (e1, _, e2) | Ltryfinally (e1, e2) -> hit e1 || hit e2 | Lfunction {body; params = _} -> hit body | Llet (_str, _id, arg, body) -> hit arg || hit body | Lletrec (decl, body) -> hit body || hit_list_snd decl diff --git a/compiler/core/lam_pass_collect.ml b/compiler/core/lam_pass_collect.ml index f4cf2985933..6b86c5be02e 100644 --- a/compiler/core/lam_pass_collect.ml +++ b/compiler/core/lam_pass_collect.ml @@ -116,7 +116,7 @@ let collect_info (meta : Lam_stats.t) (lam : Lambda.t) = | Lstaticcatch (l1, (_, _), l2) -> collect l1; collect l2 - | Ltrywith (l1, _, l2) -> + | Ltrywith (l1, _, l2) | Ltryfinally (l1, l2) -> collect l1; collect l2 | Lifthenelse (l1, l2, l3) -> diff --git a/compiler/core/lam_pass_count.ml b/compiler/core/lam_pass_count.ml index d577365f61a..27d1c1d0893 100644 --- a/compiler/core/lam_pass_count.ml +++ b/compiler/core/lam_pass_count.ml @@ -163,7 +163,7 @@ let collect_occurs lam : occ_tbl = | Lstaticcatch (l1, (_i, _), l2) -> count bv l1; count bv l2 - | Ltrywith (l1, _v, l2) -> + | Ltrywith (l1, _, l2) | Ltryfinally (l1, l2) -> count bv l1; count bv l2 | Lifthenelse (l1, l2, l3) -> diff --git a/compiler/core/lam_pass_exits.ml b/compiler/core/lam_pass_exits.ml index 418e5c13f1b..f5c02cafcff 100644 --- a/compiler/core/lam_pass_exits.ml +++ b/compiler/core/lam_pass_exits.ml @@ -55,7 +55,7 @@ and no_bounded_variables (l : Lambda.t) = | Lfunction {body; params} -> params = [] && no_bounded_variables body | Lfor _ -> false | Lfor_of _ | Lfor_await_of _ -> false - | Ltrywith _ -> false + | Ltrywith _ | Ltryfinally _ -> false | Llet _ -> false | Lletrec (decl, body) -> decl = [] && no_bounded_variables body diff --git a/compiler/ext/config.ml b/compiler/ext/config.ml index 3febba61945..462f0a3912a 100644 --- a/compiler/ext/config.ml +++ b/compiler/ext/config.ml @@ -1,10 +1,10 @@ -let cmi_magic_number = "Caml1999I039" +let cmi_magic_number = "Caml1999I040" (* Magic numbers for marshaled values of the *current* parsetree, whose layout changes across compiler versions. *) -and ast_impl_magic_number = "ResImpl01317" +and ast_impl_magic_number = "ResImpl01318" -and ast_intf_magic_number = "ResIntf01317" +and ast_intf_magic_number = "ResIntf01318" (* Magic numbers of the frozen Parsetree0 (OCaml 4.06) layout used on the external-PPX wire. They must never be written in front of a @@ -13,7 +13,7 @@ and ast0_impl_magic_number = "Caml1999M022" and ast0_intf_magic_number = "Caml1999N022" -and cmt_magic_number = "Caml1999T047" +and cmt_magic_number = "Caml1999T048" let get_load_path () = (Compiler_request_state.current ()).load_path let set_load_path path = (Compiler_request_state.current ()).load_path <- path diff --git a/compiler/ml/ast_helper.ml b/compiler/ml/ast_helper.ml index cf0bf224c1a..7fea7b1bf57 100644 --- a/compiler/ml/ast_helper.ml +++ b/compiler/ml/ast_helper.ml @@ -163,7 +163,7 @@ module Exp = struct = mk ?loc ?attrs (Pexp_apply {funct; args; partial; transformed_jsx}) let match_ ?loc ?attrs a b = mk ?loc ?attrs (Pexp_match (a, b)) - let try_ ?loc ?attrs a b = mk ?loc ?attrs (Pexp_try (a, b)) + let try_ ?loc ?attrs ?finally a b = mk ?loc ?attrs (Pexp_try (a, b, finally)) let tuple ?loc ?attrs a = mk ?loc ?attrs (Pexp_tuple a) let construct ?loc ?attrs a b = mk ?loc ?attrs (Pexp_construct (a, b)) let variant ?loc ?attrs a b = mk ?loc ?attrs (Pexp_variant (a, b)) diff --git a/compiler/ml/ast_helper.mli b/compiler/ml/ast_helper.mli index 05908095de9..9ccdc7cba5a 100644 --- a/compiler/ml/ast_helper.mli +++ b/compiler/ml/ast_helper.mli @@ -142,7 +142,13 @@ module Exp : sig (arg_label * expression) list -> expression val match_ : ?loc:loc -> ?attrs:attrs -> expression -> case list -> expression - val try_ : ?loc:loc -> ?attrs:attrs -> expression -> case list -> expression + val try_ : + ?loc:loc -> + ?attrs:attrs -> + ?finally:expression -> + expression -> + case list -> + expression val tuple : ?loc:loc -> ?attrs:attrs -> expression list -> expression val construct : ?loc:loc -> diff --git a/compiler/ml/ast_iterator.ml b/compiler/ml/ast_iterator.ml index 1f85c513c0c..5c4b4f04262 100644 --- a/compiler/ml/ast_iterator.ml +++ b/compiler/ml/ast_iterator.ml @@ -329,9 +329,10 @@ module E = struct | Pexp_match (e, pel) -> sub.expr sub e; sub.cases sub pel - | Pexp_try (e, pel) -> + | Pexp_try (e, pel, finally) -> sub.expr sub e; - sub.cases sub pel + sub.cases sub pel; + iter_opt (sub.expr sub) finally | Pexp_tuple el -> List.iter (sub.expr sub) el | Pexp_construct (lid, {txt = args; loc = args_loc}) -> iter_loc sub lid; diff --git a/compiler/ml/ast_mapper.ml b/compiler/ml/ast_mapper.ml index 619047539f5..0c7f7d82fa0 100644 --- a/compiler/ml/ast_mapper.ml +++ b/compiler/ml/ast_mapper.ml @@ -347,7 +347,10 @@ module E = struct (List.map (map_snd (sub.expr sub)) l) | Pexp_match (e, pel) -> match_ ~loc ~attrs (sub.expr sub e) (sub.cases sub pel) - | Pexp_try (e, pel) -> try_ ~loc ~attrs (sub.expr sub e) (sub.cases sub pel) + | Pexp_try (e, pel, finally) -> + try_ ~loc ~attrs + ?finally:(map_opt (sub.expr sub) finally) + (sub.expr sub e) (sub.cases sub pel) | Pexp_tuple el -> tuple ~loc ~attrs (List.map (sub.expr sub) el) | Pexp_construct (lid, {txt = args; loc = args_loc}) -> construct ~loc ~attrs (map_loc sub lid) diff --git a/compiler/ml/ast_mapper_from0.ml b/compiler/ml/ast_mapper_from0.ml index ca5d1021fc4..b1021e8db07 100644 --- a/compiler/ml/ast_mapper_from0.ml +++ b/compiler/ml/ast_mapper_from0.ml @@ -219,6 +219,7 @@ let raw_source_from0 sub = function let for_of_attr_name = "_res.for_of" let for_await_of_attr_name = "_res.for_await_of" +let finally_attr_name = "_res.finally" let map_loc sub {loc; txt} = {loc = sub.location sub loc; txt} @@ -694,6 +695,21 @@ module E = struct | _ -> None) attrs + let extract_finally_attribute attrs = + List.find_map + (function + | {Location.txt}, Pt.PPat (_, Some expr) when txt = finally_attr_name -> + Some expr + | _ -> None) + attrs + + let remove_finally_attribute attrs = + List.filter + (function + | {Location.txt}, _ when txt = finally_attr_name -> false + | _ -> true) + attrs + let remove_for_of_attribute attrs = List.filter (function @@ -1099,7 +1115,10 @@ module E = struct l) | Pexp_match (e, pel) -> match_ ~loc ~attrs (sub.expr sub e) (sub.cases sub pel) - | Pexp_try (e, pel) -> try_ ~loc ~attrs (sub.expr sub e) (sub.cases sub pel) + | Pexp_try (e, pel) -> + let finally = extract_finally_attribute attrs in + let attrs = remove_finally_attribute attrs in + try_ ~loc ~attrs ?finally (sub.expr sub e) (sub.cases sub pel) | Pexp_tuple el -> tuple ~loc ~attrs (List.map (sub.expr sub) el) (* <> *) | Pexp_construct ({txt = Longident.Lident "[]" | Longident.Lident "::"}, _) diff --git a/compiler/ml/ast_mapper_to0.ml b/compiler/ml/ast_mapper_to0.ml index 8901c201e0b..7da9399a781 100644 --- a/compiler/ml/ast_mapper_to0.ml +++ b/compiler/ml/ast_mapper_to0.ml @@ -111,6 +111,7 @@ let add_template_attr attrs = let for_of_attr_name = "_res.for_of" let for_await_of_attr_name = "_res.for_await_of" +let finally_attr_name = "_res.finally" let ppx_context_string_attr_name = "_res.ppx_context_string" let has_ppx_context_string_attr attrs = @@ -652,7 +653,17 @@ module E = struct args) | Pexp_match (e, pel) -> match_ ~loc ~attrs (sub.expr sub e) (sub.cases sub pel) - | Pexp_try (e, pel) -> try_ ~loc ~attrs (sub.expr sub e) (sub.cases sub pel) + | Pexp_try (e, pel, finally) -> + let attrs = + match finally with + | None -> attrs + | Some finally -> + sub.attribute sub + ( Location.mkloc finally_attr_name finally.pexp_loc, + PPat (Ast_helper.Pat.any (), Some finally) ) + :: attrs + in + try_ ~loc ~attrs (sub.expr sub e) (sub.cases sub pel) | Pexp_tuple el -> tuple ~loc ~attrs (List.map (sub.expr sub) el) | Pexp_construct (lid, {txt = args; loc = args_loc}) -> let lid = map_loc sub lid in diff --git a/compiler/ml/depend.ml b/compiler/ml/depend.ml index 63930784866..7f185124226 100644 --- a/compiler/ml/depend.ml +++ b/compiler/ml/depend.ml @@ -238,9 +238,10 @@ let rec add_expr bv exp = | Pexp_match (e, pel) -> add_expr bv e; add_cases bv pel - | Pexp_try (e, pel) -> + | Pexp_try (e, pel, finally) -> add_expr bv e; - add_cases bv pel + add_cases bv pel; + add_opt add_expr bv finally | Pexp_tuple el -> List.iter (add_expr bv) el | Pexp_construct (c, {txt = args}) -> add bv c; diff --git a/compiler/ml/lambda.ml b/compiler/ml/lambda.ml index 57760bdf61f..4bca174007a 100644 --- a/compiler/ml/lambda.ml +++ b/compiler/ml/lambda.ml @@ -358,6 +358,7 @@ type t = | Lstaticraise of int * t list | Lstaticcatch of t * (int * Ident.t list) * t | Ltrywith of t * Ident.t * t + | Ltryfinally of t * t | Lifthenelse of t * t * t | Lsequence of t * t | Lbreak @@ -672,6 +673,7 @@ let letrec bindings body : t = Lletrec (bindings, body) let staticraise i args : t = Lstaticraise (i, args) let staticcatch body catch handler : t = Lstaticcatch (body, catch, handler) let try_ body id handler : t = Ltrywith (body, id, handler) +let try_finally body finally : t = Ltryfinally (body, finally) let break : t = Lbreak let continue : t = Lcontinue let while_ cond body : t = Lwhile (cond, body) @@ -765,7 +767,7 @@ let rec eq_approx (l1 : t) (l2 : t) = | _ -> false) | Lfunction _ | Llet (_, _, _, _) - | Lletrec _ | Lswitch _ | Lstaticcatch _ | Ltrywith _ + | Lletrec _ | Lswitch _ | Lstaticcatch _ | Ltrywith _ | Ltryfinally _ | Lfor (_, _, _, _, _) | Lfor_of (_, _, _) | Lfor_await_of (_, _, _) -> diff --git a/compiler/ml/lambda.mli b/compiler/ml/lambda.mli index 571568407e8..2c56a5d4145 100644 --- a/compiler/ml/lambda.mli +++ b/compiler/ml/lambda.mli @@ -373,6 +373,11 @@ type t = private | Lstaticraise of int * t list | Lstaticcatch of t * (int * Ident.t list) * t | Ltrywith of t * Ident.t * t + | Ltryfinally of t * t + (** [Ltryfinally (body, finally)] evaluates [body], then [finally] however + [body] is left (normally, by an exception, or by break/continue/return), + and has the value of [body]. A try with both catch and finally is an + [Ltryfinally] around an [Ltrywith]. *) | Lifthenelse of t * t * t | Lsequence of t * t | Lbreak @@ -491,6 +496,8 @@ val staticcatch : t -> int * Ident.t list -> t -> t val try_ : t -> Ident.t -> t -> t +val try_finally : t -> t -> t + val if_ : t -> t -> t -> t val seq : t -> t -> t diff --git a/compiler/ml/lambda_scc.ml b/compiler/ml/lambda_scc.ml index 9657ee24185..5ed65a125f5 100644 --- a/compiler/ml/lambda_scc.ml +++ b/compiler/ml/lambda_scc.ml @@ -40,6 +40,7 @@ let exists_var (p : Ident.t -> bool) (l : Lambda.t) : bool = | Lassign (id, e) -> p id || hit e | Lstaticcatch (e1, _, e2) | Ltrywith (e1, _, e2) + | Ltryfinally (e1, e2) | Lsequence (e1, e2) | Lwhile (e1, e2) | Lfor_of (_, e1, e2) diff --git a/compiler/ml/lambda_traverse.ml b/compiler/ml/lambda_traverse.ml index d4964dea779..560501d4359 100644 --- a/compiler/ml/lambda_traverse.ml +++ b/compiler/ml/lambda_traverse.ml @@ -80,6 +80,9 @@ let shallow_map_sharing (f : t -> t) (lam : t) : t = | Ltrywith (b, id, h) -> let b' = f b and h' = f h in if b' == b && h' == h then lam else try_ b' id h' + | Ltryfinally (b, fin) -> + let b' = f b and fin' = f fin in + if b' == b && fin' == fin then lam else try_finally b' fin' | Lifthenelse (a, b, c) -> let a' = f a and b' = f b and c' = f c in if a' == a && b' == b && c' == c then lam else if_ a' b' c' @@ -146,6 +149,7 @@ let make_key e = | Lstaticcatch (e1, xs, e2) -> staticcatch (tr_rec env e1) xs (tr_rec env e2) | Ltrywith (e1, x, e2) -> try_ (tr_rec env e1) x (tr_rec env e2) + | Ltryfinally (e1, e2) -> try_finally (tr_rec env e1) (tr_rec env e2) | Lifthenelse (cond, ifso, ifnot) -> if_ (tr_rec env cond) (tr_rec env ifso) (tr_rec env ifnot) | Lsequence (e1, e2) -> seq (tr_rec env e1) (tr_rec env e2) @@ -190,7 +194,7 @@ let shallow_exists (f : t -> bool) (lam : t) : bool = || Option.fold ~none:false ~some:f default | Lstaticraise (_, args) -> Ext_list.exists args f | Lstaticcatch (e1, _, e2) -> f e1 || f e2 - | Ltrywith (e1, _, e2) -> f e1 || f e2 + | Ltrywith (e1, _, e2) | Ltryfinally (e1, e2) -> f e1 || f e2 | Lifthenelse (e1, e2, e3) -> f e1 || f e2 || f e3 | Lsequence (e1, e2) -> f e1 || f e2 | Lwhile (e1, e2) -> f e1 || f e2 @@ -225,8 +229,8 @@ let free_ids get l = fv := Set_ident.remove v !fv | Lassign (id, _e) -> fv := Set_ident.add id !fv | Lvar _ | Lglobal_module _ | Lconst _ | Lapply _ | Lprim _ | Lswitch _ - | Lstringswitch _ | Lstaticraise _ | Lifthenelse _ | Lsequence _ | Lbreak - | Lcontinue | Lwhile _ -> + | Lstringswitch _ | Lstaticraise _ | Ltryfinally _ | Lifthenelse _ + | Lsequence _ | Lbreak | Lcontinue | Lwhile _ -> () in free l; diff --git a/compiler/ml/parsetree.ml b/compiler/ml/parsetree.ml index b38d060e25a..5b769c07d61 100644 --- a/compiler/ml/parsetree.ml +++ b/compiler/ml/parsetree.ml @@ -323,8 +323,10 @@ and expression_desc = *) | Pexp_match of expression * case list (* match E0 with P1 -> E1 | ... | Pn -> En *) - | Pexp_try of expression * case list - (* try E0 with P1 -> E1 | ... | Pn -> En *) + | Pexp_try of expression * case list * expression option + (* try E0 catch { | P1 => E1 | ... | Pn => En } finally E_fin + The case list is empty when there is no catch, and then there is always + a finally expression. *) | Pexp_tuple of expression list (* (E1, ..., En) diff --git a/compiler/ml/pprintast.ml b/compiler/ml/pprintast.ml index 5fb63f4a96e..796a0b7b7cd 100644 --- a/compiler/ml/pprintast.ml +++ b/compiler/ml/pprintast.ml @@ -673,11 +673,20 @@ and expression ctxt f x = | Pexp_match (e, l) -> pp f "@[@[@[<2>match %a@]@ with@]%a@]" (expression reset_ctxt) e (case_list ctxt) l - | Pexp_try (e, l) -> - pp f "@[<0>@[try@ %a@]@ @[<0>with%a@]@]" - (* "try@;@[<2>%a@]@\nwith@\n%a"*) - (expression reset_ctxt) - e (case_list ctxt) l + | Pexp_try (e, l, finally) -> ( + let pp_finally f = function + | None -> () + | Some fin -> pp f "@ @[finally@ %a@]" (expression reset_ctxt) fin + in + match l with + | [] -> + pp f "@[<0>@[try@ %a@]%a@]" (expression reset_ctxt) e pp_finally + finally + | _ -> + pp f "@[<0>@[try@ %a@]@ @[<0>with%a@]%a@]" + (* "try@;@[<2>%a@]@\nwith@\n%a"*) + (expression reset_ctxt) + e (case_list ctxt) l pp_finally finally) | Pexp_let (rf, l, e) -> (* pp f "@[<2>let %a%a in@;<1 -2>%a@]" (*no indentation here, a new line*) *) diff --git a/compiler/ml/printast.ml b/compiler/ml/printast.ml index 94763a1d864..101db14d639 100644 --- a/compiler/ml/printast.ml +++ b/compiler/ml/printast.ml @@ -319,10 +319,11 @@ and expression i ppf x = line i ppf "Pexp_match\n"; expression i ppf e; list i case ppf l - | Pexp_try (e, l) -> + | Pexp_try (e, l, finally) -> line i ppf "Pexp_try\n"; expression i ppf e; - list i case ppf l + list i case ppf l; + option i expression ppf finally | Pexp_tuple l -> line i ppf "Pexp_tuple\n"; list i expression ppf l diff --git a/compiler/ml/printlambda.ml b/compiler/ml/printlambda.ml index 90029de4e3a..0457903c6a6 100644 --- a/compiler/ml/printlambda.ml +++ b/compiler/ml/printlambda.ml @@ -349,6 +349,8 @@ let rec lam ppf = function | Ltrywith (lbody, param, lhandler) -> fprintf ppf "@[<2>(try@ %a@;<1 -1>with %a@ %a)@]" lam lbody Ident.print param lam lhandler + | Ltryfinally (lbody, lfinally) -> + fprintf ppf "@[<2>(try@ %a@;<1 -1>finally@ %a)@]" lam lbody lam lfinally | Lifthenelse (lcond, lif, lelse) -> fprintf ppf "@[<2>(if@ %a@ %a@ %a)@]" lam lcond lam lif lam lelse | Lsequence (l1, l2) -> fprintf ppf "@[<2>(seq@ %a@ %a)@]" lam l1 sequence l2 diff --git a/compiler/ml/printtyped.ml b/compiler/ml/printtyped.ml index 7e59096a454..f017f9a21cf 100644 --- a/compiler/ml/printtyped.ml +++ b/compiler/ml/printtyped.ml @@ -299,10 +299,11 @@ and expression i ppf x = expression i ppf e; list i case ppf l1; list i case ppf l2 - | Texp_try (e, l) -> + | Texp_try (e, l, finally) -> line i ppf "Texp_try\n"; expression i ppf e; - list i case ppf l + list i case ppf l; + option i expression ppf finally | Texp_tuple l -> line i ppf "Texp_tuple\n"; list i expression ppf l diff --git a/compiler/ml/rec_check.ml b/compiler/ml/rec_check.ml index 421ca6195d4..47a2a2e8760 100644 --- a/compiler/ml/rec_check.ml +++ b/compiler/ml/rec_check.ml @@ -319,10 +319,15 @@ let rec expression : Env.env -> Typedtree.expression -> Use.t = | Texp_letexception (_, e) -> expression env e | Texp_assert e -> Use.inspect (expression env e) | Texp_pack m -> modexp env m - | Texp_try (e, cases) -> + | Texp_try (e, cases, finally) -> (* This is more permissive than the old check. *) let case env {Typedtree.c_rhs} = expression env c_rhs in - Use.join (expression env e) (list case env cases) + let finally = + match finally with + | None -> Use.empty + | Some finally -> Use.discard (expression env finally) + in + Use.join (Use.join (expression env e) (list case env cases)) finally | Texp_function {params; body} -> let env = List.fold_left diff --git a/compiler/ml/tast_iterator.ml b/compiler/ml/tast_iterator.ml index 80caa9b8520..fb505e98fac 100644 --- a/compiler/ml/tast_iterator.ml +++ b/compiler/ml/tast_iterator.ml @@ -161,9 +161,10 @@ let expr sub {exp_extra; exp_desc; exp_env; _} = sub.expr sub exp; sub.cases sub list1; sub.cases sub list2 - | Texp_try (exp, cases) -> + | Texp_try (exp, cases, finally) -> sub.expr sub exp; - sub.cases sub cases + sub.cases sub cases; + Option.iter (sub.expr sub) finally | Texp_tuple list -> List.iter (sub.expr sub) list | Texp_construct (_, _, args) -> List.iter (sub.expr sub) args | Texp_variant (_, expo) -> Option.iter (sub.expr sub) expo diff --git a/compiler/ml/tast_mapper.ml b/compiler/ml/tast_mapper.ml index a6606fe8f17..b8905b8d269 100644 --- a/compiler/ml/tast_mapper.ml +++ b/compiler/ml/tast_mapper.ml @@ -219,7 +219,9 @@ let expr sub x = | Texp_match (exp, cases, exn_cases, p) -> Texp_match (sub.expr sub exp, sub.cases sub cases, sub.cases sub exn_cases, p) - | Texp_try (exp, cases) -> Texp_try (sub.expr sub exp, sub.cases sub cases) + | Texp_try (exp, cases, finally) -> + Texp_try + (sub.expr sub exp, sub.cases sub cases, opt (sub.expr sub) finally) | Texp_tuple list -> Texp_tuple (List.map (sub.expr sub) list) | Texp_construct (lid, cd, args) -> Texp_construct (lid, cd, List.map (sub.expr sub) args) diff --git a/compiler/ml/translcore.ml b/compiler/ml/translcore.ml index 3bd8184ad1b..71db3b7d672 100644 --- a/compiler/ml/translcore.ml +++ b/compiler/ml/translcore.ml @@ -916,6 +916,7 @@ let exception_id_destructed (l : Lambda.t) (fv : Ident.t) : bool = | Lassign (id, e) -> Ident.same id fv || hit e | Lstaticcatch (e1, _, e2) -> hit e1 || hit e2 | Ltrywith (e1, _, e2) -> hit e1 || hit e2 + | Ltryfinally (e1, e2) -> hit e1 || hit e2 | Lfunction {body} -> hit body | Llet (_, _, arg, body) -> hit arg || hit body | Lletrec (decl, body) -> hit body || hit_list_snd decl @@ -1324,11 +1325,22 @@ and transl_exp0 (e : Typedtree.expression) : Lambda.t = e.exp_loc | Texp_match (arg, pat_expr_list, exn_pat_expr_list, partial) -> transl_match e arg pat_expr_list exn_pat_expr_list partial - | Texp_try (body, pat_expr_list) -> - let id = Typecore.name_pattern "exn" pat_expr_list in - let handler = Matching.for_trywith (var id) (transl_cases pat_expr_list) in - let id, handler = pack_trywith_exn id handler in - try_ (transl_exp body) id handler + | Texp_try (body, pat_expr_list, finally) -> ( + let body = transl_exp body in + let body = + match pat_expr_list with + | [] -> body + | _ :: _ -> + let id = Typecore.name_pattern "exn" pat_expr_list in + let handler = + Matching.for_trywith (var id) (transl_cases pat_expr_list) + in + let id, handler = pack_trywith_exn id handler in + try_ body id handler + in + match finally with + | None -> body + | Some finally -> try_finally body (transl_exp finally)) | Texp_tuple el -> ( let ll = transl_list el in try const (Const_block (Blk_tuple, List.map extract_constant ll)) diff --git a/compiler/ml/typecore.ml b/compiler/ml/typecore.ml index 637fd6532e4..bd4640fe4db 100644 --- a/compiler/ml/typecore.ml +++ b/compiler/ml/typecore.ml @@ -79,6 +79,8 @@ type error = | Inlined_record_expected | Break_outside_loop | Continue_outside_loop + | Break_out_of_finally + | Continue_out_of_finally | Literal_overflow of string | Polyvar_literal_overflow | Unknown_literal of string * char @@ -150,6 +152,9 @@ type recarg = Allowed | Required | Rejected patterns. A failed request must not leave them in another request. *) type request_state = { loop_depth: int ref; + (* Whether the innermost loop or function boundary is a finally block, which + break and continue must not leave. *) + in_finally: bool ref; newtype_level: int option ref; pattern_variables: (Ident.t * type_expr * string loc * Location.t * bool) list ref; @@ -161,6 +166,7 @@ type request_state = { let fresh_state () = { loop_depth = ref 0; + in_finally = ref false; newtype_level = ref None; pattern_variables = ref []; pattern_force = ref []; @@ -171,6 +177,7 @@ let fresh_state () = let state_key = Request_local.new_key fresh_state let state () = Domain.DLS.get state_key let loop_depth_ref () = (state ()).loop_depth +let in_finally_ref () = (state ()).in_finally let newtype_level_ref () = (state ()).newtype_level let pattern_variables_ref () = (state ()).pattern_variables let pattern_force_ref () = (state ()).pattern_force @@ -182,10 +189,17 @@ let with_depth depth_ref f = depth_ref := saved + 1; Misc.try_finally f (fun () -> depth_ref := saved) -let with_reset_control_flow f = +let with_control_flow_boundary ~in_finally f = let saved_loop_depth = !(loop_depth_ref ()) in + let saved_in_finally = !(in_finally_ref ()) in loop_depth_ref () := 0; - Misc.try_finally f (fun () -> loop_depth_ref () := saved_loop_depth) + in_finally_ref () := in_finally; + Misc.try_finally f (fun () -> + loop_depth_ref () := saved_loop_depth; + in_finally_ref () := saved_in_finally) + +(* A function body starts without enclosing loops. *) +let with_reset_control_flow f = with_control_flow_boundary ~in_finally:false f let case lhs rhs = {c_lhs = lhs; c_guard = None; c_rhs = rhs} @@ -213,9 +227,13 @@ let iter_expression f e = | Pexp_let (_, pel, e) -> expr e; List.iter binding pel - | Pexp_match (e, pel) | Pexp_try (e, pel) -> + | Pexp_match (e, pel) -> expr e; List.iter case pel + | Pexp_try (e, pel, finally) -> + expr e; + List.iter case pel; + Option.iter expr finally | Pexp_array args | Pexp_tuple args | Pexp_construct (_, {txt = args}) @@ -1940,7 +1958,7 @@ let rec final_subexpression sexp = | Pexp_braces {expr = e} -> final_subexpression e | Pexp_let (_, _, e) | Pexp_sequence (_, e) - | Pexp_try (e, _) + | Pexp_try (e, _, _) | Pexp_ifthenelse (_, e, _) | Pexp_ternary (_, e, _) | Pexp_match (_, {pc_rhs = e} :: _) -> @@ -2140,7 +2158,7 @@ let rec type_approx env sexp = params, type_approx env body )) | Pexp_match (_, {pc_rhs = e} :: _) -> type_approx env e - | Pexp_try (e, _) -> type_approx env e + | Pexp_try (e, _, _) -> type_approx env e | Pexp_tuple l -> newty (Ttuple (List.map (type_approx env) l)) | Pexp_ifthenelse (_, e, _) -> type_approx env e | Pexp_ternary (_, e, _) -> type_approx env e @@ -2810,15 +2828,25 @@ and type_expect_ ?deprecated_context ~context ?(recarg = Rejected) env sexp exp_attributes = sexp.pexp_attributes; exp_env = env; } - | Pexp_try (sbody, caselist) -> + | Pexp_try (sbody, caselist, sfinally) -> let body = type_expect ~context:None env sbody ty_expected in let cases, _ = type_cases ~call_context:`Try env (Predef.type_exn ()) ty_expected false loc caselist in + (* JavaScript runs the finally block after the body or catch has already + decided how the try is left, so a break or continue there would cancel + a pending exception or result. *) + let finally = + Option.map + (fun sfinally -> + with_control_flow_boundary ~in_finally:true (fun () -> + type_statement ~context:None env sfinally)) + sfinally + in re { - exp_desc = Texp_try (body, cases); + exp_desc = Texp_try (body, cases, finally); exp_loc = loc; exp_extra = []; exp_type = body.exp_type; @@ -3238,7 +3266,12 @@ and type_expect_ ?deprecated_context ~context ?(recarg = Rejected) env sexp } | Pexp_break -> if !(loop_depth_ref ()) = 0 then - raise (Error (loc, env, Break_outside_loop)) + raise + (Error + ( loc, + env, + if !(in_finally_ref ()) then Break_out_of_finally + else Break_outside_loop )) else rue { @@ -3251,7 +3284,12 @@ and type_expect_ ?deprecated_context ~context ?(recarg = Rejected) env sexp } | Pexp_continue -> if !(loop_depth_ref ()) = 0 then - raise (Error (loc, env, Continue_outside_loop)) + raise + (Error + ( loc, + env, + if !(in_finally_ref ()) then Continue_out_of_finally + else Continue_outside_loop )) else rue { @@ -5379,6 +5417,14 @@ let report_error env loc ppf error = fprintf ppf "`break` can only be used directly inside a loop body." | Continue_outside_loop -> fprintf ppf "`continue` can only be used inside a loop body." + | Break_out_of_finally -> + fprintf ppf + "`break` cannot leave a `finally` block. Only a loop inside the block \ + can be broken out of." + | Continue_out_of_finally -> + fprintf ppf + "`continue` cannot leave a `finally` block. Only a loop inside the block \ + can be continued." | Literal_overflow ty -> fprintf ppf "Integer literal exceeds the range of representable integers of type %s" diff --git a/compiler/ml/typecore.mli b/compiler/ml/typecore.mli index 330b0b93ecf..a25c08f9e9e 100644 --- a/compiler/ml/typecore.mli +++ b/compiler/ml/typecore.mli @@ -95,6 +95,8 @@ type error = | Inlined_record_expected | Break_outside_loop | Continue_outside_loop + | Break_out_of_finally + | Continue_out_of_finally | Literal_overflow of string | Polyvar_literal_overflow | Unknown_literal of string * char diff --git a/compiler/ml/typedtree.ml b/compiler/ml/typedtree.ml index 688dc72836b..aaff3f86332 100644 --- a/compiler/ml/typedtree.ml +++ b/compiler/ml/typedtree.ml @@ -104,7 +104,7 @@ and expression_desc = transformed_jsx: bool; } | Texp_match of expression * case list * case list * partial - | Texp_try of expression * case list + | Texp_try of expression * case list * expression option | Texp_tuple of expression list | Texp_construct of Longident.t loc * constructor_description * expression list diff --git a/compiler/ml/typedtree.mli b/compiler/ml/typedtree.mli index 98b5295ebea..54a7e69a766 100644 --- a/compiler/ml/typedtree.mli +++ b/compiler/ml/typedtree.mli @@ -204,8 +204,11 @@ and expression_desc = [Texp_match (E0, [(P1, E1); (P2, E2)], [(P3, E3)], _)] *) - | Texp_try of expression * case list - (** try E with P1 -> E1 | ... | PN -> EN *) + | Texp_try of expression * case list * expression option + (** try E catch { | P1 => E1 | ... | PN => EN } finally E_fin + + The case list is empty when there is no catch, and then there is + always a finally expression. *) | Texp_tuple of expression list (** (E1, ..., EN) *) | Texp_construct of Longident.t loc * constructor_description * expression list diff --git a/compiler/ml/typedtree_iter.ml b/compiler/ml/typedtree_iter.ml index a748bff361d..0962b6e5a07 100644 --- a/compiler/ml/typedtree_iter.ml +++ b/compiler/ml/typedtree_iter.ml @@ -241,9 +241,10 @@ end = struct iter_expression exp; iter_cases list1; iter_cases list2 - | Texp_try (exp, list) -> + | Texp_try (exp, list, finally) -> iter_expression exp; - iter_cases list + iter_cases list; + Option.iter iter_expression finally | Texp_tuple list -> List.iter iter_expression list | Texp_construct (_, _, args) -> List.iter iter_expression args | Texp_variant (_label, expo) -> ( diff --git a/compiler/syntax/src/res_comments_table.ml b/compiler/syntax/src/res_comments_table.ml index f69fcf96894..fd7de0fa982 100644 --- a/compiler/syntax/src/res_comments_table.ml +++ b/compiler/syntax/src/res_comments_table.ml @@ -1012,6 +1012,24 @@ and walk_value_binding vb t comments = let comments = walk_pattern vb.pvb_pat comments in walk_expression_after vb.pvb_pat.ppat_loc vb.pvb_expr comments +(* Walks the scrutinee of a switch or the body of a try, and returns the + comments after it. *) +and walk_switch_or_try_head (expr : Parsetree.expression) t comments = + let before, inside, after = partition_by_loc comments expr.pexp_loc in + let after = + if is_block_expr expr then ( + let after_expr, rest = partition_adjacent_trailing expr.pexp_loc after in + walk_expression expr t (List.concat [before; inside; after_expr]); + rest) + else ( + attach t.leading expr.pexp_loc before; + walk_expression expr t inside; + after) + in + let after_expr, rest = partition_adjacent_trailing expr.pexp_loc after in + attach t.trailing expr.pexp_loc after_expr; + rest + and walk_expression expr t comments = let open Location in let walk_apply_expr call_expr arguments t comments = @@ -1421,23 +1439,37 @@ and walk_expression expr t comments = attach t.leading mod_expr.pmod_loc before; walk_module_expr mod_expr t inside; attach t.trailing mod_expr.pmod_loc after - | Pexp_match (expr, cases) | Pexp_try (expr, cases) -> - let before, inside, after = partition_by_loc comments expr.pexp_loc in - let after = - if is_block_expr expr then ( - let after_expr, rest = - partition_adjacent_trailing expr.pexp_loc after + | Pexp_match (expr, cases) | Pexp_try (expr, cases, None) -> + let rest = walk_switch_or_try_head expr t comments in + walk_list (cases |> List.map (fun case -> Case case)) t rest + | Pexp_try (expr, cases, Some finally) -> + let before_finally, inside, after = + partition_by_loc comments finally.pexp_loc + in + let rest = walk_switch_or_try_head expr t before_finally in + (* The finally location starts at its first expression, so comments + between the cases and that expression lead the finally block. *) + let rest = + match (cases, List.rev cases) with + | first :: _, last :: _ -> + let cases_loc = + {first.pc_lhs.ppat_loc with loc_end = last.pc_rhs.pexp_loc.loc_end} in - walk_expression expr t (List.concat [before; inside; after_expr]); - rest) - else ( - attach t.leading expr.pexp_loc before; - walk_expression expr t inside; - after) + let before, inside, after = partition_by_loc rest cases_loc in + let after_cases, rest = partition_adjacent_trailing cases_loc after in + walk_list + (cases |> List.map (fun case -> Case case)) + t + (List.concat [before; inside; after_cases]); + rest + | _ -> rest in - let after_expr, rest = partition_adjacent_trailing expr.pexp_loc after in - attach t.trailing expr.pexp_loc after_expr; - walk_list (cases |> List.map (fun case -> Case case)) t rest + attach t.leading finally.pexp_loc rest; + if is_block_expr finally then + walk_expression finally t (List.concat [inside; after]) + else ( + walk_expression finally t inside; + attach t.trailing finally.pexp_loc after) (* unary expression: todo use parsetreeviewer *) | Pexp_dict entries when Res_parsetree_viewer.dict_has_spread entries -> (* The rows themselves, not the array of each run of rows, which the diff --git a/compiler/syntax/src/res_core.ml b/compiler/syntax/src/res_core.ml index 222cffc834d..76f86ca88d4 100644 --- a/compiler/syntax/src/res_core.ml +++ b/compiler/syntax/src/res_core.ml @@ -3562,12 +3562,38 @@ and parse_try_expression p = let start_pos = Parser.start_pos p in Parser.expect Try p; let expr = parse_expr ~context:WhenExpr p in - Parser.expect Res_token.catch p; - Parser.expect Lbrace p; - let cases = parse_pattern_matching p in - Parser.expect Rbrace p; + let cases, finally = + if Parser.peek p = Res_token.finally then ([], Some (parse_finally_block p)) + else ( + Parser.expect Res_token.catch p; + Parser.expect Lbrace p; + let cases = parse_pattern_matching p in + Parser.expect Rbrace p; + (* [finally] is not a keyword. After a catch it only starts a finally + block when a brace follows, so a statement after the try can still + use a [finally] identifier. *) + let finally = + if Parser.peek p = Res_token.finally && Parser.peek2 p = Lbrace then + Some (parse_finally_block p) + else None + in + (cases, finally)) + in let loc = mk_loc start_pos (Parser.position p) in - Ast_helper.Exp.try_ ~loc expr cases + Ast_helper.Exp.try_ ~loc ?finally expr cases + +and parse_finally_block p = + if Parser.peek2 p <> Lbrace then + Parser.err p + (Diagnostics.message "A `finally` block needs braces: `finally { ... }`"); + Parser.next p; + match Parser.peek p with + | Lbrace -> + Parser.next p; + let finally = parse_expr_block p in + Parser.expect Rbrace p; + finally + | _ -> parse_expr p and parse_if_condition p = Parser.leave_breadcrumb p Grammar.IfCondition; diff --git a/compiler/syntax/src/res_printer.ml b/compiler/syntax/src/res_printer.ml index df1026e17fe..7a8ece8c4e9 100644 --- a/compiler/syntax/src/res_printer.ml +++ b/compiler/syntax/src/res_printer.ml @@ -3796,7 +3796,7 @@ and print_expression ~state (e : Parsetree.expression) cmt_tbl = ]) | Pexp_sequence _ -> print_expression_block ~state ~braces:true e cmt_tbl | Pexp_let _ -> print_expression_block ~state ~braces:true e cmt_tbl - | Pexp_try (expr, cases) -> + | Pexp_try (expr, cases, finally) -> let expr_doc = let doc = print_expression_with_comments ~state expr cmt_tbl in match Parens.expr expr with @@ -3804,13 +3804,23 @@ and print_expression ~state (e : Parsetree.expression) cmt_tbl = | Braced braces -> print_braces doc expr braces | Nothing -> doc in - Doc.concat - [ - Doc.text "try "; - expr_doc; - Doc.text " catch "; - print_cases ~state cases cmt_tbl; - ] + let catch_doc = + match cases with + | [] -> Doc.nil + | _ :: _ -> + Doc.concat [Doc.text " catch "; print_cases ~state cases cmt_tbl] + in + let finally_doc = + match finally with + | None -> Doc.nil + | Some finally -> + Doc.concat + [ + Doc.text " finally "; + print_expression_block ~state ~braces:true finally cmt_tbl; + ] + in + Doc.concat [Doc.text "try "; expr_doc; catch_doc; finally_doc] | Pexp_match (expr, cases) -> let expr_doc = let doc = print_expression_with_comments ~state expr cmt_tbl in diff --git a/compiler/syntax/src/res_token.ml b/compiler/syntax/src/res_token.ml index 92e02afcbf3..b8639efaed9 100644 --- a/compiler/syntax/src/res_token.ml +++ b/compiler/syntax/src/res_token.ml @@ -288,3 +288,4 @@ let is_keyword_txt str = with Not_found -> false let catch = Lident "catch" +let finally = Lident "finally" diff --git a/tests/analysis_tests/tests-reanalyze/deadcode/expected/exception.txt b/tests/analysis_tests/tests-reanalyze/deadcode/expected/exception.txt index a817b7bc5b2..1c9b93d1877 100644 --- a/tests/analysis_tests/tests-reanalyze/deadcode/expected/exception.txt +++ b/tests/analysis_tests/tests-reanalyze/deadcode/expected/exception.txt @@ -112,8 +112,16 @@ ExnA.res:1:5-7 bar might throw Not_found (ExnA.res:1:16) and is not annotated with @throws(Not_found) + Exception Analysis + ExnFinally.res:2:5-24 + throwsThroughFinally might throw Not_found (ExnFinally.res:3:6) and is not annotated with @throws(Not_found) + + Exception Analysis + ExnFinally.res:7:5-19 + throwsInFinally might throw Not_found (ExnFinally.res:11:4) and is not annotated with @throws(Not_found) + Exception Analysis ExternalTest.res:7:5-24 bigIntFromStringExn2 might throw JsExn (ExternalTest.res:7:35) and is not annotated with @throws(JsExn) - Analysis reported 29 issues (Exception Analysis:29) + Analysis reported 31 issues (Exception Analysis:31) diff --git a/tests/analysis_tests/tests-reanalyze/deadcode/src/exception/ExnFinally.res b/tests/analysis_tests/tests-reanalyze/deadcode/src/exception/ExnFinally.res new file mode 100644 index 00000000000..cf3be36dfe4 --- /dev/null +++ b/tests/analysis_tests/tests-reanalyze/deadcode/src/exception/ExnFinally.res @@ -0,0 +1,19 @@ +// A finally block without catch handles no exception +let throwsThroughFinally = () => + try throw(Not_found) finally { + () + } + +let throwsInFinally = () => + try () catch { + | _ => () + } finally { + throw(Not_found) + } + +let catchesBeforeFinally = () => + try throw(Not_found) catch { + | Not_found => () + } finally { + () + } diff --git a/tests/build_tests/super_errors/expected/break_out_of_finally.res.expected b/tests/build_tests/super_errors/expected/break_out_of_finally.res.expected new file mode 100644 index 00000000000..a93de0e4bb4 --- /dev/null +++ b/tests/build_tests/super_errors/expected/break_out_of_finally.res.expected @@ -0,0 +1,11 @@ + + We've found a bug for you! + /.../fixtures/break_out_of_finally.res:6:7-11 + + 4 ┆ () + 5 ┆ } finally { + 6 ┆ break + 7 ┆ } + 8 ┆ } + + `break` cannot leave a `finally` block. Only a loop inside the block can be broken out of. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/continue_out_of_finally.res.expected b/tests/build_tests/super_errors/expected/continue_out_of_finally.res.expected new file mode 100644 index 00000000000..cd247d068ab --- /dev/null +++ b/tests/build_tests/super_errors/expected/continue_out_of_finally.res.expected @@ -0,0 +1,11 @@ + + We've found a bug for you! + /.../fixtures/continue_out_of_finally.res:8:7-14 + + 6 ┆ | _ => () + 7 ┆ } finally { + 8 ┆ continue + 9 ┆ } + 10 ┆ } + + `continue` cannot leave a `finally` block. Only a loop inside the block can be continued. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/finally_non_unit.res.expected b/tests/build_tests/super_errors/expected/finally_non_unit.res.expected new file mode 100644 index 00000000000..49ff1bf760d --- /dev/null +++ b/tests/build_tests/super_errors/expected/finally_non_unit.res.expected @@ -0,0 +1,15 @@ + + We've found a bug for you! + /.../fixtures/finally_non_unit.res:7:5-13 + + 5 ┆ 1 + 6 ┆ } finally { + 7 ┆ compute() + 8 ┆ } + 9 ┆ + + This function call returns: int + But it's expected to return: unit + + - Did you mean to assign this to a variable? + - If you don't care about the result of this expression, you can assign it to _ via let _ = ... or pipe it to ignore via expression->ignore \ No newline at end of file diff --git a/tests/build_tests/super_errors/fixtures/break_out_of_finally.res b/tests/build_tests/super_errors/fixtures/break_out_of_finally.res new file mode 100644 index 00000000000..ab8d75b014a --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/break_out_of_finally.res @@ -0,0 +1,8 @@ +let f = () => + while true { + try { + () + } finally { + break + } + } diff --git a/tests/build_tests/super_errors/fixtures/continue_out_of_finally.res b/tests/build_tests/super_errors/fixtures/continue_out_of_finally.res new file mode 100644 index 00000000000..af3d383e8c6 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/continue_out_of_finally.res @@ -0,0 +1,10 @@ +let f = () => + for _ in 0 to 3 { + try { + () + } catch { + | _ => () + } finally { + continue + } + } diff --git a/tests/build_tests/super_errors/fixtures/finally_non_unit.res b/tests/build_tests/super_errors/fixtures/finally_non_unit.res new file mode 100644 index 00000000000..8eec11d400f --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/finally_non_unit.res @@ -0,0 +1,8 @@ +let compute = () => 1 + +let f = () => + try { + 1 + } finally { + compute() + } diff --git a/tests/ounit_tests/ounit_ast_mapper0_tests.ml b/tests/ounit_tests/ounit_ast_mapper0_tests.ml index 8c758807af1..bf4a9de4142 100644 --- a/tests/ounit_tests/ounit_ast_mapper0_tests.ml +++ b/tests/ounit_tests/ounit_ast_mapper0_tests.ml @@ -442,6 +442,33 @@ let test_ternary_roundtrips_through_ast0 _ = OUnit.assert_equal ["before"; "after"] (attr_names round_tripped.pexp_attributes) +let test_try_finally_roundtrips_through_ast0 _ = + let ident name = + Ast_helper.Exp.ident ~loc (Location.mknoloc (Longident.Lident name)) + in + let finally = + Ast_helper.Exp.ident ~loc:(source_loc 30 37) + (Location.mknoloc (Longident.Lident "cleanup")) + in + let try_ = + Ast_helper.Exp.try_ ~loc + ~attrs:[attr "before" (Parsetree.PStr [])] + ~finally (ident "body") [] + in + let wire = to_expr0 try_ in + (match wire.pexp_desc with + | Parsetree0.Pexp_try (_, []) -> () + | _ -> assert_failure "Expected a v0 try without cases"); + let attr_names attrs = List.map (fun ({Location.txt}, _) -> txt) attrs in + OUnit.assert_equal ["_res.finally"; "before"] + (attr_names wire.pexp_attributes); + let round_tripped = map_expr0 wire in + match round_tripped.pexp_desc with + | Parsetree.Pexp_try (_, [], Some round_tripped_finally) -> + OUnit.assert_equal ["before"] (attr_names round_tripped.pexp_attributes); + OUnit.assert_equal finally.pexp_loc round_tripped_finally.pexp_loc + | _ -> assert_failure "Expected a try with a finally after the v0 roundtrip" + let test_v0_ternary_marker_preserves_other_attribute_order _ = let ident name = Ast_helper0.Exp.ident ~loc (Location.mknoloc (Longident.Lident name)) @@ -2637,6 +2664,8 @@ let suites = >:: test_string_literals_roundtrip_through_ast0; "removed_regexp_extension" >:: test_removed_regexp_extension; "regexp_parser_locations" >:: test_regexp_parser_locations; + "try_finally_roundtrips_through_ast0" + >:: test_try_finally_roundtrips_through_ast0; "regexp_roundtrip_through_ast0" >:: test_regexp_roundtrip_through_ast0; "ppx_regexp_payloads" >:: test_ppx_regexp_payloads; "malformed_ppx_regexp_payloads" >:: test_malformed_ppx_regexp_payloads; diff --git a/tests/syntax_tests/data/ast-mapping/TryExpressions.res b/tests/syntax_tests/data/ast-mapping/TryExpressions.res new file mode 100644 index 00000000000..754bf7a69d6 --- /dev/null +++ b/tests/syntax_tests/data/ast-mapping/TryExpressions.res @@ -0,0 +1,42 @@ +let tryCatch = () => + try { + dangerous() + } catch { + | Not_found => 0 + } + +let tryFinally = () => + try { + dangerous() + } finally { + cleanup() + } + +let tryCatchFinally = () => + try { + dangerous() + } catch { + | Not_found => 0 + } finally { + cleanup() + } + +// The finally block keeps its attributes and the try keeps its own +let attributed = () => + @attr + try { + dangerous() + } finally { + @inner cleanup() + } + +let nested = () => + try { + try { + dangerous() + } finally { + inner() + } + } finally { + outer() + } diff --git a/tests/syntax_tests/data/ast-mapping/expected/TryExpressions.res.txt b/tests/syntax_tests/data/ast-mapping/expected/TryExpressions.res.txt new file mode 100644 index 00000000000..0ffead0fb5d --- /dev/null +++ b/tests/syntax_tests/data/ast-mapping/expected/TryExpressions.res.txt @@ -0,0 +1,43 @@ +let tryCatch = () => + try { + dangerous() + } catch { + | Not_found => 0 + } + +let tryFinally = () => + try { + dangerous() + } finally { + cleanup() + } + +let tryCatchFinally = () => + try { + dangerous() + } catch { + | Not_found => 0 + } finally { + cleanup() + } + +// The finally block keeps its attributes and the try keeps its own +let attributed = () => + @attr + try { + dangerous() + } finally { + @inner + cleanup() + } + +let nested = () => + try { + try { + dangerous() + } finally { + inner() + } + } finally { + outer() + } diff --git a/tests/syntax_tests/data/parsing/errors/expressions/expected/tryFinally.res.txt b/tests/syntax_tests/data/parsing/errors/expressions/expected/tryFinally.res.txt new file mode 100644 index 00000000000..fb114ecd4ef --- /dev/null +++ b/tests/syntax_tests/data/parsing/errors/expressions/expected/tryFinally.res.txt @@ -0,0 +1,24 @@ + + Syntax error! + syntax_tests/data/parsing/errors/expressions/tryFinally.res:1:29-35 + + 1 │ let a = try dangerousCall() finally cleanup() + 2 │ + 3 │ let b = try { + + A `finally` block needs braces: `finally { ... }` + + + Syntax error! + syntax_tests/data/parsing/errors/expressions/tryFinally.res:7:2-9 + + 5 │ } catch { + 6 │ | _ => () + 7 │ } finally + 8 │ + + Consecutive statements on a line must be separated by ';' or a newline + +let a = try dangerousCall () finally cleanup () +let b = try {dangerousCall ()} with | _ -> () +;;finally \ No newline at end of file diff --git a/tests/syntax_tests/data/parsing/errors/expressions/tryFinally.res b/tests/syntax_tests/data/parsing/errors/expressions/tryFinally.res new file mode 100644 index 00000000000..ad7669ae98a --- /dev/null +++ b/tests/syntax_tests/data/parsing/errors/expressions/tryFinally.res @@ -0,0 +1,7 @@ +let a = try dangerousCall() finally cleanup() + +let b = try { + dangerousCall() +} catch { +| _ => () +} finally diff --git a/tests/syntax_tests/data/parsing/grammar/expressions/expected/try.res.txt b/tests/syntax_tests/data/parsing/grammar/expressions/expected/try.res.txt index 02a9ff710bb..62fcf1307b5 100644 --- a/tests/syntax_tests/data/parsing/grammar/expressions/expected/try.res.txt +++ b/tests/syntax_tests/data/parsing/grammar/expressions/expected/try.res.txt @@ -2,4 +2,10 @@ with | Foo -> Console.log {js|catched Foo|js} | Exit -> Console.log {js|catched exit|js} ;;try myDangerousFn () with | Foo -> Console.log {js|catched Foo|js}[@@attr ] -let x = {let y = 1 in try {apply y} with | _ -> 2} \ No newline at end of file +let x = {let y = 1 in try {apply y} with | _ -> 2} +;;try {dangerousCall ()} finally cleanup () +;;try dangerousCall () with | Foo -> Console.log {js|catched Foo|js} + finally cleanup () +let x = try 1 finally () +;;try dangerousCall () with | _ -> () +;;finally x \ No newline at end of file diff --git a/tests/syntax_tests/data/parsing/grammar/expressions/try.res b/tests/syntax_tests/data/parsing/grammar/expressions/try.res index bc356733775..7e9f627f497 100644 --- a/tests/syntax_tests/data/parsing/grammar/expressions/try.res +++ b/tests/syntax_tests/data/parsing/grammar/expressions/try.res @@ -20,3 +20,23 @@ let x = { | _ => 2 } } + +try { + dangerousCall() +} finally { + cleanup() +} + +try dangerousCall() catch { +| Foo => Console.log("catched Foo") +} finally { + cleanup() +} + +let x = try 1 finally {()} + +// `finally` stays an identifier unless a block follows it +try dangerousCall() catch { +| _ => () +} +finally(x) diff --git a/tests/syntax_tests/data/printer/comments/expected/expr.res.txt b/tests/syntax_tests/data/printer/comments/expected/expr.res.txt index 99434ca5865..848e546b303 100644 --- a/tests/syntax_tests/data/printer/comments/expected/expr.res.txt +++ b/tests/syntax_tests/data/printer/comments/expected/expr.res.txt @@ -211,6 +211,26 @@ for await /* c0 */ x /* c1 */ of /* c2 */ xs /* c3 */ { /* before */ | InfiniteLoop2 /* after */ => /* here */ () /* trailing */ } // trailing +/* c0 */ try { + /* c1 */ + // in body + dangerous() /* c2 */ +} finally { + /* c3 */ + // in finally + cleanup() /* c4 */ +} // trailing finally + +try { + dangerous() +} catch { +// Above +/* before */ | Exit /* after */ => /* here */ () /* trailing */ +} finally { + /* leading */ cleanup() // trailing cleanup + // last +} + // Pexp_fun let f = (/* c0 */ a /* c1 */, /* c2 */ b /* c3 */): /* c4 */ int /* c5 */ => /* c6 */ 20 /* c7 */ let multiply = (/* c0 */ m1 /* c1 */, /* c2 */ m2 /* c3 */) => { diff --git a/tests/syntax_tests/data/printer/comments/expr.res b/tests/syntax_tests/data/printer/comments/expr.res index b3c62347391..d8452eefec0 100644 --- a/tests/syntax_tests/data/printer/comments/expr.res +++ b/tests/syntax_tests/data/printer/comments/expr.res @@ -213,6 +213,24 @@ for await /* c0 */ x /* c1 */ of /* c2 */ xs /* c3 */ { | /* before */ InfiniteLoop2 /* after */ => /* here */ () /* trailing */ } // trailing +/* c0 */ try /* c1 */ { + // in body + dangerous() /* c2 */ +} /* c3 */ finally { + // in finally + cleanup() /* c4 */ +} // trailing finally + +try { + dangerous() +} catch { +// Above +| /* before */ Exit /* after */ => /* here */ () /* trailing */ +} finally { + /* leading */ cleanup() // trailing cleanup + // last +} + // Pexp_fun let f = (/* c0 */ a /* c1 */, /* c2 */ b /* c3 */) : /* c4 */int /* c5 */ => /* c6 */ 20 /* c7 */ let multiply = (/* c0 */ m1 /* c1 */, /* c2 */ m2 /* c3 */) => { diff --git a/tests/syntax_tests/data/printer/expr/expected/try.res.txt b/tests/syntax_tests/data/printer/expr/expected/try.res.txt index ec619723d13..70659483b27 100644 --- a/tests/syntax_tests/data/printer/expr/expected/try.res.txt +++ b/tests/syntax_tests/data/printer/expr/expected/try.res.txt @@ -30,3 +30,70 @@ let () = try myDangerousFn() catch { | Foo => Console.log() } + +try { + dangerousCall() +} finally { + cleanup() +} + +try dangerousCall() finally { + cleanup() +} + +try { + let x = 1 + dangerousCall(x) +} catch { +| Foo => Console.log("foo") +| Exit => Console.log("exit") +} finally { + cleanup() + Console.log("done") +} + +let result = try { + compute() +} catch { +| _ => 0 +} finally { + cleanup() +} + +let x = try 1 finally { + () +} + +let nested = try { + try { + inner() + } finally { + innerCleanup() + } +} finally { + outerCleanup() +} + +let f = async () => + try { + await fetchData() + } catch { + | _ => None + } finally { + setLoading(false) + } + +@attr +try myDangerousFn() finally { + cleanup() +} + +let finally = x => x +let afterTry = () => { + try { + () + } catch { + | _ => () + } + finally(1) +} diff --git a/tests/syntax_tests/data/printer/expr/try.res b/tests/syntax_tests/data/printer/expr/try.res index 710882a5cb3..e5ba2fde04e 100644 --- a/tests/syntax_tests/data/printer/expr/try.res +++ b/tests/syntax_tests/data/printer/expr/try.res @@ -31,3 +31,66 @@ let () = try myDangerousFn() catch { | Foo => Console.log() } + +try { + dangerousCall() +} finally { + cleanup() +} + +try dangerousCall() finally { + cleanup() +} + +try { + let x = 1 + dangerousCall(x) +} catch { +| Foo => Console.log("foo") +| Exit => Console.log("exit") +} finally { + cleanup() + Console.log("done") +} + +let result = try { + compute() +} catch { +| _ => 0 +} finally {cleanup()} + +let x = try 1 finally {()} + +let nested = try { + try { + inner() + } finally { + innerCleanup() + } +} finally { + outerCleanup() +} + +let f = async () => + try { + await fetchData() + } catch { + | _ => None + } finally { + setLoading(false) + } + +@attr +try myDangerousFn() finally { + cleanup() +} + +let finally = x => x +let afterTry = () => { + try { + () + } catch { + | _ => () + } + finally(1) +} diff --git a/tests/tests/src/try_finally_test.mjs b/tests/tests/src/try_finally_test.mjs new file mode 100644 index 00000000000..d49c27b1894 --- /dev/null +++ b/tests/tests/src/try_finally_test.mjs @@ -0,0 +1,339 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + +import * as Mocha from "mocha"; +import * as Test_utils from "./test_utils.mjs"; +import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.mjs"; + +let Boom = /* @__PURE__ */Primitive_exceptions.create("Try_finally_test.Boom"); + +Mocha.describe("Try_finally_test", () => { + Mocha.test("finally runs after the body", () => { + let log = []; + let result; + try { + log.push("body"); + result = 1; + } finally { + log.push("finally"); + } + Test_utils.eq("File \"try_finally_test.res\", line 18, characters 7-14", [ + "body", + "finally" + ], log); + Test_utils.eq("File \"try_finally_test.res\", line 19, characters 7-14", 1, result); + }); + Mocha.test("finally runs when the body throws", () => { + let log = []; + let run = () => { + try { + log.push("body"); + throw { + RE_EXN_ID: Boom, + _1: 1, + Error: new Error() + }; + } finally { + log.push("finally"); + } + }; + let caught; + try { + caught = run(); + } catch (raw_n) { + let n = Primitive_exceptions.internalToException(raw_n); + if (n.RE_EXN_ID === Boom) { + caught = n._1; + } else { + throw n; + } + } + Test_utils.eq("File \"try_finally_test.res\", line 34, characters 7-14", [ + "body", + "finally" + ], log); + Test_utils.eq("File \"try_finally_test.res\", line 35, characters 7-14", 1, caught); + }); + Mocha.test("catch runs before finally", () => { + let log = []; + let result; + try { + log.push("body"); + throw { + RE_EXN_ID: Boom, + _1: 2, + Error: new Error() + }; + } catch (raw_n) { + let n = Primitive_exceptions.internalToException(raw_n); + if (n.RE_EXN_ID === Boom) { + log.push("catch"); + result = n._1; + } else { + throw n; + } + } finally { + log.push("finally"); + } + Test_utils.eq("File \"try_finally_test.res\", line 50, characters 7-14", [ + "body", + "catch", + "finally" + ], log); + Test_utils.eq("File \"try_finally_test.res\", line 51, characters 7-14", 2, result); + }); + Mocha.test("finally runs when the catch handler throws", () => { + let log = []; + let run = () => { + try { + throw { + RE_EXN_ID: Boom, + _1: 3, + Error: new Error() + }; + } catch (raw_n) { + let n = Primitive_exceptions.internalToException(raw_n); + if (n.RE_EXN_ID === Boom) { + throw { + RE_EXN_ID: Boom, + _1: n._1 + 1 | 0, + Error: new Error() + }; + } + throw n; + } finally { + log.push("finally"); + } + }; + let caught; + try { + caught = run(); + } catch (raw_n) { + let n = Primitive_exceptions.internalToException(raw_n); + if (n.RE_EXN_ID === Boom) { + caught = n._1; + } else { + throw n; + } + } + Test_utils.eq("File \"try_finally_test.res\", line 67, characters 7-14", ["finally"], log); + Test_utils.eq("File \"try_finally_test.res\", line 68, characters 7-14", 4, caught); + }); + Mocha.test("an exception the catch does not match passes through finally", () => { + let log = []; + let run = () => { + try { + throw { + RE_EXN_ID: Boom, + _1: 5, + Error: new Error() + }; + } catch (raw_exn) { + let exn = Primitive_exceptions.internalToException(raw_exn); + if (exn.RE_EXN_ID === "Not_found") { + return 0; + } + throw exn; + } finally { + log.push("finally"); + } + }; + let caught; + try { + caught = run(); + } catch (raw_n) { + let n = Primitive_exceptions.internalToException(raw_n); + if (n.RE_EXN_ID === Boom) { + caught = n._1; + } else { + throw n; + } + } + Test_utils.eq("File \"try_finally_test.res\", line 84, characters 7-14", ["finally"], log); + Test_utils.eq("File \"try_finally_test.res\", line 85, characters 7-14", 5, caught); + }); + Mocha.test("finally runs on break and continue", () => { + let log = []; + for (let i = 0; i <= 5; ++i) { + try { + if (i === 1) { + continue; + } + if (i === 3) { + break; + } + log.push(i.toString()); + } finally { + log.push("f" + i.toString()); + } + } + Test_utils.eq("File \"try_finally_test.res\", line 103, characters 7-14", [ + "0", + "f0", + "f1", + "2", + "f2", + "f3" + ], log); + }); + Mocha.test("finally runs on break from a switch inside the try", () => { + let log = []; + let i = 0; + loop_0: while (i < 5) { + i = i + 1 | 0; + try { + let n = i; + switch (n) { + case 2 : + continue loop_0; + case 4 : + break loop_0; + default: + log.push(n.toString()); + } + } finally { + log.push("f" + i.toString()); + } + }; + Test_utils.eq("File \"try_finally_test.res\", line 121, characters 7-14", [ + "1", + "f1", + "f2", + "3", + "f3", + "f4" + ], log); + }); + Mocha.test("loops inside finally can use break", () => { + let log = []; + try { + log.push("body"); + } finally { + for (let i = 0; i <= 5; ++i) { + if (i === 2) { + break; + } + log.push(i.toString()); + } + } + Test_utils.eq("File \"try_finally_test.res\", line 136, characters 7-14", [ + "body", + "0", + "1" + ], log); + }); + Mocha.test("nested finally blocks run innermost first", () => { + let log = []; + let caught; + try { + try { + try { + throw { + RE_EXN_ID: Boom, + _1: 6, + Error: new Error() + }; + } finally { + log.push("inner"); + } + } finally { + log.push("outer"); + } + } catch (raw_n) { + let n = Primitive_exceptions.internalToException(raw_n); + if (n.RE_EXN_ID === Boom) { + caught = n._1; + } else { + throw n; + } + } + Test_utils.eq("File \"try_finally_test.res\", line 154, characters 7-14", [ + "inner", + "outer" + ], log); + Test_utils.eq("File \"try_finally_test.res\", line 155, characters 7-14", 6, caught); + }); + Mocha.test("recursion under finally keeps call order", () => { + let log = []; + let count = (n, retried) => { + try { + if (n > 0) { + return count(n - 1 | 0, retried); + } + if (retried) { + return 0; + } + throw { + RE_EXN_ID: Boom, + _1: n, + Error: new Error() + }; + } catch (raw_exn) { + let exn = Primitive_exceptions.internalToException(raw_exn); + if (exn.RE_EXN_ID === Boom) { + return count(n + 2 | 0, true); + } + throw exn; + } finally { + log.push(n); + } + }; + Test_utils.eq("File \"try_finally_test.res\", line 176, characters 7-14", 0, count(2, false)); + Test_utils.eq("File \"try_finally_test.res\", line 177, characters 7-14", [ + 0, + 1, + 2, + 0, + 1, + 2 + ], log); + }); + Mocha.test("finally runs after an awaited body", async () => { + let log = []; + let run = async () => { + try { + let value = await Promise.resolve(7); + log.push("body"); + return value; + } finally { + log.push("finally"); + } + }; + let result = await run(); + Test_utils.eq("File \"try_finally_test.res\", line 191, characters 7-14", [ + "body", + "finally" + ], log); + return Test_utils.eq("File \"try_finally_test.res\", line 192, characters 7-14", 7, result); + }); + Mocha.test("finally runs after a rejected await", async () => { + let log = []; + let run = async () => { + try { + return await Promise.reject({ + RE_EXN_ID: Boom, + _1: 8 + }); + } finally { + log.push("finally"); + } + }; + let caught; + try { + caught = await run(); + } catch (raw_n) { + let n = Primitive_exceptions.internalToException(raw_n); + if (n.RE_EXN_ID === Boom) { + caught = n._1; + } else { + throw n; + } + } + Test_utils.eq("File \"try_finally_test.res\", line 206, characters 7-14", ["finally"], log); + return Test_utils.eq("File \"try_finally_test.res\", line 207, characters 7-14", 8, caught); + }); +}); + +export { + Boom, +} +/* Not a pure module */ diff --git a/tests/tests/src/try_finally_test.res b/tests/tests/src/try_finally_test.res new file mode 100644 index 00000000000..ae01773de88 --- /dev/null +++ b/tests/tests/src/try_finally_test.res @@ -0,0 +1,209 @@ +open Mocha +open Test_utils + +@module("mocha") +external testAsync: (string, unit => promise) => unit = "test" + +exception Boom(int) + +describe(__MODULE__, () => { + test("finally runs after the body", () => { + let log = [] + let result = try { + log->Array.push("body") + 1 + } finally { + log->Array.push("finally") + } + eq(__LOC__, ["body", "finally"], log) + eq(__LOC__, 1, result) + }) + + test("finally runs when the body throws", () => { + let log = [] + let run = () => + try { + log->Array.push("body") + throw(Boom(1)) + } finally { + log->Array.push("finally") + } + let caught = try run() catch { + | Boom(n) => n + } + eq(__LOC__, ["body", "finally"], log) + eq(__LOC__, 1, caught) + }) + + test("catch runs before finally", () => { + let log = [] + let result = try { + log->Array.push("body") + throw(Boom(2)) + } catch { + | Boom(n) => + log->Array.push("catch") + n + } finally { + log->Array.push("finally") + } + eq(__LOC__, ["body", "catch", "finally"], log) + eq(__LOC__, 2, result) + }) + + test("finally runs when the catch handler throws", () => { + let log = [] + let run = () => + try { + throw(Boom(3)) + } catch { + | Boom(n) => throw(Boom(n + 1)) + } finally { + log->Array.push("finally") + } + let caught = try run() catch { + | Boom(n) => n + } + eq(__LOC__, ["finally"], log) + eq(__LOC__, 4, caught) + }) + + test("an exception the catch does not match passes through finally", () => { + let log = [] + let run = () => + try { + throw(Boom(5)) + } catch { + | Not_found => 0 + } finally { + log->Array.push("finally") + } + let caught = try run() catch { + | Boom(n) => n + } + eq(__LOC__, ["finally"], log) + eq(__LOC__, 5, caught) + }) + + test("finally runs on break and continue", () => { + let log = [] + for i in 0 to 5 { + try { + if i == 1 { + continue + } + if i == 3 { + break + } + log->Array.push(Int.toString(i)) + } finally { + log->Array.push("f" ++ Int.toString(i)) + } + } + eq(__LOC__, ["0", "f0", "f1", "2", "f2", "f3"], log) + }) + + test("finally runs on break from a switch inside the try", () => { + let log = [] + let i = ref(0) + while i.contents < 5 { + i := i.contents + 1 + try { + switch i.contents { + | 2 => continue + | 4 => break + | n => log->Array.push(Int.toString(n)) + } + } finally { + log->Array.push("f" ++ Int.toString(i.contents)) + } + } + eq(__LOC__, ["1", "f1", "f2", "3", "f3", "f4"], log) + }) + + test("loops inside finally can use break", () => { + let log = [] + try { + log->Array.push("body") + } finally { + for i in 0 to 5 { + if i == 2 { + break + } + log->Array.push(Int.toString(i)) + } + } + eq(__LOC__, ["body", "0", "1"], log) + }) + + test("nested finally blocks run innermost first", () => { + let log = [] + let caught = try { + try { + try { + throw(Boom(6)) + } finally { + log->Array.push("inner") + } + } finally { + log->Array.push("outer") + } + } catch { + | Boom(n) => n + } + eq(__LOC__, ["inner", "outer"], log) + eq(__LOC__, 6, caught) + }) + + // A self tail call under a finally must stay a real call: its finally + // blocks run after the recursive call returns, innermost first. + test("recursion under finally keeps call order", () => { + let log = [] + let rec count = (n, ~retried) => + try { + if n > 0 { + count(n - 1, ~retried) + } else if retried { + 0 + } else { + throw(Boom(n)) + } + } catch { + | Boom(_) => count(n + 2, ~retried=true) + } finally { + log->Array.push(n) + } + eq(__LOC__, 0, count(2, ~retried=false)) + eq(__LOC__, [0, 1, 2, 0, 1, 2], log) + }) + + testAsync("finally runs after an awaited body", async () => { + let log = [] + let run = async () => + try { + let value = await Promise.resolve(7) + log->Array.push("body") + value + } finally { + log->Array.push("finally") + } + let result = await run() + eq(__LOC__, ["body", "finally"], log) + eq(__LOC__, 7, result) + }) + + testAsync("finally runs after a rejected await", async () => { + let log = [] + let run = async () => + try { + await Promise.reject(Boom(8)) + } finally { + log->Array.push("finally") + } + let caught = try await run() catch { + | Boom(n) => n + } + eq(__LOC__, ["finally"], log) + eq(__LOC__, 8, caught) + }) +})