From 02b0c1189c77beea02e78f20c0dfcdfecd1aa537 Mon Sep 17 00:00:00 2001 From: Cristiano Calcagno Date: Mon, 14 Sep 2026 04:55:32 +0200 Subject: [PATCH 1/2] Add early return with provisional extension syntax Signed-off-by: Cristiano Calcagno --- CHANGELOG.md | 1 + analysis/reanalyze/src/side_effects.ml | 2 +- analysis/src/completion_front_end.ml | 3 + compiler/core/lam_analysis.ml | 14 +- compiler/core/lam_analysis.mli | 4 + compiler/core/lam_arity_analysis.ml | 2 +- compiler/core/lam_bounded_vars.ml | 1 + compiler/core/lam_check.ml | 2 + compiler/core/lam_closure.ml | 1 + compiler/core/lam_compile.ml | 45 +++- compiler/core/lam_exit_count.ml | 2 +- compiler/core/lam_hit.ml | 2 + compiler/core/lam_pass_collect.ml | 1 + compiler/core/lam_pass_count.ml | 1 + compiler/core/lam_pass_exits.ml | 1 + compiler/ext/config.ml | 2 +- compiler/frontend/bs_builtin_ppx.ml | 4 + compiler/ml/lambda.ml | 9 + compiler/ml/lambda.mli | 5 + compiler/ml/lambda_scc.ml | 1 + compiler/ml/lambda_traverse.ml | 7 +- compiler/ml/printlambda.ml | 1 + compiler/ml/printtyped.ml | 3 + compiler/ml/rec_check.ml | 7 +- compiler/ml/tast_iterator.ml | 1 + compiler/ml/tast_mapper.ml | 1 + compiler/ml/translcore.ml | 2 + compiler/ml/typecore.ml | 131 ++++++++-- compiler/ml/typecore.mli | 2 + compiler/ml/typedtree.ml | 1 + compiler/ml/typedtree.mli | 6 + compiler/ml/typedtree_iter.ml | 1 + tests/ERROR_VARIANTS.md | 7 +- .../analysis_tests/tests/src/EarlyReturn.res | 13 + .../tests/src/expected/EarlyReturn.res.txt | 55 ++++ .../return_async_mismatch.res.expected | 14 + .../return_existential_escape.res.expected | 13 + .../expected/return_in_default.res.expected | 10 + .../expected/return_in_module.res.expected | 11 + .../return_invalid_payload.res.expected | 8 + .../return_nested_type_mismatch.res.expected | 13 + .../return_outside_function.res.expected | 8 + .../return_sites_mismatch.res.expected | 14 + .../return_type_mismatch.res.expected | 14 + ...warning_21_return_unreachable.res.expected | 10 + .../fixtures/return_async_mismatch.res | 6 + .../fixtures/return_existential_escape.res | 6 + .../fixtures/return_in_default.res | 4 + .../fixtures/return_in_module.res | 6 + .../fixtures/return_invalid_payload.res | 1 + .../fixtures/return_nested_type_mismatch.res | 4 + .../fixtures/return_outside_function.res | 1 + .../fixtures/return_sites_mismatch.res | 6 + .../fixtures/return_type_mismatch.res | 6 + .../warning_21_return_unreachable.res | 4 + .../ounit_lambda_traverse_tests.ml | 23 ++ .../data/ast-mapping/EarlyReturn.res | 7 + .../ast-mapping/expected/EarlyReturn.res.txt | 7 + tests/tests/src/early_return_helper.mjs | 14 + tests/tests/src/early_return_helper.res | 7 + tests/tests/src/early_return_test.mjs | 239 ++++++++++++++++++ tests/tests/src/early_return_test.res | 215 ++++++++++++++++ 62 files changed, 967 insertions(+), 45 deletions(-) create mode 100644 tests/analysis_tests/tests/src/EarlyReturn.res create mode 100644 tests/analysis_tests/tests/src/expected/EarlyReturn.res.txt create mode 100644 tests/build_tests/super_errors/expected/return_async_mismatch.res.expected create mode 100644 tests/build_tests/super_errors/expected/return_existential_escape.res.expected create mode 100644 tests/build_tests/super_errors/expected/return_in_default.res.expected create mode 100644 tests/build_tests/super_errors/expected/return_in_module.res.expected create mode 100644 tests/build_tests/super_errors/expected/return_invalid_payload.res.expected create mode 100644 tests/build_tests/super_errors/expected/return_nested_type_mismatch.res.expected create mode 100644 tests/build_tests/super_errors/expected/return_outside_function.res.expected create mode 100644 tests/build_tests/super_errors/expected/return_sites_mismatch.res.expected create mode 100644 tests/build_tests/super_errors/expected/return_type_mismatch.res.expected create mode 100644 tests/build_tests/super_errors/expected/warning_21_return_unreachable.res.expected create mode 100644 tests/build_tests/super_errors/fixtures/return_async_mismatch.res create mode 100644 tests/build_tests/super_errors/fixtures/return_existential_escape.res create mode 100644 tests/build_tests/super_errors/fixtures/return_in_default.res create mode 100644 tests/build_tests/super_errors/fixtures/return_in_module.res create mode 100644 tests/build_tests/super_errors/fixtures/return_invalid_payload.res create mode 100644 tests/build_tests/super_errors/fixtures/return_nested_type_mismatch.res create mode 100644 tests/build_tests/super_errors/fixtures/return_outside_function.res create mode 100644 tests/build_tests/super_errors/fixtures/return_sites_mismatch.res create mode 100644 tests/build_tests/super_errors/fixtures/return_type_mismatch.res create mode 100644 tests/build_tests/super_errors/fixtures/warning_21_return_unreachable.res create mode 100644 tests/syntax_tests/data/ast-mapping/EarlyReturn.res create mode 100644 tests/syntax_tests/data/ast-mapping/expected/EarlyReturn.res.txt create mode 100644 tests/tests/src/early_return_helper.mjs create mode 100644 tests/tests/src/early_return_helper.res create mode 100644 tests/tests/src/early_return_test.mjs create mode 100644 tests/tests/src/early_return_test.res diff --git a/CHANGELOG.md b/CHANGELOG.md index 2958a0c989..530e3b53b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ #### :rocket: New Feature +- Add early return from function bodies, provisionally encoded as `%return(value)`, including loops and async functions. https://github.com/rescript-lang/rescript/pull/TBD - Support UTF-16 surrogate-pair escapes such as `"\uD83D\uDE00"` in ordinary string literals. https://github.com/rescript-lang/rescript/pull/8606 - Support dynamic imports of external bindings annotated with `@scope`; the generated import follows the complete property path. These imports were previously rejected. https://github.com/rescript-lang/rescript/pull/8582 - Add `@res.hoistedFunction` for emitting nested module functions as flat JavaScript exports. https://github.com/rescript-lang/rescript/pull/8402 diff --git a/analysis/reanalyze/src/side_effects.ml b/analysis/reanalyze/src/side_effects.ml index 14a5334a0d..6e24d1aa42 100644 --- a/analysis/reanalyze/src/side_effects.ml +++ b/analysis/reanalyze/src/side_effects.ml @@ -28,7 +28,7 @@ let rec expr_no_side_effects (expr : Typedtree.expression) = | Texp_function _ -> true (* Loop control changes whether subsequent code in the enclosing loop runs, so it should not be treated as a removable pure expression. *) - | Texp_break | Texp_continue -> false + | Texp_return _ | Texp_break | Texp_continue -> false | Texp_apply {funct = {exp_desc = Texp_ident (path, _, _)}; args} when path |> path_is_whitelisted_for_side_effects -> args |> List.for_all (fun (_, eo) -> eo |> expr_opt_no_side_effects) diff --git a/analysis/src/completion_front_end.ml b/analysis/src/completion_front_end.ml index 9d4b52dbaa..c3bcd3c592 100644 --- a/analysis/src/completion_front_end.ml +++ b/analysis/src/completion_front_end.ml @@ -1253,6 +1253,9 @@ let completion_with_parser1 ~debug ~offset ~pos_cursor ~kind_file match expr.pexp_desc with | Pexp_object_literal fields -> List.iter (fun (_, e) -> iterator.expr iterator e) fields + | Pexp_extension + ({txt = "return"}, PStr [{pstr_desc = Pstr_eval (value, [])}]) -> + iterator.expr iterator value | Pexp_extension ({txt}, _) -> set_result (CextensionNode txt) | Pexp_constant _ -> set_result Cnone | Pexp_ident lid -> diff --git a/compiler/core/lam_analysis.ml b/compiler/core/lam_analysis.ml index 5e3440173d..e7bcb5fd2e 100644 --- a/compiler/core/lam_analysis.ml +++ b/compiler/core/lam_analysis.ml @@ -122,7 +122,7 @@ let rec no_side_effects (lam : Lambda.t) : bool = | 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 - | Lbreak | Lcontinue -> false + | Lreturn _ | Lbreak | Lcontinue -> false | Lletrec (bindings, body) -> Ext_list.for_all_snd bindings no_side_effects && no_side_effects body | Lwhile _ -> @@ -186,6 +186,7 @@ let rec size (lam : Lambda.t) = | Ltrywith _ -> really_big () | Lifthenelse (l1, l2, l3) -> 1 + size l1 + size l2 + size l3 | Lsequence (l1, l2) -> size l1 + size l2 + | Lreturn value -> 1 + size value | Lbreak | Lcontinue -> 1 | Lwhile _ -> really_big () | Lfor _ -> really_big () @@ -249,9 +250,18 @@ let destruct_pattern (body : Lambda.t) params args = | Some _ | None -> false) | _ -> false +(* Returns are relative to the nearest function, so ordinary beta reduction + must preserve that boundary. Nested functions retain their own boundary. *) +let rec contains_return = function + | Lambda.Lreturn _ -> true + | Lfunction _ -> false + | lam -> Lambda_traverse.shallow_exists contains_return lam + (* Async functions cannot be beta reduced *) let lfunction_can_be_inlined (lfunction : Lambda.lfunction) = - (not lfunction.attr.async) && lfunction.attr.directive = None + (not lfunction.attr.async) + && lfunction.attr.directive = None + && not (contains_return lfunction.body) (** Hints to inlining *) let ok_to_inline_fun_when_app (m : Lambda.lfunction) (args : Lambda.t list) = diff --git a/compiler/core/lam_analysis.mli b/compiler/core/lam_analysis.mli index e42f736f14..e7e4b598bd 100644 --- a/compiler/core/lam_analysis.mli +++ b/compiler/core/lam_analysis.mli @@ -29,6 +29,10 @@ val no_side_effects : Lambda.t -> bool val size : Lambda.t -> int +val contains_return : Lambda.t -> bool +(** Whether a return targets the surrounding function. Does not descend into + nested functions, which establish their own return scope. *) + val lfunction_can_be_inlined : Lambda.lfunction -> bool val ok_to_inline_fun_when_app : Lambda.lfunction -> Lambda.t list -> bool diff --git a/compiler/core/lam_arity_analysis.ml b/compiler/core/lam_arity_analysis.ml index cab4cef656..02f60b5238 100644 --- a/compiler/core/lam_arity_analysis.ml +++ b/compiler/core/lam_arity_analysis.ml @@ -123,7 +123,7 @@ let rec get_arity (meta : Lam_stats.t) (lam : Lambda.t) : Lam_arity.t = | 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 - | Lbreak | Lcontinue -> Lam_arity.non_function_arity_info + | Lreturn _ | Lbreak | Lcontinue -> Lam_arity.non_function_arity_info | Lwhile _ | Lfor _ | Lfor_of _ | Lfor_await_of _ | Lassign _ -> Lam_arity.non_function_arity_info diff --git a/compiler/core/lam_bounded_vars.ml b/compiler/core/lam_bounded_vars.ml index 4737086a08..9d2e8bd20b 100644 --- a/compiler/core/lam_bounded_vars.ml +++ b/compiler/core/lam_bounded_vars.ml @@ -160,6 +160,7 @@ let rewrite (map : _ Hash_ident.t) (lam : Lambda.t) : Lambda.t = let l1 = aux l1 in let l2 = aux l2 in Lambda.seq l1 l2 + | Lreturn value -> Lambda.return (aux value) | Lbreak -> Lambda.break | Lcontinue -> Lambda.continue | Lwhile (l1, l2) -> diff --git a/compiler/core/lam_check.ml b/compiler/core/lam_check.ml index 91b71de50d..ff54a768aa 100644 --- a/compiler/core/lam_check.ml +++ b/compiler/core/lam_check.ml @@ -95,6 +95,7 @@ let check ~file ~pass lam = check_staticfails e2 cxt | Lifthenelse (e1, e2, e3) -> check_list [e1; e2; e3] cxt | Lsequence (e1, e2) -> check_list [e1; e2] cxt + | Lreturn value -> check_staticfails value cxt | Lassign (_id, e) -> check_staticfails e cxt in let rec iter_list xs = Ext_list.iter xs iter @@ -164,6 +165,7 @@ let check ~file ~pass lam = iter e1; def v; iter e2 + | Lreturn e -> iter e | Lassign (id, e) -> use id; iter e diff --git a/compiler/core/lam_closure.ml b/compiler/core/lam_closure.ml index 5386d5ab25..113e75238e 100644 --- a/compiler/core/lam_closure.ml +++ b/compiler/core/lam_closure.ml @@ -143,6 +143,7 @@ let free_variables (export_idents : Set_ident.t) (params : stats Map_ident.t) local_add v; iter sink_pos e1; iter sink_pos e2 + | Lreturn e -> iter top e | Lassign (id, e) -> used top id; iter top e diff --git a/compiler/core/lam_compile.ml b/compiler/core/lam_compile.ml index ddfcc13e57..c204417184 100644 --- a/compiler/core/lam_compile.ml +++ b/compiler/core/lam_compile.ml @@ -42,6 +42,7 @@ 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_ + | Lreturn body -> source_loc_of_lam body | Lstaticcatch (body, _, _) | Ltrywith (body, _, _) -> source_loc_of_lam body | Lstringswitch (_, cases, default) -> ( match cases with @@ -1190,19 +1191,22 @@ let compile output_prefix = *) and compile_while (predicate : Lambda.t) (body : Lambda.t) (lambda_cxt : Lam_compile_context.t) = + let direct_condition = Lam_analysis.contains_return predicate in + let predicate_cxt = + if direct_condition then + (* The condition will sit inside a new while(true) below. Any break or + continue targeting an outer loop must cross that new loop, just as + it must cross a switch, so force an explicit outer-loop label. *) + Lam_compile_context.enter_switch lambda_cxt + else lambda_cxt + in match compile_lambda - {lambda_cxt with continuation = NeedValue Not_tail} + {predicate_cxt with continuation = NeedValue Not_tail} predicate with | {value = None} -> assert false | {block; value = Some e} -> - (* st = NeedValue -- this should be optimized and never happen *) - let e = - match block with - | [] -> e - | _ -> E.of_block block ~e - in let loop_cxt, loop_frame = Lam_compile_context.push_loop lambda_cxt in let body_block = Js_output.output_as_block @@ -1212,7 +1216,18 @@ let compile output_prefix = in (* The label stays absent for ordinary loops and is filled in lazily if a nested switch emits break/continue for this loop. *) - let block = [S.while_ ?label:loop_frame.label e body_block] in + let condition, body_block = + if direct_condition then + (* A helper function would capture the condition's return. Evaluate + it at the top of each iteration in the original function. *) + (E.true_, block @ [S.if_ (E.not e) [S.break_ ()]] @ body_block) + else + ( (match block with + | [] -> e + | _ -> E.of_block block ~e), + body_block ) + in + let block = [S.while_ ?label:loop_frame.label condition body_block] in Js_output.output_of_block_and_expression lambda_cxt.continuation block E.unit (* all non-tail @@ -1899,6 +1914,20 @@ let compile output_prefix = | Lswitch (switch_arg, sw) -> compile_switch switch_arg sw lambda_cxt | Lstaticraise (i, largs) -> compile_staticraise i largs lambda_cxt | Lstaticcatch _ -> compile_staticcatch cur_lam lambda_cxt + | Lreturn value -> + (* Preserve handlers around operand evaluation. Do not infer a self-tail + call here from a continuation that may have been reset by a loop or + an expression context. The dummy value satisfies NeedValue callers + on this nonreturning path. *) + let output = + compile_lambda + { + lambda_cxt with + continuation = EffectCall (Maybe_tail_is_return Tail_in_try); + } + value + in + {output with value = Some E.undefined; output_finished = True} | Lbreak -> ( match lambda_cxt.loop_stack with | [] -> assert false diff --git a/compiler/core/lam_exit_count.ml b/compiler/core/lam_exit_count.ml index cfda12c69e..b3990e4fdb 100644 --- a/compiler/core/lam_exit_count.ml +++ b/compiler/core/lam_exit_count.ml @@ -113,7 +113,7 @@ let count_helper (lam : Lambda.t) : collection option = | Lfor_await_of (_, l1, l2) -> count l1; count l2 - | Lassign (_, l) -> count l + | Lreturn l | Lassign (_, l) -> count l and count_default sw = match sw.sw_failaction with | None -> () diff --git a/compiler/core/lam_hit.ml b/compiler/core/lam_hit.ml index dcd0dc9700..bf0cc1fd81 100644 --- a/compiler/core/lam_hit.ml +++ b/compiler/core/lam_hit.ml @@ -36,6 +36,7 @@ let hit_variables (fv : Set_ident.t) (l : t) : bool = and hit (l : t) = match (l : t) with | Lvar id -> hit_var id + | Lreturn value -> hit value | Lassign (id, e) -> hit_var id || hit e | Lstaticcatch (e1, (_, _vars), e2) -> hit e1 || hit e2 | Ltrywith (e1, _exn, e2) -> hit e1 || hit e2 @@ -73,6 +74,7 @@ let hit_variable (fv : Ident.t) (l : t) : bool = and hit (l : t) = match (l : t) with | Lvar id -> hit_var id + | Lreturn value -> hit value | Lassign (id, e) -> hit_var id || hit e | Lstaticcatch (e1, (_, _vars), e2) -> hit e1 || hit e2 | Ltrywith (e1, _exn, e2) -> hit e1 || hit e2 diff --git a/compiler/core/lam_pass_collect.ml b/compiler/core/lam_pass_collect.ml index 383a53f305..84ef79acb6 100644 --- a/compiler/core/lam_pass_collect.ml +++ b/compiler/core/lam_pass_collect.ml @@ -139,6 +139,7 @@ let collect_info (meta : Lam_stats.t) (lam : Lambda.t) = | Lfor_await_of (_, l1, l2) -> collect l1; collect l2 + | Lreturn l -> collect l | Lassign (_v, l) -> (* Lalias-bound variables are never assigned, so don't increase v's refcollect *) diff --git a/compiler/core/lam_pass_count.ml b/compiler/core/lam_pass_count.ml index f2d8dde559..53a62a1de6 100644 --- a/compiler/core/lam_pass_count.ml +++ b/compiler/core/lam_pass_count.ml @@ -124,6 +124,7 @@ let collect_occurs lam : occ_tbl = (* count [l2] first, If v is unused, l1 will be removed, so don't count its variables *) if kind = Strict || used v then count bv l1 + | Lreturn l -> count bv l | Lassign (_, l) -> (* Lalias-bound variables are never assigned, so don't increase this ident's refcount *) diff --git a/compiler/core/lam_pass_exits.ml b/compiler/core/lam_pass_exits.ml index 24b12988bb..008054f5b2 100644 --- a/compiler/core/lam_pass_exits.ml +++ b/compiler/core/lam_pass_exits.ml @@ -33,6 +33,7 @@ and no_bounded_variables (l : Lambda.t) = match l with | Lvar _ -> true | Lconst _ -> true + | Lreturn e -> no_bounded_variables e | Lassign (_id, e) -> no_bounded_variables e | Lapply {ap_func; ap_args; _} -> no_bounded_variables ap_func && no_list ap_args diff --git a/compiler/ext/config.ml b/compiler/ext/config.ml index be12e47763..2d15eb91b1 100644 --- a/compiler/ext/config.ml +++ b/compiler/ext/config.ml @@ -13,6 +13,6 @@ and ast0_impl_magic_number = "Caml1999M022" and ast0_intf_magic_number = "Caml1999N022" -and cmt_magic_number = "Caml1999T037" +and cmt_magic_number = "Caml1999T038" let load_path = ref ([] : string list) diff --git a/compiler/frontend/bs_builtin_ppx.ml b/compiler/frontend/bs_builtin_ppx.ml index 15cb024374..1e0f8818a3 100644 --- a/compiler/frontend/bs_builtin_ppx.ml +++ b/compiler/frontend/bs_builtin_ppx.ml @@ -129,6 +129,10 @@ let expr_mapper ~async_context ~in_function_def (self : mapper) ~attrs:(self.attributes self e.pexp_attributes) {e with pexp_desc = raw; pexp_attributes = []} (Ast_comb.to_regexp_type loc) + | Pexp_extension ({txt = "return"}, _) -> + (* Unlike unknown extensions, return contains an ordinary expression: + lower await, JSX, and other builtins in its operand before typing. *) + Ast_mapper.default_mapper.expr self e | Pexp_extension extension -> Ast_exp_extension.handle_extension e self extension | Pexp_constant (Pconst_integer (s, Some 'l')) -> diff --git a/compiler/ml/lambda.ml b/compiler/ml/lambda.ml index 5bed56fa7b..d205fa671c 100644 --- a/compiler/ml/lambda.ml +++ b/compiler/ml/lambda.ml @@ -349,6 +349,10 @@ type t = | Ltrywith of t * Ident.t * t | Lifthenelse of t * t * t | Lsequence of t * t + | Lreturn of t + (** Evaluates its operand, then exits the nearest [Lfunction]. This is + effectful even for a pure operand. Moving it across a function + boundary (in particular beta reduction) is invalid. *) | Lbreak | Lcontinue | Lwhile of t * t @@ -664,6 +668,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 return value : t = Lreturn value let break : t = Lbreak let continue : t = Lcontinue let while_ cond body : t = Lwhile (cond, body) @@ -728,6 +733,10 @@ let rec eq_approx (l1 : t) (l2 : t) = match l2 with | Lsequence (a0, b0) -> eq_approx a a0 && eq_approx b b0 | _ -> false) + | Lreturn v -> ( + match l2 with + | Lreturn v2 -> eq_approx v v2 + | _ -> false) | Lbreak -> l2 = Lbreak | Lcontinue -> l2 = Lcontinue | Lwhile (p, b) -> ( diff --git a/compiler/ml/lambda.mli b/compiler/ml/lambda.mli index 38ffead4b9..c8f852fd40 100644 --- a/compiler/ml/lambda.mli +++ b/compiler/ml/lambda.mli @@ -363,6 +363,10 @@ type t = private | Ltrywith of t * Ident.t * t | Lifthenelse of t * t * t | Lsequence of t * t + | Lreturn of t + (** Evaluates its operand, then exits the nearest [Lfunction]. This is + effectful even for a pure operand. Moving it across a function + boundary (in particular beta reduction) is invalid. *) | Lbreak | Lcontinue | Lwhile of t * t @@ -506,6 +510,7 @@ val if_ : t -> t -> t -> t val seq : t -> t -> t +val return : t -> t val break : t val continue : t diff --git a/compiler/ml/lambda_scc.ml b/compiler/ml/lambda_scc.ml index 8050f6f90b..df228bfecc 100644 --- a/compiler/ml/lambda_scc.ml +++ b/compiler/ml/lambda_scc.ml @@ -37,6 +37,7 @@ let exists_var (p : Ident.t -> bool) (l : Lambda.t) : bool = and hit (l : Lambda.t) = match l with | Lvar id -> p id + | Lreturn value -> hit value | Lassign (id, e) -> p id || hit e | Lstaticcatch (e1, _, e2) | Ltrywith (e1, _, e2) diff --git a/compiler/ml/lambda_traverse.ml b/compiler/ml/lambda_traverse.ml index f4e6cd4fe2..4fb4ce4803 100644 --- a/compiler/ml/lambda_traverse.ml +++ b/compiler/ml/lambda_traverse.ml @@ -27,6 +27,9 @@ open Lambda let shallow_map_sharing (f : t -> t) (lam : t) : t = match lam with | Lvar _ | Lglobal_module _ | Lconst _ | Lbreak | Lcontinue -> lam + | Lreturn value -> + let value' = f value in + if value == value' then lam else return value' | Lapply ap -> let fn = f ap.ap_func in let args = Ext_list.map_sharing ap.ap_args f in @@ -142,6 +145,7 @@ let make_key e = | 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) + | Lreturn value -> return (tr_rec env value) | Lbreak -> break | Lcontinue -> continue | Lassign (x, e) -> assign x (tr_rec env e) @@ -168,6 +172,7 @@ let shallow_exists (f : t -> bool) (lam : t) : bool = match lam with | Lvar _ | Lglobal_module _ | Lconst _ | Lbreak | Lcontinue -> false | Lapply {ap_func; ap_args} -> f ap_func || Ext_list.exists ap_args f + | Lreturn value -> f value | Lfunction {body} -> f body | Llet (_, _, arg, body) -> f arg || f body | Lletrec (decl, body) -> f body || Ext_list.exists_snd decl f @@ -217,7 +222,7 @@ let free_ids get l = | Lassign (id, _e) -> fv := Set_ident.add !fv id | Lvar _ | Lglobal_module _ | Lconst _ | Lapply _ | Lprim _ | Lswitch _ | Lstringswitch _ | Lstaticraise _ | Lifthenelse _ | Lsequence _ | Lbreak - | Lcontinue | Lwhile _ -> + | Lreturn _ | Lcontinue | Lwhile _ -> () in free l; diff --git a/compiler/ml/printlambda.ml b/compiler/ml/printlambda.ml index e006d602aa..2d34f210ee 100644 --- a/compiler/ml/printlambda.ml +++ b/compiler/ml/printlambda.ml @@ -355,6 +355,7 @@ let rec lam ppf = function | 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 + | Lreturn value -> fprintf ppf "(return %a)" lam value | Lbreak -> fprintf ppf "break" | Lcontinue -> fprintf ppf "continue" | Lwhile (lcond, lbody) -> diff --git a/compiler/ml/printtyped.ml b/compiler/ml/printtyped.ml index bccf85b697..1a421ce2d9 100644 --- a/compiler/ml/printtyped.ml +++ b/compiler/ml/printtyped.ml @@ -342,6 +342,9 @@ and expression i ppf x = line i ppf "Texp_sequence\n"; expression i ppf e1; expression i ppf e2 + | Texp_return exp -> + line i ppf "Texp_return\n"; + expression (i + 1) ppf exp | Texp_break -> line i ppf "Texp_break\n" | Texp_continue -> line i ppf "Texp_continue\n" | Texp_while (e1, e2) -> diff --git a/compiler/ml/rec_check.ml b/compiler/ml/rec_check.ml index 2fdd4eb184..dcd32d3a5b 100644 --- a/compiler/ml/rec_check.ml +++ b/compiler/ml/rec_check.ml @@ -207,9 +207,9 @@ let rec classify_expression : Typedtree.expression -> sd = Static | Texp_apply {funct = {exp_desc = Texp_ident (_, _, vd)}} when is_ref vd -> Static - | Texp_apply _ | Texp_match _ | Texp_ifthenelse _ | Texp_object_get _ - | Texp_object_set _ | Texp_field _ | Texp_assert _ | Texp_try _ - | Texp_tagged_template _ | Texp_template _ -> + | Texp_return _ | Texp_apply _ | Texp_match _ | Texp_ifthenelse _ + | Texp_object_get _ | Texp_object_set _ | Texp_field _ | Texp_assert _ + | Texp_try _ | Texp_tagged_template _ | Texp_template _ -> Dynamic let rec expression : Env.env -> Typedtree.expression -> Use.t = @@ -252,6 +252,7 @@ let rec expression : Env.env -> Typedtree.expression -> Use.t = (* The body is evaluated, but not used, and not available for inclusion in another value *) (discard (expression env body))) + | Texp_return exp -> Use.inspect (expression env exp) | Texp_constant _ -> Use.empty | Texp_break | Texp_continue -> Use.empty | Texp_apply diff --git a/compiler/ml/tast_iterator.ml b/compiler/ml/tast_iterator.ml index 1c0689cc5d..4d83b0f1ff 100644 --- a/compiler/ml/tast_iterator.ml +++ b/compiler/ml/tast_iterator.ml @@ -185,6 +185,7 @@ let expr sub {exp_extra; exp_desc; exp_env; _} = | Texp_sequence (exp1, exp2) -> sub.expr sub exp1; sub.expr sub exp2 + | Texp_return exp -> sub.expr sub exp | Texp_break | Texp_continue -> () | Texp_while (exp1, exp2) -> sub.expr sub exp1; diff --git a/compiler/ml/tast_mapper.ml b/compiler/ml/tast_mapper.ml index 35b081825e..2e48e2a9da 100644 --- a/compiler/ml/tast_mapper.ml +++ b/compiler/ml/tast_mapper.ml @@ -243,6 +243,7 @@ let expr sub x = (sub.expr sub exp1, sub.expr sub exp2, opt (sub.expr sub) expo) | Texp_sequence (exp1, exp2) -> Texp_sequence (sub.expr sub exp1, sub.expr sub exp2) + | Texp_return exp -> Texp_return (sub.expr sub exp) | Texp_break -> Texp_break | Texp_continue -> Texp_continue | Texp_while (exp1, exp2) -> diff --git a/compiler/ml/translcore.ml b/compiler/ml/translcore.ml index 2756d7438f..6b1ca68b40 100644 --- a/compiler/ml/translcore.ml +++ b/compiler/ml/translcore.ml @@ -930,6 +930,7 @@ let exception_id_destructed (l : Lambda.t) (fv : Ident.t) : bool = | Lprim {primitive = Praise; args = [Lvar _]; loc = _} -> false | Lprim {primitive = _; args; loc = _} -> hit_list args | Lvar id -> Ident.same id fv + | Lreturn value -> hit value | Lassign (id, e) -> Ident.same id fv || hit e | Lstaticcatch (e1, _, e2) -> hit e1 || hit e2 | Ltrywith (e1, _, e2) -> hit e1 || hit e2 @@ -1267,6 +1268,7 @@ and transl_exp0 (e : Typedtree.expression) : Lambda.t = | Texp_ifthenelse (cond, ifso, None) -> if_ (transl_exp cond) (transl_exp ifso) lambda_unit | Texp_sequence (expr1, expr2) -> seq (transl_exp expr1) (transl_exp expr2) + | Texp_return value -> return (transl_exp value) | Texp_break -> break | Texp_continue -> continue | Texp_while (cond, body) -> while_ (transl_exp cond) (transl_exp body) diff --git a/compiler/ml/typecore.ml b/compiler/ml/typecore.ml index 0f2d516fbe..4b20eccad5 100644 --- a/compiler/ml/typecore.ml +++ b/compiler/ml/typecore.ml @@ -77,6 +77,8 @@ type error = | Inlined_record_expected | Invalid_extension_constructor_payload | Not_an_extension_constructor + | Return_outside_function + | Invalid_return_payload | Break_outside_loop | Continue_outside_loop | Literal_overflow of string @@ -145,6 +147,17 @@ let rp node = type recarg = Allowed | Required | Rejected +(* A return is scoped to a source function body, never a parameter default + or module initializer (functors introduce backend function boundaries). + Keep the actual result variable: instantiating it here would allow different + return sites to infer unrelated result types. *) +let return_type : type_expr option ref = ref None + +let with_return_type typ f = + let saved = !return_type in + return_type := typ; + Misc.try_finally f (fun () -> return_type := saved) + let loop_depth = ref 0 let with_depth depth_ref f = @@ -165,6 +178,9 @@ let iter_expression f e = let rec expr e = f e; match e.pexp_desc with + | Pexp_extension + ({txt = "return"}, PStr [{pstr_desc = Pstr_eval (value, [])}]) -> + expr value | Pexp_extension _ (* we don't iterate under extension point *) | Pexp_ident _ | Pexp_constant _ -> () @@ -3124,8 +3140,8 @@ and type_expect_ ?deprecated_context ~context ?(recarg = Rejected) env sexp | Pexp_sequence (sexp1, sexp2) -> let exp1 = type_statement ~context:None env sexp1 in (match exp1.exp_desc with - | Texp_break | Texp_continue -> - (* Loop control should only reuse the nonreturning-statement warning when + | Texp_return _ | Texp_break | Texp_continue -> + (* Control flow should only reuse the nonreturning-statement warning when there is a following statement in the same block/sequence. *) Location.prerr_warning (final_subexpression sexp1).pexp_loc Warnings.Nonreturning_statement @@ -3319,6 +3335,14 @@ and type_expect_ ?deprecated_context ~context ?(recarg = Rejected) env sexp if separate then begin_def (); let cty = Typetexp.transl_simple_type env false sty in let ty = cty.ctyp_type in + (* An annotation on the function result must constrain early returns before + entering a GADT case. Otherwise the first case fixes a fresh result + variable to its concrete type instead of using the local equation for + the annotated abstract type. Do not propagate unrelated constraints. *) + (match !return_type with + | Some result when repr result == repr ty_expected -> + unify_exp_types ~context:None loc env result ty + | Some _ | None -> ()); let arg, ty' = if separate then ( end_def (); @@ -3507,7 +3531,7 @@ and type_expect_ ?deprecated_context ~context ?(recarg = Rejected) env sexp begin_def (); Ident.set_current_time ty.level; let context = Typetexp.narrow () in - let modl = !type_module env smodl in + let modl = with_return_type None (fun () -> !type_module env smodl) in let id, new_env = Env.enter_module name.txt modl.mod_type env in Ctype.init_def (Ident.current_time ()); Typetexp.widen context; @@ -3571,7 +3595,9 @@ and type_expect_ ?deprecated_context ~context ?(recarg = Rejected) env sexp | {desc = Tvar _} -> raise (Error (loc, env, Cannot_infer_signature)) | _ -> raise (Error (loc, env, Not_a_packed_module ty_expected)) in - let modl, tl' = !type_package env m p nl in + let modl, tl' = + with_return_type None (fun () -> !type_package env m p nl) + in rue { exp_desc = Texp_pack modl; @@ -3616,6 +3642,25 @@ and type_expect_ ?deprecated_context ~context ?(recarg = Rejected) env sexp exp_env = env; } | _ -> raise (Error (loc, env, Invalid_extension_constructor_payload))) + | Pexp_extension ({txt = "return"}, payload) -> ( + let result_type = + match !return_type with + | Some typ -> typ + | None -> raise (Error (loc, env, Return_outside_function)) + in + match payload with + | PStr [{pstr_desc = Pstr_eval (value, [])}] -> + let value = type_expect ~context:None env value result_type in + re + { + exp_desc = Texp_return value; + exp_loc = loc; + exp_extra = []; + exp_type = instance env ty_expected; + exp_attributes = sexp.pexp_attributes; + exp_env = env; + } + | _ -> raise (Error (loc, env, Invalid_return_payload))) | Pexp_extension ext -> raise (Error_forward (Builtin_attributes.error_of_extension ext)) | Pexp_await _ -> (* should be handled earlier *) assert false @@ -3952,13 +3997,25 @@ and type_function ~async loc attrs env ty_expected_ in let typed_params, body_env, unpacks, default_lets = with_reset_control_flow (fun () -> - type_params [] env [] [] sparams_bindings ty_params) + with_return_type None (fun () -> + type_params [] env [] [] sparams_bindings ty_params)) in let body_exp = with_reset_control_flow (fun () -> let sbody = wrap_unpacks sbody unpacks in let ty_res' = if has_gadts then correct_levels ty_res else ty_res in - let exp = type_expect ~context:None body_env sbody ty_res' in + let result_type = + if async then ( + let payload = newvar () in + unify_exp_types ~context:None loc body_env ty_res' + (newconstr Predef.path_promise [payload]); + payload) + else ty_res' + in + let exp = + with_return_type (Some result_type) (fun () -> + type_expect ~context:None body_env sbody ty_res') + in {exp with exp_type = instance env ty_res'}) in (if has_gadts then @@ -4561,8 +4618,13 @@ and type_statement ~context env sexp = let exp = type_exp ~context env sexp in end_def (); let ty = expand_head env exp.exp_type and tv = newvar () in - if is_Tvar ty && ty.level > tv.level then - Location.prerr_warning loc Warnings.Nonreturning_statement; + if + is_Tvar ty && ty.level > tv.level + && + match exp.exp_desc with + | Texp_return _ -> false + | _ -> true + then Location.prerr_warning loc Warnings.Nonreturning_statement; let expected_ty = instance_def Predef.type_unit in let context = type_clash_context_in_statement sexp in unify_exp ~context env exp expected_ty; @@ -4673,24 +4735,40 @@ and type_cases ~(call_context : [`LetUnwrap | `Switch | `Function | `Try]) env in (* Format.printf "@[%i %i, ty_res' =@ %a@]@." lev (get_current_level()) Printtyp.raw_type_expr ty_res'; *) - let guard = - match pc_guard with - | None -> None - | Some scond -> - Some - (type_expect ~context:(Some IfCondition) ext_env - (wrap_unpacks scond unpacks) - Predef.type_bool) + let type_body () = + let guard = + match pc_guard with + | None -> None + | Some scond -> + Some + (type_expect ~context:(Some IfCondition) ext_env + (wrap_unpacks scond unpacks) + Predef.type_bool) + in + let exp = + type_expect + ~context: + (match call_context with + | `Switch -> Some SwitchReturn + | `Try -> Some TryReturn + | `LetUnwrap -> Some LetUnwrapReturn + | `Function -> None) + ext_env sexp ty_res' + in + (guard, exp) in - let exp = - type_expect - ~context: - (match call_context with - | `Switch -> Some SwitchReturn - | `Try -> Some TryReturn - | `LetUnwrap -> Some LetUnwrapReturn - | `Function -> None) - ext_env sexp ty_res' + let guard, exp = + match !return_type with + | Some result when contains_gadt env pc_lhs -> + (* Early results need the same local-equation isolation as the + implicit case result, including returns in guards. Reconcile + the copy outside the case so existential types cannot escape. *) + let local_result = correct_levels result in + let typed = with_return_type (Some local_result) type_body in + unify_exp_types ~context:None pc_rhs.pexp_loc env result + local_result; + typed + | Some _ | None -> type_body () in { c_lhs = pat; @@ -5292,6 +5370,9 @@ let report_error env loc ppf error = "Invalid [%%extension_constructor] payload, a constructor is expected." | Not_an_extension_constructor -> fprintf ppf "This constructor is not an extension constructor." + | Return_outside_function -> + fprintf ppf "Early return can only be used inside a function body." + | Invalid_return_payload -> fprintf ppf "Expected %%return(value)." | Break_outside_loop -> fprintf ppf "`break` can only be used directly inside a loop body." | Continue_outside_loop -> diff --git a/compiler/ml/typecore.mli b/compiler/ml/typecore.mli index cc0cc6415c..ad1b0c6bcd 100644 --- a/compiler/ml/typecore.mli +++ b/compiler/ml/typecore.mli @@ -110,6 +110,8 @@ type error = | Inlined_record_expected | Invalid_extension_constructor_payload | Not_an_extension_constructor + | Return_outside_function + | Invalid_return_payload | Break_outside_loop | Continue_outside_loop | Literal_overflow of string diff --git a/compiler/ml/typedtree.ml b/compiler/ml/typedtree.ml index e870c3dd7b..5e06f908d3 100644 --- a/compiler/ml/typedtree.ml +++ b/compiler/ml/typedtree.ml @@ -117,6 +117,7 @@ and expression_desc = | Texp_array of expression list | Texp_ifthenelse of expression * expression * expression option | Texp_sequence of expression * expression + | Texp_return of expression | Texp_break | Texp_continue | Texp_while of expression * expression diff --git a/compiler/ml/typedtree.mli b/compiler/ml/typedtree.mli index 66946b201f..51c18e845d 100644 --- a/compiler/ml/typedtree.mli +++ b/compiler/ml/typedtree.mli @@ -225,6 +225,12 @@ and expression_desc = | Texp_array of expression list | Texp_ifthenelse of expression * expression * expression option | Texp_sequence of expression * expression + | Texp_return of expression + (** Return from the nearest enclosing function. The operand has that + function body's result type (the resolved payload for async functions); + the expression itself never produces a value. + Parameter defaults and module initializers do not inherit a return + scope. The provisional parsetree encoding is [%return(value)]. *) | Texp_break | Texp_continue | Texp_while of expression * expression diff --git a/compiler/ml/typedtree_iter.ml b/compiler/ml/typedtree_iter.ml index 14e6952c32..432145257f 100644 --- a/compiler/ml/typedtree_iter.ml +++ b/compiler/ml/typedtree_iter.ml @@ -272,6 +272,7 @@ end = struct | Texp_sequence (exp1, exp2) -> iter_expression exp1; iter_expression exp2 + | Texp_return exp -> iter_expression exp | Texp_break | Texp_continue -> () | Texp_while (exp1, exp2) -> iter_expression exp1; diff --git a/tests/ERROR_VARIANTS.md b/tests/ERROR_VARIANTS.md index a4d7dc70d6..b21b045b28 100644 --- a/tests/ERROR_VARIANTS.md +++ b/tests/ERROR_VARIANTS.md @@ -207,7 +207,7 @@ Source: [typecore.ml:27](../compiler/ml/typecore.ml). | `Or_pattern_type_clash` | ✓ | `or_pattern_type_clash.res` | | | `Multiply_bound_variable` | ✓ | `multiply_bound_variable.res` | | | `Orpat_vars` | ✓ | `orpat_vars_unbalanced.res` | | -| `Expr_type_clash` | ✓ | many `*.res` | Most-fired expression error. Trace-shape sub-cases covered: `if_return_type_mismatch.res` (IfReturn), `maybe_unwrap_option.res` (MaybeUnwrapOption), `string_concat_non_string.res` (StringConcat), `labeled_fn_argument_type_clash.res` (FunctionArgument with explicit label), `math_operator_*.res` (MathOperator family), `ternary_branch_mismatch.res`, `switch_different_types.res`, `try_catch_same_type.res`, `comparison_operator.res`, `array_item_type_mismatch.res`, `array_literal_passed_to_tuple.res`, `if_condition_mismatch.res`, `while_condition.res`, `for_loop_condition.res`, `assert_condition.res`, `function_call_mismatch.res`, `awaiting_non_promise.res`, multiple `jsx_*` fixtures (including `jsx_preserve_external_function.res` for a plain-function external in a JSX component position), `object_literal_for_poly_field.res` (object literal against a polymorphic field annotation). | +| `Expr_type_clash` | ✓ | many `*.res` | Early-return result checks: `return_type_mismatch.res`, `return_sites_mismatch.res`, `return_nested_type_mismatch.res`, `return_async_mismatch.res`, `return_existential_escape.res`. Most-fired expression error. Trace-shape sub-cases covered: `if_return_type_mismatch.res` (IfReturn), `maybe_unwrap_option.res` (MaybeUnwrapOption), `string_concat_non_string.res` (StringConcat), `labeled_fn_argument_type_clash.res` (FunctionArgument with explicit label), `math_operator_*.res` (MathOperator family), `ternary_branch_mismatch.res`, `switch_different_types.res`, `try_catch_same_type.res`, `comparison_operator.res`, `array_item_type_mismatch.res`, `array_literal_passed_to_tuple.res`, `if_condition_mismatch.res`, `while_condition.res`, `for_loop_condition.res`, `assert_condition.res`, `function_call_mismatch.res`, `awaiting_non_promise.res`, multiple `jsx_*` fixtures (including `jsx_preserve_external_function.res` for a plain-function external in a JSX component position), `object_literal_for_poly_field.res` (object literal against a polymorphic field annotation). | | `Apply_non_function` | ✓ | `apply_non_function.res` | | | `Apply_wrong_label` | ✓ | `apply_wrong_label.res` | | | `Label_multiply_defined` | ✓ | `label_multiply_defined_literal.res` | | @@ -237,6 +237,8 @@ Source: [typecore.ml:27](../compiler/ml/typecore.ml). | `Inlined_record_expected` | ✓ | `inlined_record_expected.res`, `super_errors_multi/Cross_inline_record_constructor` | | | `Invalid_extension_constructor_payload` | ✓ | `invalid_extension_constructor_payload.res` | | | `Not_an_extension_constructor` | ✓ | `not_an_extension_constructor.res` | | +| `Return_outside_function` | ✓ | `return_outside_function.res`, `return_in_default.res`, `return_in_module.res` | Parameter defaults and module initializers do not inherit a return scope. | +| `Invalid_return_payload` | ✓ | `return_invalid_payload.res` | | | `Break_outside_loop` | ✓ | `break_outside_loop.res`, `break_in_nested_function.res` | | | `Continue_outside_loop` | ✓ | `continue_outside_loop.res`, `continue_in_nested_function.res` | | | `Literal_overflow` | ✓ | `intoverflow.res` | | @@ -556,7 +558,8 @@ warnings still fire. Fixtures follow the naming convention `warning__.res` so coverage gaps stay greppable. Warning 11 (`Unused_match`) is covered by -`warning_11_equivalent_string_patterns.res`. +`warning_11_equivalent_string_patterns.res`. Warning 21 (`Nonreturning_statement`) +for early return is covered by `warning_21_return_unreachable.res`. ### Removed warnings diff --git a/tests/analysis_tests/tests/src/EarlyReturn.res b/tests/analysis_tests/tests/src/EarlyReturn.res new file mode 100644 index 0000000000..05dc2cb87f --- /dev/null +++ b/tests/analysis_tests/tests/src/EarlyReturn.res @@ -0,0 +1,13 @@ +let returned = 42 +// ^ref + +let get = () => %return(returned) +// ^hov + +let typed = () => %return(returned) +// ^hov + +let completion = () => { + %return(returned) + // ^com +} diff --git a/tests/analysis_tests/tests/src/expected/EarlyReturn.res.txt b/tests/analysis_tests/tests/src/expected/EarlyReturn.res.txt new file mode 100644 index 0000000000..70ccde1cb3 --- /dev/null +++ b/tests/analysis_tests/tests/src/expected/EarlyReturn.res.txt @@ -0,0 +1,55 @@ +References src/EarlyReturn.res 0:4 +[ + { + "range": { + "end": { "character": 12, "line": 0 }, + "start": { "character": 4, "line": 0 } + }, + "uri": "file:///EarlyReturn.res" + }, + { + "range": { + "end": { "character": 32, "line": 3 }, + "start": { "character": 24, "line": 3 } + }, + "uri": "file:///EarlyReturn.res" + }, + { + "range": { + "end": { "character": 34, "line": 6 }, + "start": { "character": 26, "line": 6 } + }, + "uri": "file:///EarlyReturn.res" + }, + { + "range": { + "end": { "character": 18, "line": 10 }, + "start": { "character": 10, "line": 10 } + }, + "uri": "file:///EarlyReturn.res" + } +] + +Hover src/EarlyReturn.res 3:26 +{ "contents": { "kind": "markdown", "value": "```rescript\nint\n```" } } + +Hover src/EarlyReturn.res 6:4 +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nunit => int\n```" + } +} + +Complete src/EarlyReturn.res 10:13 +posCursor:[10:13] posNoWhite:[10:12] Found expr:[9:17->12:1] +posCursor:[10:13] posNoWhite:[10:12] Found expr:[10:2->10:19] +posCursor:[10:13] posNoWhite:[10:12] Found expr:[10:10->10:18] +Pexp_ident returned:[10:10->10:18] +Completable: Cpath Value[returned] +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Value[returned] +Path returned +[ { "detail": "int", "kind": 12, "label": "returned", "tags": [] } ] + diff --git a/tests/build_tests/super_errors/expected/return_async_mismatch.res.expected b/tests/build_tests/super_errors/expected/return_async_mismatch.res.expected new file mode 100644 index 0000000000..4630e8cf3e --- /dev/null +++ b/tests/build_tests/super_errors/expected/return_async_mismatch.res.expected @@ -0,0 +1,14 @@ + + We've found a bug for you! + /.../fixtures/return_async_mismatch.res:5:3-4 + + 3 │ %return("wrong") + 4 │ } + 5 │ 42 + 6 │ } + 7 │ + + This has type: int + But it's expected to have type: string + + You can convert int to string with Int.toString. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/return_existential_escape.res.expected b/tests/build_tests/super_errors/expected/return_existential_escape.res.expected new file mode 100644 index 0000000000..9faf8c6740 --- /dev/null +++ b/tests/build_tests/super_errors/expected/return_existential_escape.res.expected @@ -0,0 +1,13 @@ + + We've found a bug for you! + /.../fixtures/return_existential_escape.res:4:23 + + 2 │ let get = value => { + 3 │ switch value { + 4 │ | Box(x) => %return(x) + 5 │ } + 6 │ } + + This has type: \"$Box_'a" + But it's expected to have type: 'a + The type constructor $Box_'a would escape its scope \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/return_in_default.res.expected b/tests/build_tests/super_errors/expected/return_in_default.res.expected new file mode 100644 index 0000000000..20525efd82 --- /dev/null +++ b/tests/build_tests/super_errors/expected/return_in_default.res.expected @@ -0,0 +1,10 @@ + + We've found a bug for you! + /.../fixtures/return_in_default.res:2:15-25 + + 1 │ let f = () => { + 2 │ let g = (~x=%return(42)) => x + 3 │ g() + 4 │ } + + Early return can only be used inside a function body. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/return_in_module.res.expected b/tests/build_tests/super_errors/expected/return_in_module.res.expected new file mode 100644 index 0000000000..74c3d14e30 --- /dev/null +++ b/tests/build_tests/super_errors/expected/return_in_module.res.expected @@ -0,0 +1,11 @@ + + We've found a bug for you! + /.../fixtures/return_in_module.res:3:13-23 + + 1 │ let f = () => { + 2 │ module M = { + 3 │ let x = %return(42) + 4 │ } + 5 │ M.x + + Early return can only be used inside a function body. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/return_invalid_payload.res.expected b/tests/build_tests/super_errors/expected/return_invalid_payload.res.expected new file mode 100644 index 0000000000..6ccddde4bb --- /dev/null +++ b/tests/build_tests/super_errors/expected/return_invalid_payload.res.expected @@ -0,0 +1,8 @@ + + We've found a bug for you! + /.../fixtures/return_invalid_payload.res:1:15-21 + + 1 │ let f = () => %return + 2 │ + + Expected %return(value). \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/return_nested_type_mismatch.res.expected b/tests/build_tests/super_errors/expected/return_nested_type_mismatch.res.expected new file mode 100644 index 0000000000..397e4908b3 --- /dev/null +++ b/tests/build_tests/super_errors/expected/return_nested_type_mismatch.res.expected @@ -0,0 +1,13 @@ + + We've found a bug for you! + /.../fixtures/return_nested_type_mismatch.res:2:33-34 + + 1 │ let f = () => { + 2 │ let g = (): string => %return(42) + 3 │ g() + 4 │ } + + This has type: int + But it's expected to have type: string + + You can convert int to string with Int.toString. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/return_outside_function.res.expected b/tests/build_tests/super_errors/expected/return_outside_function.res.expected new file mode 100644 index 0000000000..5b65da2715 --- /dev/null +++ b/tests/build_tests/super_errors/expected/return_outside_function.res.expected @@ -0,0 +1,8 @@ + + We've found a bug for you! + /.../fixtures/return_outside_function.res:1:13-23 + + 1 │ let value = %return(42) + 2 │ + + Early return can only be used inside a function body. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/return_sites_mismatch.res.expected b/tests/build_tests/super_errors/expected/return_sites_mismatch.res.expected new file mode 100644 index 0000000000..d440490bc0 --- /dev/null +++ b/tests/build_tests/super_errors/expected/return_sites_mismatch.res.expected @@ -0,0 +1,14 @@ + + We've found a bug for you! + /.../fixtures/return_sites_mismatch.res:5:11-17 + + 3 │ %return(42) + 4 │ } + 5 │ %return("wrong") + 6 │ } + 7 │ + + This has type: string + But it's expected to have type: int + + You can convert string to int with Int.fromString. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/return_type_mismatch.res.expected b/tests/build_tests/super_errors/expected/return_type_mismatch.res.expected new file mode 100644 index 0000000000..c36bb2d3b4 --- /dev/null +++ b/tests/build_tests/super_errors/expected/return_type_mismatch.res.expected @@ -0,0 +1,14 @@ + + We've found a bug for you! + /.../fixtures/return_type_mismatch.res:5:3-9 + + 3 │ %return(42) + 4 │ } + 5 │ "wrong" + 6 │ } + 7 │ + + This has type: string + But it's expected to have type: int + + You can convert string to int with Int.fromString. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/warning_21_return_unreachable.res.expected b/tests/build_tests/super_errors/expected/warning_21_return_unreachable.res.expected new file mode 100644 index 0000000000..fdfc99f2f6 --- /dev/null +++ b/tests/build_tests/super_errors/expected/warning_21_return_unreachable.res.expected @@ -0,0 +1,10 @@ + + Warning number 21 + /.../fixtures/warning_21_return_unreachable.res:2:3-13 + + 1 │ let f = () => { + 2 │ %return(42) + 3 │ 7 + 4 │ } + + This statement does not continue execution; following code is unreachable. \ No newline at end of file diff --git a/tests/build_tests/super_errors/fixtures/return_async_mismatch.res b/tests/build_tests/super_errors/fixtures/return_async_mismatch.res new file mode 100644 index 0000000000..919492b71a --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/return_async_mismatch.res @@ -0,0 +1,6 @@ +let f = async x => { + if x { + %return("wrong") + } + 42 +} diff --git a/tests/build_tests/super_errors/fixtures/return_existential_escape.res b/tests/build_tests/super_errors/fixtures/return_existential_escape.res new file mode 100644 index 0000000000..5165f304cf --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/return_existential_escape.res @@ -0,0 +1,6 @@ +type rec box = Box('a): box +let get = value => { + switch value { + | Box(x) => %return(x) + } +} diff --git a/tests/build_tests/super_errors/fixtures/return_in_default.res b/tests/build_tests/super_errors/fixtures/return_in_default.res new file mode 100644 index 0000000000..0e9ad343dc --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/return_in_default.res @@ -0,0 +1,4 @@ +let f = () => { + let g = (~x=%return(42)) => x + g() +} diff --git a/tests/build_tests/super_errors/fixtures/return_in_module.res b/tests/build_tests/super_errors/fixtures/return_in_module.res new file mode 100644 index 0000000000..b9df3ef0d4 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/return_in_module.res @@ -0,0 +1,6 @@ +let f = () => { + module M = { + let x = %return(42) + } + M.x +} diff --git a/tests/build_tests/super_errors/fixtures/return_invalid_payload.res b/tests/build_tests/super_errors/fixtures/return_invalid_payload.res new file mode 100644 index 0000000000..54611ede24 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/return_invalid_payload.res @@ -0,0 +1 @@ +let f = () => %return diff --git a/tests/build_tests/super_errors/fixtures/return_nested_type_mismatch.res b/tests/build_tests/super_errors/fixtures/return_nested_type_mismatch.res new file mode 100644 index 0000000000..b8810a8055 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/return_nested_type_mismatch.res @@ -0,0 +1,4 @@ +let f = () => { + let g = (): string => %return(42) + g() +} diff --git a/tests/build_tests/super_errors/fixtures/return_outside_function.res b/tests/build_tests/super_errors/fixtures/return_outside_function.res new file mode 100644 index 0000000000..8dac172b61 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/return_outside_function.res @@ -0,0 +1 @@ +let value = %return(42) diff --git a/tests/build_tests/super_errors/fixtures/return_sites_mismatch.res b/tests/build_tests/super_errors/fixtures/return_sites_mismatch.res new file mode 100644 index 0000000000..688fcac86a --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/return_sites_mismatch.res @@ -0,0 +1,6 @@ +let f = x => { + if x { + %return(42) + } + %return("wrong") +} diff --git a/tests/build_tests/super_errors/fixtures/return_type_mismatch.res b/tests/build_tests/super_errors/fixtures/return_type_mismatch.res new file mode 100644 index 0000000000..a7de6d1c89 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/return_type_mismatch.res @@ -0,0 +1,6 @@ +let f = x => { + if x { + %return(42) + } + "wrong" +} diff --git a/tests/build_tests/super_errors/fixtures/warning_21_return_unreachable.res b/tests/build_tests/super_errors/fixtures/warning_21_return_unreachable.res new file mode 100644 index 0000000000..5aeb50e769 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/warning_21_return_unreachable.res @@ -0,0 +1,4 @@ +let f = () => { + %return(42) + 7 +} diff --git a/tests/ounit_tests/ounit_lambda_traverse_tests.ml b/tests/ounit_tests/ounit_lambda_traverse_tests.ml index 2bc3a09006..1fc5ce864b 100644 --- a/tests/ounit_tests/ounit_lambda_traverse_tests.ml +++ b/tests/ounit_tests/ounit_lambda_traverse_tests.ml @@ -34,6 +34,7 @@ let nodes : (string * Lambda.t) list = ("trywith", Lambda.try_ debugger y var); ("ifthenelse", Lambda.if_ var debugger debugger); ("sequence", Lambda.seq debugger var); + ("return", Lambda.return var); ("while", Lambda.while_ var debugger); ("for", Lambda.for_ y var var Upto debugger); ("for_of", Lambda.for_of y var debugger); @@ -49,6 +50,28 @@ let nodes : (string * Lambda.t) list = let suites = __FILE__ >::: [ + ( "return is effectful and preserves its function boundary" >:: fun _ -> + let value = Lambda.return (Lambda.var x) in + assert_bool "return cannot be discarded" + (not (Lam_analysis.no_side_effects value)); + assert_bool "return operand is free" + (Set_ident.mem (Lambda_traverse.free_variables value) x); + let fn body = + match + Lambda.function_ ~loc ~attr:Lambda.default_function_attribute + ~params:[x] ~body + with + | Lfunction fn -> fn + | _ -> assert false + in + assert_bool "returning function cannot be beta reduced" + (not (Lam_analysis.lfunction_can_be_inlined (fn value))); + let nested = + Lambda.function_ ~loc ~attr:Lambda.default_function_attribute + ~params:[x] ~body:value + in + assert_bool "a nested function retains its return scope" + (Lam_analysis.lfunction_can_be_inlined (fn nested)) ); ( "an unchanged child is not rebuilt" >:: fun _ -> List.iter (fun (name, node) -> diff --git a/tests/syntax_tests/data/ast-mapping/EarlyReturn.res b/tests/syntax_tests/data/ast-mapping/EarlyReturn.res new file mode 100644 index 0000000000..736d90ac98 --- /dev/null +++ b/tests/syntax_tests/data/ast-mapping/EarlyReturn.res @@ -0,0 +1,7 @@ +let first = x => { + if x > 0 { + %return(x) + } + 0 +} +let nested = () => () => %return(()) diff --git a/tests/syntax_tests/data/ast-mapping/expected/EarlyReturn.res.txt b/tests/syntax_tests/data/ast-mapping/expected/EarlyReturn.res.txt new file mode 100644 index 0000000000..736d90ac98 --- /dev/null +++ b/tests/syntax_tests/data/ast-mapping/expected/EarlyReturn.res.txt @@ -0,0 +1,7 @@ +let first = x => { + if x > 0 { + %return(x) + } + 0 +} +let nested = () => () => %return(()) diff --git a/tests/tests/src/early_return_helper.mjs b/tests/tests/src/early_return_helper.mjs new file mode 100644 index 0000000000..d54116e6da --- /dev/null +++ b/tests/tests/src/early_return_helper.mjs @@ -0,0 +1,14 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + + +function first(x) { + if (x > 0) { + return 42; + } + return 7; +} + +export { + first, +} +/* No side effect */ diff --git a/tests/tests/src/early_return_helper.res b/tests/tests/src/early_return_helper.res new file mode 100644 index 0000000000..8fdd5515b6 --- /dev/null +++ b/tests/tests/src/early_return_helper.res @@ -0,0 +1,7 @@ +@inline +let first = x => { + if x > 0 { + %return(42) + } + 7 +} diff --git a/tests/tests/src/early_return_test.mjs b/tests/tests/src/early_return_test.mjs new file mode 100644 index 0000000000..700563dd47 --- /dev/null +++ b/tests/tests/src/early_return_test.mjs @@ -0,0 +1,239 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + +import * as Mocha from "mocha"; +import * as Test_utils from "./test_utils.mjs"; +import * as Early_return_helper from "./early_return_helper.mjs"; +import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.mjs"; + +function choose(x) { + if (x > 0) { + return 42; + } + return 7; +} + +function alwaysReturn(x) { + return x; +} + +function identity(x) { + return x; +} + +function inLoops(target) { + for (let i = 0; i <= 3; ++i) { + let j = 0; + loop_0: while (j < 3) { + j = j + 1 | 0; + switch (target) { + case "first" : + return i + j | 0; + case "second" : + if (i === 2) { + return 20; + } + break; + case "third" : + continue loop_0; + default: + break loop_0; + } + }; + } + return 99; +} + +async function returnAsync(early) { + if (early) { + return await Promise.resolve(42); + } + return 7; +} + +function getGadt(v) { + return v._0; +} + +let OperandFailure = /* @__PURE__ */Primitive_exceptions.create("Early_return_test.OperandFailure"); + +function inTry(fail) { + try { + if (!fail) { + return 42; + } + throw { + RE_EXN_ID: OperandFailure, + Error: new Error() + }; + } catch (raw_exn) { + let exn = Primitive_exceptions.internalToException(raw_exn); + if (exn.RE_EXN_ID === OperandFailure) { + return 7; + } + throw exn; + } +} + +Mocha.describe("Early_return_test", () => { + Mocha.test("early and implicit results", () => { + Test_utils.eq("File \"early_return_test.res\", line 77, characters 7-14", 42, getGadt({ + TAG: "Int", + _0: 42 + })); + Test_utils.eq("File \"early_return_test.res\", line 78, characters 7-14", "ok", getGadt({ + TAG: "String", + _0: "ok" + })); + Test_utils.eq("File \"early_return_test.res\", line 79, characters 7-14", 42, choose(1)); + Test_utils.eq("File \"early_return_test.res\", line 80, characters 7-14", 7, choose(0)); + Test_utils.eq("File \"early_return_test.res\", line 81, characters 7-14", 42, identity(42)); + Test_utils.eq("File \"early_return_test.res\", line 82, characters 7-14", "ok", identity("ok")); + }); + Mocha.test("calls preserve the callee's return boundary", () => { + Test_utils.eq("File \"early_return_test.res\", line 86, characters 7-14", 43, alwaysReturn(42) + 1 | 0); + Test_utils.eq("File \"early_return_test.res\", line 87, characters 7-14", 43, Early_return_helper.first(1) + 1 | 0); + Test_utils.eq("File \"early_return_test.res\", line 88, characters 7-14", 8, Early_return_helper.first(0) + 1 | 0); + let outer = () => { + let inner = () => "inner"; + Test_utils.eq("File \"early_return_test.res\", line 91, characters 9-16", "inner", inner()); + return 42; + }; + Test_utils.eq("File \"early_return_test.res\", line 94, characters 7-14", 42, outer()); + Test_utils.eq("File \"early_return_test.res\", line 95, characters 7-14", 43, (() => 42)() + 1 | 0); + }); + Mocha.test("return from nested loops and switches", () => { + Test_utils.eq("File \"early_return_test.res\", line 99, characters 7-14", 1, inLoops("first")); + Test_utils.eq("File \"early_return_test.res\", line 100, characters 7-14", 20, inLoops("second")); + Test_utils.eq("File \"early_return_test.res\", line 101, characters 7-14", 99, inLoops("third")); + Test_utils.eq("File \"early_return_test.res\", line 102, characters 7-14", 99, inLoops("other")); + }); + Mocha.test("return in while conditions stays in the source function", () => { + let f = limit => { + let i = 0; + while (true) { + if (i === limit) { + return i; + } + if (i >= 3) { + break; + } + i = i + 1 | 0; + continue; + }; + return 99; + }; + Test_utils.eq("File \"early_return_test.res\", line 119, characters 7-14", 2, f(2)); + Test_utils.eq("File \"early_return_test.res\", line 120, characters 7-14", 99, f(5)); + let outerBreak = early => { + let i = 0; + loop_0: while (i < 5) { + i = i + 1 | 0; + while (true) { + if (i === 2) { + break loop_0; + } + if (early) { + return 42; + } + break; + }; + }; + return i; + }; + Test_utils.eq("File \"early_return_test.res\", line 139, characters 7-14", 2, outerBreak(false)); + Test_utils.eq("File \"early_return_test.res\", line 140, characters 7-14", 42, outerBreak(true)); + }); + Mocha.test("return is not caught but operand exceptions are", () => { + Test_utils.eq("File \"early_return_test.res\", line 144, characters 7-14", 42, inTry(false)); + Test_utils.eq("File \"early_return_test.res\", line 145, characters 7-14", 7, inTry(true)); + }); + Mocha.test("expression position preserves evaluation and skips later effects", () => { + let seen = /* [] */0; + let effect = n => { + seen = { + hd: n, + tl: seen + }; + return n; + }; + let f = early => { + let first = effect(1); + let tmp; + if (early) { + return effect(2); + } + tmp = effect(3); + effect(4); + return first + tmp | 0; + }; + Test_utils.eq("File \"early_return_test.res\", line 168, characters 7-14", 2, f(true)); + Test_utils.eq("File \"early_return_test.res\", line 169, characters 7-14", { + hd: 2, + tl: { + hd: 1, + tl: /* [] */0 + } + }, seen); + seen = /* [] */0; + Test_utils.eq("File \"early_return_test.res\", line 171, characters 7-14", 4, f(false)); + Test_utils.eq("File \"early_return_test.res\", line 172, characters 7-14", { + hd: 4, + tl: { + hd: 3, + tl: { + hd: 1, + tl: /* [] */0 + } + } + }, seen); + }); + Mocha.test("return from for-of closes the iterator", () => { + let closed = false; + let values = (onClose => (function* () { + try { yield 1; yield 2; } + finally { onClose(); } + })()); + let f = () => { + for (let value of values(() => { + closed = true; + })) { + return value; + } + return 0; + }; + Test_utils.eq("File \"early_return_test.res\", line 189, characters 7-14", 1, f()); + Test_utils.eq("File \"early_return_test.res\", line 190, characters 7-14", true, closed); + }); + Mocha.test("short circuit and unit return", () => { + let seen = 0; + let f = enabled => { + let tmp = false; + if (enabled) { + return 42; + tmp = undefined; + } + return 7; + }; + let stop = () => {}; + stop(); + Test_utils.eq("File \"early_return_test.res\", line 206, characters 7-14", 0, seen); + Test_utils.eq("File \"early_return_test.res\", line 207, characters 7-14", 42, f(true)); + Test_utils.eq("File \"early_return_test.res\", line 208, characters 7-14", 7, f(false)); + }); + Mocha.test("async returns", async () => { + Test_utils.eq("File \"early_return_test.res\", line 212, characters 7-14", 42, await returnAsync(true)); + return Test_utils.eq("File \"early_return_test.res\", line 213, characters 7-14", 7, await returnAsync(false)); + }); +}); + +export { + choose, + alwaysReturn, + identity, + inLoops, + returnAsync, + getGadt, + OperandFailure, + inTry, +} +/* Not a pure module */ diff --git a/tests/tests/src/early_return_test.res b/tests/tests/src/early_return_test.res new file mode 100644 index 0000000000..eaf3169a45 --- /dev/null +++ b/tests/tests/src/early_return_test.res @@ -0,0 +1,215 @@ +open Mocha +open Test_utils + +@module("mocha") +external testAsync: (string, unit => promise) => unit = "test" + +// The extension is a temporary encoding, independent of eventual surface syntax. +let choose = x => { + if x > 0 { + %return(42) + } + 7 +} + +@inline +let alwaysReturn = x => %return(x) + +let identity = x => { + if true { + %return(x) + } + x +} + +let inLoops = target => { + for i in 0 to 3 { + let j = ref(0) + while j.contents < 3 { + j.contents = j.contents + 1 + switch target { + | "first" => %return(i + j.contents) + | "second" => + if i == 2 { + %return(20) + } + | "third" => continue + | _ => break + } + } + } + 99 +} + +let returnAsync = async early => { + if early { + %return(await Promise.resolve(42)) + } + 7 +} + +type rec value<'a> = Int(int): value | String(string): value +let getGadt = (type a, v: value): a => { + switch v { + | Int(x) => %return(x) + | String(x) => %return(x) + } +} + +exception OperandFailure + +let inTry = fail => { + try { + %return( + if fail { + throw(OperandFailure) + } else { + 42 + } + ) + } catch { + | OperandFailure => %return(7) + } +} + +describe(__MODULE__, () => { + test("early and implicit results", () => { + eq(__LOC__, 42, getGadt(Int(42))) + eq(__LOC__, "ok", getGadt(String("ok"))) + eq(__LOC__, 42, choose(1)) + eq(__LOC__, 7, choose(0)) + eq(__LOC__, 42, identity(42)) + eq(__LOC__, "ok", identity("ok")) + }) + + test("calls preserve the callee's return boundary", () => { + eq(__LOC__, 43, alwaysReturn(42) + 1) + eq(__LOC__, 43, Early_return_helper.first(1) + 1) + eq(__LOC__, 8, Early_return_helper.first(0) + 1) + let outer = () => { + let inner = () => %return("inner") + eq(__LOC__, "inner", inner()) + %return(42) + } + eq(__LOC__, 42, outer()) + eq(__LOC__, 43, (() => %return(42))() + 1) + }) + + test("return from nested loops and switches", () => { + eq(__LOC__, 1, inLoops("first")) + eq(__LOC__, 20, inLoops("second")) + eq(__LOC__, 99, inLoops("third")) + eq(__LOC__, 99, inLoops("other")) + }) + + test("return in while conditions stays in the source function", () => { + let f = limit => { + let i = ref(0) + while { + if i.contents == limit { + %return(i.contents) + } + i.contents < 3 + } { + i.contents = i.contents + 1 + continue + } + 99 + } + eq(__LOC__, 2, f(2)) + eq(__LOC__, 99, f(5)) + let outerBreak = early => { + let i = ref(0) + while i.contents < 5 { + i.contents = i.contents + 1 + while { + if i.contents == 2 { + break + } + if early { + %return(42) + } + true + } { + break + } + } + i.contents + } + eq(__LOC__, 2, outerBreak(false)) + eq(__LOC__, 42, outerBreak(true)) + }) + + test("return is not caught but operand exceptions are", () => { + eq(__LOC__, 42, inTry(false)) + eq(__LOC__, 7, inTry(true)) + }) + + test("expression position preserves evaluation and skips later effects", () => { + let seen = ref(list{}) + let effect = n => { + seen.contents = list{n, ...seen.contents} + n + } + let f = early => { + let first = effect(1) + let pair = ( + first, + if early { + %return(effect(2)) + } else { + effect(3) + }, + ) + ignore(effect(4)) + let (a, b) = pair + a + b + } + eq(__LOC__, 2, f(true)) + eq(__LOC__, list{2, 1}, seen.contents) + seen.contents = list{} + eq(__LOC__, 4, f(false)) + eq(__LOC__, list{4, 3, 1}, seen.contents) + }) + + test("return from for-of closes the iterator", () => { + let closed = ref(false) + let values: (unit => unit) => iterable = %raw(` + onClose => (function* () { + try { yield 1; yield 2; } + finally { onClose(); } + })() + `) + let f = () => { + for value of values(() => {closed.contents = true}) { + %return(value) + } + 0 + } + eq(__LOC__, 1, f()) + eq(__LOC__, true, closed.contents) + }) + + test("short circuit and unit return", () => { + let seen = ref(0) + let f = enabled => { + ignore(enabled && {%return(42)}) + 7 + } + let stop = () => { + if true { + %return(()) + } + seen.contents = 1 + } + stop() + eq(__LOC__, 0, seen.contents) + eq(__LOC__, 42, f(true)) + eq(__LOC__, 7, f(false)) + }) + + testAsync("async returns", async () => { + eq(__LOC__, 42, await returnAsync(true)) + eq(__LOC__, 7, await returnAsync(false)) + }) +}) From 162fba647dfc7f6cd5bf8910d64301e8b68a6d7c Mon Sep 17 00:00:00 2001 From: Cristiano Calcagno Date: Mon, 14 Sep 2026 04:56:32 +0200 Subject: [PATCH 2/2] Link early-return changelog entry to PR 8656 Signed-off-by: Cristiano Calcagno --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 530e3b53b5..40faca6a09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,7 @@ #### :rocket: New Feature -- Add early return from function bodies, provisionally encoded as `%return(value)`, including loops and async functions. https://github.com/rescript-lang/rescript/pull/TBD +- Add early return from function bodies, provisionally encoded as `%return(value)`, including loops and async functions. https://github.com/rescript-lang/rescript/pull/8656 - Support UTF-16 surrogate-pair escapes such as `"\uD83D\uDE00"` in ordinary string literals. https://github.com/rescript-lang/rescript/pull/8606 - Support dynamic imports of external bindings annotated with `@scope`; the generated import follows the complete property path. These imports were previously rejected. https://github.com/rescript-lang/rescript/pull/8582 - Add `@res.hoistedFunction` for emitting nested module functions as flat JavaScript exports. https://github.com/rescript-lang/rescript/pull/8402