Skip to content

Commit e0e2ce4

Browse files
cknittcristianoc
andauthored
Option optimization: do not create redundant local vars (#7915)
* Option optimization: do not create redundant local vars * Cleanup * Comment * Address review comments * Don't use Lam_analysis.no_side_effects here * Document refine_let rewrite semantics Signed-off-by: Cristiano Calcagno <[email protected]> * CHANGELOG --------- Signed-off-by: Cristiano Calcagno <[email protected]> Co-authored-by: Cristiano Calcagno <[email protected]>
1 parent 1a8dfdb commit e0e2ce4

File tree

4 files changed

+104
-96
lines changed

4 files changed

+104
-96
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
#### :nail_care: Polish
2828

2929
- Keep track of compiler info during build. https://github.com/rescript-lang/rescript/pull/7889
30+
- Improve option optimization for constants. https://github.com/rescript-lang/rescript/pull/7913
31+
- Option optimization: do not create redundant local vars. https://github.com/rescript-lang/rescript/pull/7915
3032

3133
#### :house: Internal
3234

@@ -50,7 +52,6 @@
5052
- Add (dev-)dependencies to build schema. https://github.com/rescript-lang/rescript/pull/7892
5153
- Dedicated error for dict literal spreads. https://github.com/rescript-lang/rescript/pull/7901
5254
- Dedicated error message for when mixing up `:` and `=` in various positions. https://github.com/rescript-lang/rescript/pull/7900
53-
- Improve option optimization for constants. https://github.com/rescript-lang/rescript/pull/7913
5455

5556
# 12.0.0-beta.11
5657

compiler/core/lam_util.ml

Lines changed: 98 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -38,86 +38,104 @@ let add_required_modules ( x : Ident.t list) (meta : Lam_stats.t) =
3838
*)
3939

4040

41-
(*
42-
It's impossible to have a case like below:
43-
{[
44-
(let export_f = ... in export_f)
45-
]}
46-
Even so, it's still correct
47-
*)
48-
let refine_let
49-
~kind param
50-
(arg : Lam.t) (l : Lam.t) : Lam.t =
51-
52-
match (kind : Lam_compat.let_kind ), arg, l with
53-
| _, _, Lvar w when Ident.same w param
54-
(* let k = xx in k
55-
there is no [rec] so [k] would not appear in [xx]
56-
*)
57-
-> arg (* TODO: optimize here -- it's safe to do substitution here *)
58-
| _, _, Lprim {primitive ; args = [Lvar w]; loc ; _} when Ident.same w param
59-
&& (function | Lam_primitive.Pmakeblock _ -> false | _ -> true) primitive
60-
(* don't inline inside a block *)
61-
-> Lam.prim ~primitive ~args:[arg] loc
62-
(* we can not do this substitution when capttured *)
63-
(* | _, Lvar _, _ -> (\** let u = h in xxx*\) *)
64-
(* (\* assert false *\) *)
65-
(* Ext_log.err "@[substitution >> @]@."; *)
66-
(* let v= subst_lambda (Map_ident.singleton param arg ) l in *)
67-
(* Ext_log.err "@[substitution << @]@."; *)
68-
(* v *)
69-
| _, _, Lapply {ap_func=fn; ap_args = [Lvar w]; ap_info; ap_transformed_jsx} when
70-
Ident.same w param &&
71-
(not (Lam_hit.hit_variable param fn ))
72-
->
73-
(* does not work for multiple args since
74-
evaluation order unspecified, does not apply
75-
for [js] in general, since the scope of js ir is loosen
76-
77-
here we remove the definition of [param]
78-
{[ let k = v in (body) k
79-
]}
80-
#1667 make sure body does not hit k
81-
*)
82-
Lam.apply fn [arg] ap_info ~ap_transformed_jsx
83-
| (Strict | StrictOpt ),
84-
( Lvar _ | Lconst _ |
85-
Lprim {primitive = Pfield (_ , Fld_module _) ;
86-
args = [ Lglobal_module _ | Lvar _ ]; _}) , _ ->
87-
(* (match arg with *)
88-
(* | Lconst _ -> *)
89-
(* Ext_log.err "@[%a %s@]@." *)
90-
(* Ident.print param (string_of_lambda arg) *)
91-
(* | _ -> ()); *)
92-
(* No side effect and does not depend on store,
93-
since function evaluation is always delayed
94-
*)
95-
Lam.let_ Alias param arg l
96-
| ( (Strict | StrictOpt ) ), (Lfunction _ ), _ ->
97-
(*It can be promoted to [Alias], however,
98-
we don't want to do this, since we don't want the
99-
function to be inlined to a block, for example
100-
{[
101-
let f = fun _ -> 1 in
102-
[0, f]
103-
]}
104-
TODO: punish inliner to inline functions
105-
into a block
106-
*)
107-
Lam.let_ StrictOpt param arg l
108-
(* Not the case, the block itself can have side effects
109-
we can apply [no_side_effects] pass
110-
| Some Strict, Lprim(Pmakeblock (_,_,Immutable),_) ->
111-
Llet(StrictOpt, param, arg, l)
112-
*)
113-
| Strict, _ ,_ when Lam_analysis.no_side_effects arg ->
114-
Lam.let_ StrictOpt param arg l
115-
| Variable, _, _ ->
116-
Lam.let_ Variable param arg l
117-
| kind, _, _ ->
118-
Lam.let_ kind param arg l
119-
(* | None , _, _ ->
120-
Lam.let_ Strict param arg l *)
41+
(* refine_let normalises let-bindings so we avoid redundant locals while
42+
preserving the semantics encoded by Lambda's let_kind. Downstream passes at
43+
the JS backend interpret the k-tag as the shape of code they are allowed to
44+
emit:
45+
Strict --> emit `const x = e; body`, with `e` evaluated exactly once.
46+
Reordering `e` or duplicating it would be incorrect.
47+
StrictOpt --> emit either `const x = e; body` (when `x` is used) or drop
48+
the declaration entirely (when DCE prunes `x`). Duplicating
49+
`e` remains forbidden.
50+
Alias --> emit `const x = e; body` or substitute `e` directly at each
51+
use site, removing the binding if convenient.
52+
Variable --> emit a thunked shape like `function() { return e; }` or keep
53+
the original `let` without forcing; evaluation must stay
54+
deferred.
55+
56+
The function implements this contract through ordered rewrite clauses:
57+
- (Return) [let[k] x = e in x] ⟶ e
58+
- (Prim) [let[k] x = e in prim p x] ⟶ prim p e (p ≠ makeblock)
59+
- (Call) [let[k] x = e in f x] ⟶ f e (x not captured in f)
60+
- (Alias) [let[k] x = e in body] ⟶ let[Alias] x = e in body
61+
when k ∈ {Strict, StrictOpt} and SafeAlias(e)
62+
- (Strict λ) [let[Strict] x = fn in body] ⟶ let[StrictOpt] x = fn in body
63+
- (Strict Pure) [let[Strict] x = e in body] ⟶ let[StrictOpt] x = e in body
64+
when no_side_effects(e)
65+
Falling through keeps the original binding. Only the Alias clause changes
66+
evaluation strategy downstream, so we keep its predicate intentionally
67+
syntactic and narrow. *)
68+
let refine_let ~kind param (arg : Lam.t) (l : Lam.t) : Lam.t =
69+
let is_block_constructor = function
70+
| Lam_primitive.Pmakeblock _ -> true
71+
| _ -> false
72+
in
73+
(* SafeAlias is the predicate that justifies the (Alias) rewrite
74+
let[k] x = e in body --> let[Alias] x = e in body
75+
for strict bindings. Turning a binding into [Alias] authorises JS codegen
76+
to inline [e] at every use site or drop `const x = e` entirely, so every
77+
clause below must ensure that duplicate evaluation of [e] is equivalent to
78+
the single eager evaluation promised by [Strict]/[StrictOpt]. *)
79+
let rec is_safe_to_alias (lam : Lam.t) =
80+
match lam with
81+
| Lvar _ | Lconst _ ->
82+
(* var/const --> emitting multiple `const` reads is identical to the
83+
original eager evaluation, so codegen may inline them freely. *)
84+
true
85+
| Lprim { primitive = Pfield (_, Fld_module _); args = [ (Lglobal_module _ | Lvar _) ]; _ } ->
86+
(* field read --> access hits an immutable module block; inlining emits
87+
the same read the eager binding would have performed once. *)
88+
true
89+
| Lprim { primitive = Psome_not_nest; args = [inner]; _ } ->
90+
(* some_not_nest(inner) --> expands to two explicit rewrites:
91+
let[k] x = inner --> let[Alias] x = inner
92+
let[Alias] x = inner --> let[Alias] x = Some(inner)
93+
The recursive call discharges the first arrow; the constructor wrap is
94+
allocation-free in JS, so the second arrow preserves the single eager
95+
evaluation promised by Strict/StrictOpt. *)
96+
is_safe_to_alias inner
97+
| _ -> false
98+
in
99+
match (kind : Lam_compat.let_kind), arg, l with
100+
| _, _, Lvar w when Ident.same w param ->
101+
(* If the body immediately returns the binding (e.g. `{ let x = value; x }`),
102+
we skip creating `x` and keep `value`. There is no `rec`, so `value`
103+
cannot refer back to `x`, and we avoid generating a redundant local. *)
104+
arg
105+
| _, _, Lprim { primitive; args = [ Lvar w ]; loc; _ }
106+
when Ident.same w param && not (is_block_constructor primitive) ->
107+
(* When we immediately feed the binding into a primitive, like
108+
`{ let x = value; Array.length(x) }`, we inline the primitive call
109+
with `value`. This only happens for primitives that are pure and do not
110+
allocate new blocks, so evaluation order and side effects stay the same. *)
111+
Lam.prim ~primitive ~args:[arg] loc
112+
| _, _, Lapply { ap_func = fn; ap_args = [ Lvar w ]; ap_info; ap_transformed_jsx }
113+
when Ident.same w param && not (Lam_hit.hit_variable param fn) ->
114+
(* For a function call such as `{ let x = value; someFn(x) }`, we can
115+
rewrite to `someFn(value)` as long as the callee does not capture `x`.
116+
This removes the temporary binding while preserving the call semantics. *)
117+
Lam.apply fn [arg] ap_info ~ap_transformed_jsx
118+
| (Strict | StrictOpt), arg, _ when is_safe_to_alias arg ->
119+
(* `Strict` and `StrictOpt` bindings both evaluate the RHS immediately
120+
(with `StrictOpt` allowing later elimination if unused). When that RHS
121+
is pure — `{ let x = Some(value); ... }`, `{ let x = 3; ... }`, or a module
122+
field read — we mark it as an alias so downstream passes can inline the
123+
original expression and drop the temporary. *)
124+
Lam.let_ Alias param arg l
125+
| Strict, Lfunction _, _ ->
126+
(* If we eagerly evaluate a function binding such as
127+
`{ let makeGreeting = () => "hi"; ... }`, we end up allocating the
128+
closure immediately. Downgrading `Strict` to `StrictOpt` preserves the
129+
original laziness while still letting later passes inline when safe. *)
130+
Lam.let_ StrictOpt param arg l
131+
| Strict, _, _ when Lam_analysis.no_side_effects arg ->
132+
(* A strict binding whose expression has no side effects — think
133+
`{ let x = computePure(); use(x); }` — can be relaxed to `StrictOpt`.
134+
This keeps the original semantics yet allows downstream passes to skip
135+
evaluating `x` when it turns out to be unused. *)
136+
Lam.let_ StrictOpt param arg l
137+
| kind, _, _ ->
138+
Lam.let_ kind param arg l
121139
122140
let alias_ident_or_global (meta : Lam_stats.t) (k:Ident.t) (v:Ident.t)
123141
(v_kind : Lam_id_kind.t) =
@@ -260,11 +278,3 @@ let is_var (lam : Lam.t) id =
260278
lapply (let a = 3 in let b = 4 in fun x y -> x + y) 2 3
261279
]}
262280
*)
263-
264-
265-
266-
267-
268-
269-
270-

tests/tests/src/option_optimisation.mjs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@
33
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
44

55
function boolean(val1, val2) {
6-
let a = val1;
7-
let b = val2;
8-
if (b || a) {
6+
if (val2 || val1) {
97
return "a";
108
} else {
119
return "b";
@@ -33,8 +31,7 @@ function constant() {
3331
}
3432

3533
function param(opt) {
36-
let x = opt;
37-
console.log(x);
34+
console.log(opt);
3835
}
3936

4037
export {

tests/tests/src/option_wrapping_test.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,6 @@ let x37 = new Intl.DateTimeFormat();
5858

5959
let x38 = new Intl.NumberFormat();
6060

61-
let x39 = true;
62-
6361
let x40 = new Intl.Collator();
6462

6563
let x41 = new Intl.RelativeTimeFormat();
@@ -91,6 +89,8 @@ let x5 = {
9189

9290
let x12 = "test";
9391

92+
let x39 = true;
93+
9494
let x44 = [
9595
1,
9696
2

0 commit comments

Comments
 (0)