@@ -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
122140let 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-
0 commit comments