Skip to content

Commit 3ed0e72

Browse files
committed
Add Parsetree-level Option stdlib optimizations (forEach/map/flatMap)
1 parent fbb8a4e commit 3ed0e72

9 files changed

+830
-16
lines changed
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
open Parsetree
2+
open Longident
3+
4+
(*
5+
Optimise calls to Option.forEach/map/flatMap so they produce the same switch
6+
structure as handwritten code. We only rewrite calls whose callback is a
7+
simple literal lambda or identifier; more complex callbacks are left intact
8+
to preserve ReScript's call-by-value semantics.
9+
*)
10+
11+
let value_name = "__res_option_value"
12+
13+
type option_call = ForEach | Map | FlatMap
14+
15+
(* Inlineable callbacks are bare identifiers (possibly wrapped in coercions or
16+
type annotations). Those can be applied directly inside the emitted switch
17+
without introducing a let-binding that might change evaluation behaviour. *)
18+
let rec callback_is_inlineable expr =
19+
match expr.pexp_desc with
20+
| Pexp_ident _ -> true
21+
| Pexp_constraint (inner, _) | Pexp_coerce (inner, _, _) ->
22+
callback_is_inlineable inner
23+
| _ -> false
24+
25+
(* Detect literal lambdas (ignoring type annotations) so we can reuse their
26+
argument binder in the rewritten switch. *)
27+
let rec inline_lambda expr =
28+
match expr.pexp_desc with
29+
| Pexp_constraint (inner, _) | Pexp_coerce (inner, _, _) ->
30+
inline_lambda inner
31+
| Pexp_fun {arg_label = Asttypes.Nolabel; lhs; rhs; async = false} ->
32+
Some (lhs, rhs)
33+
| _ -> None
34+
35+
let transform (expr : Parsetree.expression) : Parsetree.expression =
36+
match expr.pexp_desc with
37+
| Pexp_apply
38+
{
39+
funct =
40+
{
41+
pexp_desc =
42+
Pexp_ident
43+
{txt = Ldot (Lident ("Option" | "Stdlib_Option"), fname)};
44+
};
45+
args = [(_, opt_expr); (_, func_expr)];
46+
} -> (
47+
let call_kind =
48+
match fname with
49+
| "forEach" -> Some ForEach
50+
| "map" -> Some Map
51+
| "flatMap" -> Some FlatMap
52+
| _ -> None
53+
in
54+
match call_kind with
55+
| None -> expr
56+
| Some call_kind -> (
57+
let loc_ghost = {expr.pexp_loc with loc_ghost = true} in
58+
let emit_option_match value_pat result_expr =
59+
let some_rhs =
60+
match call_kind with
61+
| ForEach | FlatMap -> result_expr
62+
| Map ->
63+
Ast_helper.Exp.construct ~loc:loc_ghost
64+
{txt = Lident "Some"; loc = loc_ghost}
65+
(Some result_expr)
66+
in
67+
let none_rhs =
68+
match call_kind with
69+
| ForEach ->
70+
Ast_helper.Exp.construct ~loc:loc_ghost
71+
{txt = Lident "()"; loc = loc_ghost}
72+
None
73+
| Map | FlatMap ->
74+
Ast_helper.Exp.construct ~loc:loc_ghost
75+
{txt = Lident "None"; loc = loc_ghost}
76+
None
77+
in
78+
let mk_case ctor payload rhs =
79+
{
80+
Parsetree.pc_bar = None;
81+
pc_lhs =
82+
Ast_helper.Pat.construct ~loc:loc_ghost
83+
{txt = Lident ctor; loc = loc_ghost}
84+
payload;
85+
pc_guard = None;
86+
pc_rhs = rhs;
87+
}
88+
in
89+
let some_case = mk_case "Some" (Some value_pat) some_rhs in
90+
let none_case = mk_case "None" None none_rhs in
91+
let transformed =
92+
Ast_helper.Exp.match_ ~loc:loc_ghost opt_expr [some_case; none_case]
93+
in
94+
{
95+
transformed with
96+
pexp_loc = expr.pexp_loc;
97+
pexp_attributes = expr.pexp_attributes;
98+
}
99+
in
100+
match inline_lambda func_expr with
101+
(* Literal lambda with a simple binder: reuse the binder directly inside
102+
the generated switch, so the body runs exactly once with the option's
103+
payload. *)
104+
| Some ({ppat_desc = Parsetree.Ppat_var {txt}}, body) ->
105+
let value_pat =
106+
Ast_helper.Pat.var ~loc:loc_ghost {txt; loc = loc_ghost}
107+
in
108+
emit_option_match value_pat body
109+
(* Callback is a simple identifier (possibly annotated). Apply it inside
110+
the switch so evaluation order matches handwritten code. *)
111+
| _ when callback_is_inlineable func_expr ->
112+
let value_pat =
113+
Ast_helper.Pat.var ~loc:loc_ghost {txt = value_name; loc = loc_ghost}
114+
in
115+
let value_ident =
116+
Ast_helper.Exp.ident ~loc:loc_ghost
117+
{txt = Lident value_name; loc = loc_ghost}
118+
in
119+
let apply_callback =
120+
Ast_helper.Exp.apply ~loc:loc_ghost func_expr
121+
[(Asttypes.Nolabel, value_ident)]
122+
in
123+
emit_option_match value_pat apply_callback
124+
(* Complex callbacks are left as-is so we don't change when they run. *)
125+
| _ -> expr))
126+
| _ -> expr
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
val transform : Parsetree.expression -> Parsetree.expression

compiler/frontend/bs_builtin_ppx.ml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@ let expr_mapper ~async_context ~in_function_def (self : mapper)
112112
body;
113113
pexp_attributes;
114114
})
115-
| Pexp_apply _ -> Ast_exp_apply.app_exp_mapper e self
115+
| Pexp_apply _ ->
116+
Ast_exp_apply.app_exp_mapper e self |> Ast_option_optimizations.transform
116117
| Pexp_match
117118
( b,
118119
[

tests/tests/src/core/Core_ObjectTests.mjs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as Test from "./Test.mjs";
44
import * as Stdlib_BigInt from "@rescript/runtime/lib/es6/Stdlib_BigInt.js";
55
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
66
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
7+
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
78

89
let eq = Primitive_object.equal;
910

@@ -530,10 +531,13 @@ runGetTest({
530531
3
531532
]
532533
}),
533-
get: i => Stdlib_Option.getOr(Stdlib_Option.map(i["a"], i => i.concat([
534-
4,
535-
5
536-
])), []),
534+
get: i => {
535+
let i$1 = i["a"];
536+
return Stdlib_Option.getOr(i$1 !== undefined ? Primitive_option.valFromOption(i$1).concat([
537+
4,
538+
5
539+
]) : undefined, []);
540+
},
537541
expected: [
538542
1,
539543
2,

tests/tests/src/core/Core_TempTests.mjs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -205,13 +205,11 @@ console.log(regex.test(string));
205205

206206
let result = regex.exec(string);
207207

208-
let result$1 = (result == null) ? undefined : Primitive_option.some(result);
209-
210-
console.log(Stdlib_Option.map(result$1, prim => prim.input));
208+
console.log(!(result == null) ? result.input : undefined);
211209

212-
console.log(Stdlib_Option.map(result$1, prim => prim.index));
210+
console.log(!(result == null) ? result.index : undefined);
213211

214-
console.log(Stdlib_Option.map(result$1, prim => prim.slice(1)));
212+
console.log(!(result == null) ? result.slice(1) : undefined);
215213

216214
console.info("");
217215

@@ -322,6 +320,8 @@ let formatter = Core_IntlTests.formatter;
322320

323321
let segments = Core_IntlTests.segments;
324322

323+
let result$1 = (result == null) ? undefined : Primitive_option.some(result);
324+
325325
export {
326326
_collator,
327327
collator,

tests/tests/src/core/intl/Core_IntlTests.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Generated by ReScript, PLEASE EDIT WITH CARE
22

33
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
4-
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
54
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
65
import * as Core_Intl_LocaleTest from "./Core_Intl_LocaleTest.mjs";
76
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
@@ -58,7 +57,8 @@ try {
5857
let e$2 = Primitive_exceptions.internalToException(raw_e$2);
5958
if (e$2.RE_EXN_ID === "JsExn") {
6059
let e$3 = e$2._1;
61-
let message = Stdlib_Option.map(Stdlib_JsExn.message(e$3), prim => prim.toLowerCase());
60+
let __res_option_value = Stdlib_JsExn.message(e$3);
61+
let message = __res_option_value !== undefined ? __res_option_value.toLowerCase() : undefined;
6262
let exit = 0;
6363
if (message === "invalid key : someinvalidkey") {
6464
console.log("Caught expected error");

0 commit comments

Comments
 (0)