diff --git a/AGENTS.md b/AGENTS.md index 725007dd7b..743b856eef 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,6 +30,8 @@ make format && make checkformat - **Do not introduce new keywords unless absolutely necessary** - Try to find ways to implement features without reserving keywords, as seen with the "catch" implementation that avoids making it a keyword. +- **No underscore if not matching on all record fields** - Do not add "; _" to pattern matches in the compiler code. OCaml warning 9 (missing fields in a record pattern) is disabled in this project. + ## Compiler Architecture ### Compilation Pipeline diff --git a/compiler/frontend/ast_option_optimizations.ml b/compiler/frontend/ast_option_optimizations.ml deleted file mode 100644 index 4ca14651db..0000000000 --- a/compiler/frontend/ast_option_optimizations.ml +++ /dev/null @@ -1,126 +0,0 @@ -open Parsetree -open Longident - -(* - Optimise calls to Option.forEach/map/flatMap so they produce the same switch - structure as handwritten code. We only rewrite calls whose callback is a - simple literal lambda or identifier; more complex callbacks are left intact - to preserve ReScript's call-by-value semantics. -*) - -let value_name = "__res_option_value" - -type option_call = ForEach | Map | FlatMap - -(* Inlineable callbacks are bare identifiers (possibly wrapped in coercions or - type annotations). Those can be applied directly inside the emitted switch - without introducing a let-binding that might change evaluation behaviour. *) -let rec callback_is_inlineable expr = - match expr.pexp_desc with - | Pexp_ident _ -> true - | Pexp_constraint (inner, _) | Pexp_coerce (inner, _, _) -> - callback_is_inlineable inner - | _ -> false - -(* Detect literal lambdas (ignoring type annotations) so we can reuse their - argument binder in the rewritten switch. *) -let rec inline_lambda expr = - match expr.pexp_desc with - | Pexp_constraint (inner, _) | Pexp_coerce (inner, _, _) -> - inline_lambda inner - | Pexp_fun {arg_label = Asttypes.Nolabel; lhs; rhs; async = false} -> - Some (lhs, rhs) - | _ -> None - -let transform (expr : Parsetree.expression) : Parsetree.expression = - match expr.pexp_desc with - | Pexp_apply - { - funct = - { - pexp_desc = - Pexp_ident - {txt = Ldot (Lident ("Option" | "Stdlib_Option"), fname)}; - }; - args = [(_, opt_expr); (_, func_expr)]; - } -> ( - let call_kind = - match fname with - | "forEach" -> Some ForEach - | "map" -> Some Map - | "flatMap" -> Some FlatMap - | _ -> None - in - match call_kind with - | None -> expr - | Some call_kind -> ( - let loc_ghost = {expr.pexp_loc with loc_ghost = true} in - let emit_option_match value_pat result_expr = - let some_rhs = - match call_kind with - | ForEach | FlatMap -> result_expr - | Map -> - Ast_helper.Exp.construct ~loc:loc_ghost - {txt = Lident "Some"; loc = loc_ghost} - (Some result_expr) - in - let none_rhs = - match call_kind with - | ForEach -> - Ast_helper.Exp.construct ~loc:loc_ghost - {txt = Lident "()"; loc = loc_ghost} - None - | Map | FlatMap -> - Ast_helper.Exp.construct ~loc:loc_ghost - {txt = Lident "None"; loc = loc_ghost} - None - in - let mk_case ctor payload rhs = - { - Parsetree.pc_bar = None; - pc_lhs = - Ast_helper.Pat.construct ~loc:loc_ghost - {txt = Lident ctor; loc = loc_ghost} - payload; - pc_guard = None; - pc_rhs = rhs; - } - in - let some_case = mk_case "Some" (Some value_pat) some_rhs in - let none_case = mk_case "None" None none_rhs in - let transformed = - Ast_helper.Exp.match_ ~loc:loc_ghost opt_expr [some_case; none_case] - in - { - transformed with - pexp_loc = expr.pexp_loc; - pexp_attributes = expr.pexp_attributes; - } - in - match inline_lambda func_expr with - (* Literal lambda with a simple binder: reuse the binder directly inside - the generated switch, so the body runs exactly once with the option's - payload. *) - | Some ({ppat_desc = Parsetree.Ppat_var {txt}}, body) -> - let value_pat = - Ast_helper.Pat.var ~loc:loc_ghost {txt; loc = loc_ghost} - in - emit_option_match value_pat body - (* Callback is a simple identifier (possibly annotated). Apply it inside - the switch so evaluation order matches handwritten code. *) - | _ when callback_is_inlineable func_expr -> - let value_pat = - Ast_helper.Pat.var ~loc:loc_ghost {txt = value_name; loc = loc_ghost} - in - let value_ident = - Ast_helper.Exp.ident ~loc:loc_ghost - {txt = Lident value_name; loc = loc_ghost} - in - let apply_callback = - Ast_helper.Exp.apply ~loc:loc_ghost func_expr - [(Asttypes.Nolabel, value_ident)] - in - emit_option_match value_pat apply_callback - (* Complex callbacks are left as-is so we don't change when they run. *) - | _ -> expr)) - | _ -> expr diff --git a/compiler/frontend/ast_option_optimizations.mli b/compiler/frontend/ast_option_optimizations.mli deleted file mode 100644 index 84ee077695..0000000000 --- a/compiler/frontend/ast_option_optimizations.mli +++ /dev/null @@ -1 +0,0 @@ -val transform : Parsetree.expression -> Parsetree.expression diff --git a/compiler/frontend/bs_builtin_ppx.ml b/compiler/frontend/bs_builtin_ppx.ml index 3777aa37c6..08ec91fc62 100644 --- a/compiler/frontend/bs_builtin_ppx.ml +++ b/compiler/frontend/bs_builtin_ppx.ml @@ -112,8 +112,7 @@ let expr_mapper ~async_context ~in_function_def (self : mapper) body; pexp_attributes; }) - | Pexp_apply _ -> - Ast_exp_apply.app_exp_mapper e self |> Ast_option_optimizations.transform + | Pexp_apply _ -> Ast_exp_apply.app_exp_mapper e self | Pexp_match ( b, [ diff --git a/compiler/ml/translcore.ml b/compiler/ml/translcore.ml index 078cbf133a..aa194eaee3 100644 --- a/compiler/ml/translcore.ml +++ b/compiler/ml/translcore.ml @@ -646,6 +646,113 @@ let rec cut n l = let try_ids = Hashtbl.create 8 +(* Recompute metadata needed for inlining Stdlib.Option helpers at translation + time; the typed tree only marks such applications with a boolean flag. *) +type stdlib_option_call_kind = + | Stdlib_option_forEach + | Stdlib_option_map of {result_cannot_contain_undefined: bool} + | Stdlib_option_flatMap + +type stdlib_option_callback = + | Stdlib_option_inline_lambda of {param: Ident.t; body: expression} + | Stdlib_option_inline_ident of expression + +type stdlib_option_call = { + callback: stdlib_option_callback; + call_kind: stdlib_option_call_kind; + payload_not_nested: bool; +} + +type stdlib_option_fun_kind = + | Stdlib_option_fun_forEach + | Stdlib_option_fun_map + | Stdlib_option_fun_flatMap + +let stdlib_option_fun_of_path env path = + match Path.last path with + | ("forEach" | "map" | "flatMap") as fname -> ( + let canonical = Env.normalize_path_prefix None env path in + match canonical with + | Path.Pdot (Path.Pident module_ident, _, _) + when Ident.name module_ident = "Stdlib_Option" + || Ident.name module_ident = "Belt_Option" -> ( + match fname with + | "forEach" -> Some Stdlib_option_fun_forEach + | "map" -> Some Stdlib_option_fun_map + | "flatMap" -> Some Stdlib_option_fun_flatMap + | _ -> None) + | _ -> None) + | _ -> None + +let inline_lambda_callback (expr : expression) : stdlib_option_callback option = + match expr.exp_desc with + | Texp_function {arg_label = Nolabel; case; partial = Total; async = false; _} + when Option.is_none case.c_guard -> ( + match case.c_lhs.pat_desc with + | Tpat_var (param, _) -> + Some (Stdlib_option_inline_lambda {param; body = case.c_rhs}) + | _ -> None) + | _ -> None + +let inline_ident_callback (expr : expression) : stdlib_option_callback option = + match expr.exp_desc with + | Texp_ident _ -> Some (Stdlib_option_inline_ident expr) + | _ -> None + +let callback_return_type env (expr : expression) = + match (Ctype.expand_head env expr.exp_type).desc with + | Tarrow (_, ret_ty, _, _) -> Some ret_ty + | _ -> None + +let detect_stdlib_option_call env (funct : expression) + (args : (Noloc.arg_label * expression option) list) : + stdlib_option_call option = + match funct.exp_desc with + | Texp_ident (path, _, _) -> ( + match stdlib_option_fun_of_path env path with + | None -> None + | Some fun_kind -> ( + match args with + | [(Nolabel, Some opt_expr); (Nolabel, Some callback_expr)] -> ( + let callback_info = + match inline_lambda_callback callback_expr with + | Some info -> Some info + | None -> inline_ident_callback callback_expr + in + match callback_info with + | None -> None + | Some callback -> + let payload_not_nested = + match (Ctype.expand_head env opt_expr.exp_type).desc with + | Tconstr (path, [payload_ty], _) + when Path.same path Predef.path_option -> + Typeopt.type_cannot_contain_undefined payload_ty env + | _ -> false + in + let call_kind = + match fun_kind with + | Stdlib_option_fun_forEach -> Stdlib_option_forEach + | Stdlib_option_fun_map -> + let result_cannot_contain_undefined = + match callback with + | Stdlib_option_inline_lambda {body} -> + Typeopt.type_cannot_contain_undefined body.exp_type + body.exp_env + | Stdlib_option_inline_ident cb -> ( + match callback_return_type cb.exp_env cb with + | Some ret_ty -> + Typeopt.type_cannot_contain_undefined ret_ty cb.exp_env + | None -> false) + in + Stdlib_option_map {result_cannot_contain_undefined} + | Stdlib_option_fun_flatMap -> Stdlib_option_flatMap + in + Some {callback; call_kind; payload_not_nested}) + | _ -> None)) + | _ -> None + +let lambda_none = Lconst (Const_pointer (0, Pt_shape_none)) + let extract_directive_for_fn exp = exp.exp_attributes |> List.find_map (fun ({txt}, payload) -> @@ -755,10 +862,11 @@ and transl_exp0 (e : Typedtree.expression) : Lambda.lambda = (Lprim (Pccall (set_transformed_jsx d ~transformed_jsx), argl, e.exp_loc)) | _ -> wrap (Lprim (prim, argl, e.exp_loc)))) - | Texp_apply {funct; args = oargs; partial; transformed_jsx} -> + | Texp_apply {funct; args = oargs; partial; transformed_jsx} -> ( let inlined, funct = Translattribute.get_and_remove_inlined_attribute funct in + let option_call_info = detect_stdlib_option_call e.exp_env funct oargs in let uncurried_partial_application = (* In case of partial application foo(args, ...) when some args are missing, get the arity *) @@ -771,8 +879,17 @@ and transl_exp0 (e : Typedtree.expression) : Lambda.lambda = | None -> None else None in - transl_apply ~inlined ~uncurried_partial_application ~transformed_jsx - (transl_exp funct) oargs e.exp_loc + match option_call_info with + | Some info when not partial -> ( + match oargs with + | (Nolabel, Some opt_expr) :: _ -> + transl_stdlib_option_call e opt_expr info oargs + | _ -> + transl_apply ~inlined ~uncurried_partial_application ~transformed_jsx + (transl_exp funct) oargs e.exp_loc) + | _ -> + transl_apply ~inlined ~uncurried_partial_application ~transformed_jsx + (transl_exp funct) oargs 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) -> @@ -924,6 +1041,64 @@ and transl_exp0 (e : Typedtree.expression) : Lambda.lambda = if !Clflags.noassert then lambda_unit else Lifthenelse (transl_exp cond, lambda_unit, assert_failed e) +and bind_option_value ~payload_not_nested opt_var opt_loc callback = + let value_expr = + if payload_not_nested then + Lprim (Pval_from_option_not_nest, [opt_var], opt_loc) + else Lprim (Pval_from_option, [opt_var], opt_loc) + in + match callback with + | Stdlib_option_inline_lambda {param; body} -> + bind Strict param value_expr (transl_exp body) + | Stdlib_option_inline_ident expr -> + let func = transl_exp expr in + let value_id = Ident.create "__res_option_value" in + let apply = + Lapply + { + ap_func = func; + ap_args = [Lvar value_id]; + ap_inlined = Default_inline; + ap_loc = expr.exp_loc; + ap_transformed_jsx = false; + } + in + bind Strict value_id value_expr apply + +and transl_stdlib_option_call exp opt_expr info oargs = + match oargs with + | (Nolabel, Some _) :: (Nolabel, Some _) :: _ | (Nolabel, Some _) :: [] -> + let opt_lam = transl_exp opt_expr in + let opt_id = + match info.callback with + | Stdlib_option_inline_lambda {param} -> Ident.create (Ident.name param) + | _ -> Ident.create "__res_option_value" + in + let opt_var = Lvar opt_id in + let callback_result = + bind_option_value ~payload_not_nested:info.payload_not_nested opt_var + exp.exp_loc info.callback + in + let some_branch = + match info.call_kind with + | Stdlib_option_forEach -> callback_result + | Stdlib_option_map {result_cannot_contain_undefined} -> + let tag = + if result_cannot_contain_undefined then Blk_some_not_nested + else Blk_some + in + Lprim (Pmakeblock tag, [callback_result], exp.exp_loc) + | Stdlib_option_flatMap -> callback_result + in + let none_branch = + match info.call_kind with + | Stdlib_option_forEach -> lambda_unit + | Stdlib_option_map _ | Stdlib_option_flatMap -> lambda_none + in + let cond = Lprim (Pis_not_none, [opt_var], exp.exp_loc) in + bind Strict opt_id opt_lam (Lifthenelse (cond, some_branch, none_branch)) + | _ -> assert false + and transl_list expr_list = List.map transl_exp expr_list and transl_guard guard rhs = diff --git a/compiler/ml/typecore.ml b/compiler/ml/typecore.ml index 90522f13d2..e93537a8a1 100644 --- a/compiler/ml/typecore.ml +++ b/compiler/ml/typecore.ml @@ -2466,8 +2466,8 @@ and type_expect_ ~context ?in_function ?(recarg = Rejected) env sexp ty_expected | _ -> false in - if fully_applied && not is_primitive then rue (mk_apply funct args) - else rue (mk_apply funct args) + if fully_applied && not is_primitive then mk_apply funct args + else mk_apply funct args | Pexp_match (sarg, caselist) -> begin_def (); let arg = type_exp ~context:None env sarg in diff --git a/tests/tests/src/core/Core_ObjectTests.mjs b/tests/tests/src/core/Core_ObjectTests.mjs index 31b7c9569d..6c788d1009 100644 --- a/tests/tests/src/core/Core_ObjectTests.mjs +++ b/tests/tests/src/core/Core_ObjectTests.mjs @@ -4,7 +4,6 @@ import * as Test from "./Test.mjs"; import * as Stdlib_BigInt from "@rescript/runtime/lib/es6/Stdlib_BigInt.js"; import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js"; import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js"; -import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js"; let eq = Primitive_object.equal; @@ -533,7 +532,7 @@ runGetTest({ }), get: i => { let i$1 = i["a"]; - return Stdlib_Option.getOr(i$1 !== undefined ? Primitive_option.valFromOption(i$1).concat([ + return Stdlib_Option.getOr(i$1 !== undefined ? i$1.concat([ 4, 5 ]) : undefined, []); diff --git a/tests/tests/src/js_string_test.mjs b/tests/tests/src/js_string_test.mjs index a5cf627eb9..dda37d7652 100644 --- a/tests/tests/src/js_string_test.mjs +++ b/tests/tests/src/js_string_test.mjs @@ -3,7 +3,6 @@ import * as Mocha from "mocha"; import * as Js_string from "@rescript/runtime/lib/es6/Js_string.js"; import * as Test_utils from "./test_utils.mjs"; -import * as Belt_Option from "@rescript/runtime/lib/es6/Belt_Option.js"; import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js"; Mocha.describe("Js_string_test", () => { @@ -34,10 +33,13 @@ Mocha.describe("Js_string_test", () => { "na" ], Primitive_option.fromNull("banana".match(/na+/g)))); Mocha.test("match - no match", () => Test_utils.eq("File \"js_string_test.res\", line 34, characters 36-43", undefined, Primitive_option.fromNull("banana".match(/nanana+/g)))); - Mocha.test("match - not found capture groups", () => Test_utils.eq("File \"js_string_test.res\", line 37, characters 6-13", [ - "hello ", - undefined - ], Belt_Option.map(Primitive_option.fromNull("hello word".match(/hello (world)?/)), prim => prim.slice()))); + Mocha.test("match - not found capture groups", () => { + let __res_option_value = "hello word".match(/hello (world)?/); + Test_utils.eq("File \"js_string_test.res\", line 37, characters 6-13", [ + "hello ", + undefined + ], __res_option_value !== null ? __res_option_value.slice() : undefined); + }); Mocha.test("normalize", () => Test_utils.eq("File \"js_string_test.res\", line 43, characters 29-36", "foo", "foo".normalize())); Mocha.test("normalizeByForm", () => Test_utils.eq("File \"js_string_test.res\", line 44, characters 35-42", "foo", "foo".normalize("NFKD"))); Mocha.test("repeat", () => Test_utils.eq("File \"js_string_test.res\", line 46, characters 26-33", "foofoofoo", "foo".repeat(3))); diff --git a/tests/tests/src/reactTestUtils.mjs b/tests/tests/src/reactTestUtils.mjs index 6c70cb9ce3..a4a6b61c45 100644 --- a/tests/tests/src/reactTestUtils.mjs +++ b/tests/tests/src/reactTestUtils.mjs @@ -66,7 +66,8 @@ function prepareContainer(container, param) { let containerElement = document.createElement("div"); let body = document.body; if (body !== undefined) { - Primitive_option.some(Primitive_option.valFromOption(body).appendChild(containerElement)); + let body$1 = Primitive_option.valFromOption(body); + Primitive_option.some(body$1.appendChild(containerElement)); } container.contents = Primitive_option.some(containerElement); } @@ -74,7 +75,8 @@ function prepareContainer(container, param) { function cleanupContainer(container, param) { let __res_option_value = container.contents; if (__res_option_value !== undefined) { - Primitive_option.some((Primitive_option.valFromOption(__res_option_value).remove(), undefined)); + let __res_option_value$1 = Primitive_option.valFromOption(__res_option_value); + Primitive_option.some((__res_option_value$1.remove(), undefined)); } container.contents = undefined; }