From eb96936261bf2fa4bd6076e1e5b415ed3d0b8b5c Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Sat, 20 Sep 2025 11:20:28 +0200 Subject: [PATCH 1/7] Option optimization: do not create redundant local vars --- compiler/core/lam_util.ml | 3 ++ .../runtime/lib/es6/Belt_internalAVLset.js | 6 +-- .../runtime/lib/js/Belt_internalAVLset.js | 6 +-- .../src/nested/Types.res.js | 4 +- tests/tests/src/option_optimisation.mjs | 7 +-- tests/tests/src/option_wrapping_test.mjs | 52 +++++++++---------- tests/tests/src/reasonReact.mjs | 23 ++++---- 7 files changed, 48 insertions(+), 53 deletions(-) diff --git a/compiler/core/lam_util.ml b/compiler/core/lam_util.ml index 050ed66815..02f8336e5f 100644 --- a/compiler/core/lam_util.ml +++ b/compiler/core/lam_util.ml @@ -93,6 +93,9 @@ let refine_let since function evaluation is always delayed *) Lam.let_ Alias param arg l + | (Strict | StrictOpt), + ( Lprim {primitive = Psome_not_nest; _} as prim ), _ -> + Lam.let_ Alias param prim l | ( (Strict | StrictOpt ) ), (Lfunction _ ), _ -> (*It can be promoted to [Alias], however, we don't want to do this, since we don't want the diff --git a/packages/@rescript/runtime/lib/es6/Belt_internalAVLset.js b/packages/@rescript/runtime/lib/es6/Belt_internalAVLset.js index a94e510cef..8b1107758d 100644 --- a/packages/@rescript/runtime/lib/es6/Belt_internalAVLset.js +++ b/packages/@rescript/runtime/lib/es6/Belt_internalAVLset.js @@ -741,15 +741,13 @@ function rotateWithRightChild(k1) { function doubleWithLeftChild(k3) { let k3l = k3.l; - let v = rotateWithRightChild(k3l); - k3.l = v; + k3.l = rotateWithRightChild(k3l); return rotateWithLeftChild(k3); } function doubleWithRightChild(k2) { let k2r = k2.r; - let v = rotateWithLeftChild(k2r); - k2.r = v; + k2.r = rotateWithLeftChild(k2r); return rotateWithRightChild(k2); } diff --git a/packages/@rescript/runtime/lib/js/Belt_internalAVLset.js b/packages/@rescript/runtime/lib/js/Belt_internalAVLset.js index 0c97faaf22..523623ce09 100644 --- a/packages/@rescript/runtime/lib/js/Belt_internalAVLset.js +++ b/packages/@rescript/runtime/lib/js/Belt_internalAVLset.js @@ -741,15 +741,13 @@ function rotateWithRightChild(k1) { function doubleWithLeftChild(k3) { let k3l = k3.l; - let v = rotateWithRightChild(k3l); - k3.l = v; + k3.l = rotateWithRightChild(k3l); return rotateWithLeftChild(k3); } function doubleWithRightChild(k2) { let k2r = k2.r; - let v = rotateWithLeftChild(k2r); - k2.r = v; + k2.r = rotateWithLeftChild(k2r); return rotateWithRightChild(k2); } diff --git a/tests/gentype_tests/typescript-react-example/src/nested/Types.res.js b/tests/gentype_tests/typescript-react-example/src/nested/Types.res.js index 82b6292a84..41a5e2c800 100644 --- a/tests/gentype_tests/typescript-react-example/src/nested/Types.res.js +++ b/tests/gentype_tests/typescript-react-example/src/nested/Types.res.js @@ -57,8 +57,6 @@ function testInstantiateTypeParameter(x) { let currentTime = new Date(); -let optFunction = () => 3; - let ObjectId = {}; let someIntList = { @@ -80,6 +78,8 @@ let jsStringT = "a"; let jsString2T = "a"; +let optFunction = () => 3; + export { someIntList, map, diff --git a/tests/tests/src/option_optimisation.mjs b/tests/tests/src/option_optimisation.mjs index 6236bc274c..64d5a7152f 100644 --- a/tests/tests/src/option_optimisation.mjs +++ b/tests/tests/src/option_optimisation.mjs @@ -3,9 +3,7 @@ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js"; function boolean(val1, val2) { - let a = val1; - let b = val2; - if (b || a) { + if (val2 || val1) { return "a"; } else { return "b"; @@ -33,8 +31,7 @@ function constant() { } function param(opt) { - let x = opt; - console.log(x); + console.log(opt); } export { diff --git a/tests/tests/src/option_wrapping_test.mjs b/tests/tests/src/option_wrapping_test.mjs index af052d9536..3f24543fcb 100644 --- a/tests/tests/src/option_wrapping_test.mjs +++ b/tests/tests/src/option_wrapping_test.mjs @@ -8,6 +8,25 @@ let x6 = { x: 42 }; +let x10 = null; + +let x11 = Primitive_option.some(undefined); + +let x1 = "hello"; + +let x2 = 1; + +let x3 = { + TAG: "Ok", + _0: "hi" +}; + +let x4 = "polyvar"; + +let x5 = { + x: 42 +}; + let x7 = [ 1, 2, @@ -16,9 +35,7 @@ let x7 = [ let x8 = () => {}; -let x10 = null; - -let x11 = Primitive_option.some(undefined); +let x12 = "test"; let x20 = null; @@ -68,36 +85,19 @@ let x42 = new Intl.PluralRules(); let x43 = new Intl.Locale("en"); -let x45 = Promise.resolve(true); - -let x47 = {}; - -let x48 = Stdlib_Lazy.make(() => true); - -let x1 = "hello"; - -let x2 = 1; - -let x3 = { - TAG: "Ok", - _0: "hi" -}; - -let x4 = "polyvar"; - -let x5 = { - x: 42 -}; - -let x12 = "test"; - let x44 = [ 1, 2 ]; +let x45 = Promise.resolve(true); + let x46 = /* [] */0; +let x47 = {}; + +let x48 = Stdlib_Lazy.make(() => true); + export { x1, x2, diff --git a/tests/tests/src/reasonReact.mjs b/tests/tests/src/reasonReact.mjs index 4b67328758..9d52b80f43 100644 --- a/tests/tests/src/reasonReact.mjs +++ b/tests/tests/src/reasonReact.mjs @@ -92,17 +92,6 @@ function wrapReasonForJs(component, jsPropsToReason) { let dummyInteropComponent = basicComponent("interop"); function wrapJsForReason(reactClass, props, children) { - let jsElementWrapped = (extra, extra$1) => { - let props$1 = Object.assign(Object.assign({}, props), { - ref: extra$1, - key: extra - }); - let varargs = Js_array.concat(children, [ - reactClass, - props$1 - ]); - return React.createElement.apply(null, varargs); - }; return { debugName: dummyInteropComponent.debugName, reactClassInternal: dummyInteropComponent.reactClassInternal, @@ -117,7 +106,17 @@ function wrapJsForReason(reactClass, props, children) { initialState: dummyInteropComponent.initialState, retainedProps: dummyInteropComponent.retainedProps, reducer: dummyInteropComponent.reducer, - jsElementWrapped: jsElementWrapped + jsElementWrapped: (extra, extra$1) => { + let props$1 = Object.assign(Object.assign({}, props), { + ref: extra$1, + key: extra + }); + let varargs = Js_array.concat(children, [ + reactClass, + props$1 + ]); + return React.createElement.apply(null, varargs); + } }; } From a564d263675b54dcabec62fc4a04ed00b3ae970e Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Mon, 22 Sep 2025 09:44:39 +0200 Subject: [PATCH 2/7] Cleanup --- compiler/core/lam_util.ml | 145 ++++++++++++++------------------------ 1 file changed, 54 insertions(+), 91 deletions(-) diff --git a/compiler/core/lam_util.ml b/compiler/core/lam_util.ml index 02f8336e5f..372b55d587 100644 --- a/compiler/core/lam_util.ml +++ b/compiler/core/lam_util.ml @@ -38,89 +38,60 @@ let add_required_modules ( x : Ident.t list) (meta : Lam_stats.t) = *) -(* - It's impossible to have a case like below: - {[ - (let export_f = ... in export_f) - ]} - Even so, it's still correct -*) -let refine_let - ~kind param - (arg : Lam.t) (l : Lam.t) : Lam.t = - - match (kind : Lam_compat.let_kind ), arg, l with - | _, _, Lvar w when Ident.same w param - (* let k = xx in k - there is no [rec] so [k] would not appear in [xx] - *) - -> arg (* TODO: optimize here -- it's safe to do substitution here *) - | _, _, Lprim {primitive ; args = [Lvar w]; loc ; _} when Ident.same w param - && (function | Lam_primitive.Pmakeblock _ -> false | _ -> true) primitive - (* don't inline inside a block *) - -> Lam.prim ~primitive ~args:[arg] loc - (* we can not do this substitution when capttured *) - (* | _, Lvar _, _ -> (\** let u = h in xxx*\) *) - (* (\* assert false *\) *) - (* Ext_log.err "@[substitution >> @]@."; *) - (* let v= subst_lambda (Map_ident.singleton param arg ) l in *) - (* Ext_log.err "@[substitution << @]@."; *) - (* v *) - | _, _, Lapply {ap_func=fn; ap_args = [Lvar w]; ap_info; ap_transformed_jsx} when - Ident.same w param && - (not (Lam_hit.hit_variable param fn )) - -> - (* does not work for multiple args since - evaluation order unspecified, does not apply - for [js] in general, since the scope of js ir is loosen - - here we remove the definition of [param] - {[ let k = v in (body) k - ]} - #1667 make sure body does not hit k - *) - Lam.apply fn [arg] ap_info ~ap_transformed_jsx - | (Strict | StrictOpt ), - ( Lvar _ | Lconst _ | - Lprim {primitive = Pfield (_ , Fld_module _) ; - args = [ Lglobal_module _ | Lvar _ ]; _}) , _ -> - (* (match arg with *) - (* | Lconst _ -> *) - (* Ext_log.err "@[%a %s@]@." *) - (* Ident.print param (string_of_lambda arg) *) - (* | _ -> ()); *) - (* No side effect and does not depend on store, - since function evaluation is always delayed - *) - Lam.let_ Alias param arg l - | (Strict | StrictOpt), - ( Lprim {primitive = Psome_not_nest; _} as prim ), _ -> - Lam.let_ Alias param prim l - | ( (Strict | StrictOpt ) ), (Lfunction _ ), _ -> - (*It can be promoted to [Alias], however, - we don't want to do this, since we don't want the - function to be inlined to a block, for example - {[ - let f = fun _ -> 1 in - [0, f] - ]} - TODO: punish inliner to inline functions - into a block - *) - Lam.let_ StrictOpt param arg l - (* Not the case, the block itself can have side effects - we can apply [no_side_effects] pass - | Some Strict, Lprim(Pmakeblock (_,_,Immutable),_) -> - Llet(StrictOpt, param, arg, l) - *) - | Strict, _ ,_ when Lam_analysis.no_side_effects arg -> - Lam.let_ StrictOpt param arg l - | Variable, _, _ -> - Lam.let_ Variable param arg l - | kind, _, _ -> - Lam.let_ kind param arg l -(* | None , _, _ -> - Lam.let_ Strict param arg l *) +(* refine_let normalises let-bindings so we avoid redundant locals while + preserving evaluation semantics. *) +let refine_let ~kind param (arg : Lam.t) (l : Lam.t) : Lam.t = + let is_block_constructor = function + | Lam_primitive.Pmakeblock _ -> true + | _ -> false + in + let is_alias_candidate (lam : Lam.t) = + match lam with + | Lvar _ | Lconst _ -> true + | Lprim { primitive = Psome_not_nest; _ } -> true + | Lprim { primitive = Pfield (_, Fld_module _); args = [ (Lglobal_module _ | Lvar _) ]; _ } -> true + | _ -> false + in + match (kind : Lam_compat.let_kind), arg, l with + | _, _, Lvar w when Ident.same w param -> + (* If the body immediately returns the binding (e.g. `{ let x = value; x }`), + we skip creating `x` and keep `value`. There is no `rec`, so `value` + cannot refer back to `x`, and we avoid generating a redundant local. *) + arg + | _, _, Lprim { primitive; args = [ Lvar w ]; loc; _ } + when Ident.same w param && not (is_block_constructor primitive) -> + (* When we immediately feed the binding into a primitive, like + `{ let x = value; Array.length(x) }`, we inline the primitive call + with `value`. This only happens for primitives that are pure and do not + allocate new blocks, so evaluation order and side effects stay the same. *) + Lam.prim ~primitive ~args:[arg] loc + | _, _, Lapply { ap_func = fn; ap_args = [ Lvar w ]; ap_info; ap_transformed_jsx } + when Ident.same w param && not (Lam_hit.hit_variable param fn) -> + (* For a function call such as `{ let x = value; someFn(x) }`, we can + rewrite to `someFn(value)` as long as the callee does not capture `x`. + This removes the temporary binding while preserving the call semantics. *) + Lam.apply fn [arg] ap_info ~ap_transformed_jsx + | (Strict | StrictOpt), arg, _ when is_alias_candidate arg -> + (* `Strict` and `StrictOpt` bindings both evaluate the RHS immediately + (with `StrictOpt` allowing later elimination if unused). When that RHS + is pure — `{ let x = Some(value); ... }`, `{ let x = 3; ... }`, or a module + field read — we mark it as an alias so downstream passes can inline the + original expression and drop the temporary. *) + Lam.let_ Alias param arg l + | Strict, Lfunction _, _ -> + (* If we eagerly evaluate a function binding such as + `{ let makeGreeting = () => "hi"; ... }`, we end up allocating the + closure immediately. Downgrading `Strict` to `StrictOpt` preserves the + original laziness while still letting later passes inline when safe. *) + Lam.let_ StrictOpt param arg l + | Strict, _, _ when Lam_analysis.no_side_effects arg -> + (* A strict binding whose expression has no side effects — think + `{ let x = computePure(); use(x); }` — can be relaxed to `StrictOpt`. + This keeps the original semantics yet allows downstream passes to skip + evaluating `x` when it turns out to be unused. *) + Lam.let_ StrictOpt param arg l + | kind, _, _ -> + Lam.let_ kind param arg l let alias_ident_or_global (meta : Lam_stats.t) (k:Ident.t) (v:Ident.t) (v_kind : Lam_id_kind.t) = @@ -263,11 +234,3 @@ let is_var (lam : Lam.t) id = lapply (let a = 3 in let b = 4 in fun x y -> x + y) 2 3 ]} *) - - - - - - - - From f336bdae2f1d4cb9ef4c381d7d2b57944c7d9a2e Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Mon, 22 Sep 2025 09:54:12 +0200 Subject: [PATCH 3/7] Comment --- compiler/core/lam_util.ml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/compiler/core/lam_util.ml b/compiler/core/lam_util.ml index 372b55d587..a690db5ddc 100644 --- a/compiler/core/lam_util.ml +++ b/compiler/core/lam_util.ml @@ -45,6 +45,9 @@ let refine_let ~kind param (arg : Lam.t) (l : Lam.t) : Lam.t = | Lam_primitive.Pmakeblock _ -> true | _ -> false in + (* Helper for spotting RHS expressions that are safe to inline everywhere. + These are the obviously pure shapes where substituting the original value + cannot introduce extra work or side effects. *) let is_alias_candidate (lam : Lam.t) = match lam with | Lvar _ | Lconst _ -> true From 66d99109f86d8ad99d0cd83a87540a2e892b8f50 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Mon, 22 Sep 2025 10:18:46 +0200 Subject: [PATCH 4/7] Address review comments --- compiler/core/lam_util.ml | 10 ++- .../runtime/lib/es6/Belt_internalAVLset.js | 6 +- .../runtime/lib/js/Belt_internalAVLset.js | 6 +- tests/tests/src/option_wrapping_test.mjs | 66 +++++++++---------- 4 files changed, 45 insertions(+), 43 deletions(-) diff --git a/compiler/core/lam_util.ml b/compiler/core/lam_util.ml index a690db5ddc..103887cb2d 100644 --- a/compiler/core/lam_util.ml +++ b/compiler/core/lam_util.ml @@ -45,13 +45,11 @@ let refine_let ~kind param (arg : Lam.t) (l : Lam.t) : Lam.t = | Lam_primitive.Pmakeblock _ -> true | _ -> false in - (* Helper for spotting RHS expressions that are safe to inline everywhere. - These are the obviously pure shapes where substituting the original value - cannot introduce extra work or side effects. *) - let is_alias_candidate (lam : Lam.t) = + let is_safe_to_inline (lam : Lam.t) = match lam with | Lvar _ | Lconst _ -> true - | Lprim { primitive = Psome_not_nest; _ } -> true + | Lprim { primitive = Psome_not_nest; args = [inner]; _ } -> + Lam_analysis.no_side_effects inner | Lprim { primitive = Pfield (_, Fld_module _); args = [ (Lglobal_module _ | Lvar _) ]; _ } -> true | _ -> false in @@ -74,7 +72,7 @@ let refine_let ~kind param (arg : Lam.t) (l : Lam.t) : Lam.t = rewrite to `someFn(value)` as long as the callee does not capture `x`. This removes the temporary binding while preserving the call semantics. *) Lam.apply fn [arg] ap_info ~ap_transformed_jsx - | (Strict | StrictOpt), arg, _ when is_alias_candidate arg -> + | (Strict | StrictOpt), arg, _ when is_safe_to_inline arg -> (* `Strict` and `StrictOpt` bindings both evaluate the RHS immediately (with `StrictOpt` allowing later elimination if unused). When that RHS is pure — `{ let x = Some(value); ... }`, `{ let x = 3; ... }`, or a module diff --git a/packages/@rescript/runtime/lib/es6/Belt_internalAVLset.js b/packages/@rescript/runtime/lib/es6/Belt_internalAVLset.js index 8b1107758d..a94e510cef 100644 --- a/packages/@rescript/runtime/lib/es6/Belt_internalAVLset.js +++ b/packages/@rescript/runtime/lib/es6/Belt_internalAVLset.js @@ -741,13 +741,15 @@ function rotateWithRightChild(k1) { function doubleWithLeftChild(k3) { let k3l = k3.l; - k3.l = rotateWithRightChild(k3l); + let v = rotateWithRightChild(k3l); + k3.l = v; return rotateWithLeftChild(k3); } function doubleWithRightChild(k2) { let k2r = k2.r; - k2.r = rotateWithLeftChild(k2r); + let v = rotateWithLeftChild(k2r); + k2.r = v; return rotateWithRightChild(k2); } diff --git a/packages/@rescript/runtime/lib/js/Belt_internalAVLset.js b/packages/@rescript/runtime/lib/js/Belt_internalAVLset.js index 523623ce09..0c97faaf22 100644 --- a/packages/@rescript/runtime/lib/js/Belt_internalAVLset.js +++ b/packages/@rescript/runtime/lib/js/Belt_internalAVLset.js @@ -741,13 +741,15 @@ function rotateWithRightChild(k1) { function doubleWithLeftChild(k3) { let k3l = k3.l; - k3.l = rotateWithRightChild(k3l); + let v = rotateWithRightChild(k3l); + k3.l = v; return rotateWithLeftChild(k3); } function doubleWithRightChild(k2) { let k2r = k2.r; - k2.r = rotateWithLeftChild(k2r); + let v = rotateWithLeftChild(k2r); + k2.r = v; return rotateWithRightChild(k2); } diff --git a/tests/tests/src/option_wrapping_test.mjs b/tests/tests/src/option_wrapping_test.mjs index 3f24543fcb..4a67c6c13f 100644 --- a/tests/tests/src/option_wrapping_test.mjs +++ b/tests/tests/src/option_wrapping_test.mjs @@ -12,37 +12,10 @@ let x10 = null; let x11 = Primitive_option.some(undefined); -let x1 = "hello"; - -let x2 = 1; - -let x3 = { - TAG: "Ok", - _0: "hi" -}; - -let x4 = "polyvar"; - -let x5 = { - x: 42 -}; - -let x7 = [ - 1, - 2, - 3 -]; - -let x8 = () => {}; - -let x12 = "test"; - let x20 = null; let x21 = new Date(); -let x22 = /test/; - let x23 = new Map(); let x24 = new Set(); @@ -75,8 +48,6 @@ let x37 = new Intl.DateTimeFormat(); let x38 = new Intl.NumberFormat(); -let x39 = true; - let x40 = new Intl.Collator(); let x41 = new Intl.RelativeTimeFormat(); @@ -85,19 +56,48 @@ let x42 = new Intl.PluralRules(); let x43 = new Intl.Locale("en"); +let x45 = Promise.resolve(true); + +let x48 = Stdlib_Lazy.make(() => true); + +let x1 = "hello"; + +let x2 = 1; + +let x3 = { + TAG: "Ok", + _0: "hi" +}; + +let x4 = "polyvar"; + +let x5 = { + x: 42 +}; + +let x7 = [ + 1, + 2, + 3 +]; + +let x8 = () => {}; + +let x12 = "test"; + +let x22 = /test/; + +let x39 = true; + let x44 = [ 1, 2 ]; -let x45 = Promise.resolve(true); - let x46 = /* [] */0; let x47 = {}; -let x48 = Stdlib_Lazy.make(() => true); - export { x1, x2, From 61f75df014b61442b43ad71190d39f64fe7f3ecb Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Mon, 22 Sep 2025 15:42:00 +0200 Subject: [PATCH 5/7] Don't use Lam_analysis.no_side_effects here --- compiler/core/lam_util.ml | 8 +++---- .../src/nested/Types.res.js | 4 ++-- tests/tests/src/option_wrapping_test.mjs | 24 +++++++++---------- tests/tests/src/reasonReact.mjs | 23 +++++++++--------- 4 files changed, 30 insertions(+), 29 deletions(-) diff --git a/compiler/core/lam_util.ml b/compiler/core/lam_util.ml index 103887cb2d..37dba9c647 100644 --- a/compiler/core/lam_util.ml +++ b/compiler/core/lam_util.ml @@ -45,12 +45,12 @@ let refine_let ~kind param (arg : Lam.t) (l : Lam.t) : Lam.t = | Lam_primitive.Pmakeblock _ -> true | _ -> false in - let is_safe_to_inline (lam : Lam.t) = + let rec is_safe_to_alias (lam : Lam.t) = match lam with | Lvar _ | Lconst _ -> true - | Lprim { primitive = Psome_not_nest; args = [inner]; _ } -> - Lam_analysis.no_side_effects inner | Lprim { primitive = Pfield (_, Fld_module _); args = [ (Lglobal_module _ | Lvar _) ]; _ } -> true + | Lprim { primitive = Psome_not_nest; args = [inner]; _ } -> + is_safe_to_alias inner | _ -> false in match (kind : Lam_compat.let_kind), arg, l with @@ -72,7 +72,7 @@ let refine_let ~kind param (arg : Lam.t) (l : Lam.t) : Lam.t = rewrite to `someFn(value)` as long as the callee does not capture `x`. This removes the temporary binding while preserving the call semantics. *) Lam.apply fn [arg] ap_info ~ap_transformed_jsx - | (Strict | StrictOpt), arg, _ when is_safe_to_inline arg -> + | (Strict | StrictOpt), arg, _ when is_safe_to_alias arg -> (* `Strict` and `StrictOpt` bindings both evaluate the RHS immediately (with `StrictOpt` allowing later elimination if unused). When that RHS is pure — `{ let x = Some(value); ... }`, `{ let x = 3; ... }`, or a module diff --git a/tests/gentype_tests/typescript-react-example/src/nested/Types.res.js b/tests/gentype_tests/typescript-react-example/src/nested/Types.res.js index 41a5e2c800..82b6292a84 100644 --- a/tests/gentype_tests/typescript-react-example/src/nested/Types.res.js +++ b/tests/gentype_tests/typescript-react-example/src/nested/Types.res.js @@ -57,6 +57,8 @@ function testInstantiateTypeParameter(x) { let currentTime = new Date(); +let optFunction = () => 3; + let ObjectId = {}; let someIntList = { @@ -78,8 +80,6 @@ let jsStringT = "a"; let jsString2T = "a"; -let optFunction = () => 3; - export { someIntList, map, diff --git a/tests/tests/src/option_wrapping_test.mjs b/tests/tests/src/option_wrapping_test.mjs index 4a67c6c13f..1acba96df6 100644 --- a/tests/tests/src/option_wrapping_test.mjs +++ b/tests/tests/src/option_wrapping_test.mjs @@ -8,6 +8,14 @@ let x6 = { x: 42 }; +let x7 = [ + 1, + 2, + 3 +]; + +let x8 = () => {}; + let x10 = null; let x11 = Primitive_option.some(undefined); @@ -16,6 +24,8 @@ let x20 = null; let x21 = new Date(); +let x22 = /test/; + let x23 = new Map(); let x24 = new Set(); @@ -58,6 +68,8 @@ let x43 = new Intl.Locale("en"); let x45 = Promise.resolve(true); +let x47 = {}; + let x48 = Stdlib_Lazy.make(() => true); let x1 = "hello"; @@ -75,18 +87,8 @@ let x5 = { x: 42 }; -let x7 = [ - 1, - 2, - 3 -]; - -let x8 = () => {}; - let x12 = "test"; -let x22 = /test/; - let x39 = true; let x44 = [ @@ -96,8 +98,6 @@ let x44 = [ let x46 = /* [] */0; -let x47 = {}; - export { x1, x2, diff --git a/tests/tests/src/reasonReact.mjs b/tests/tests/src/reasonReact.mjs index 9d52b80f43..4b67328758 100644 --- a/tests/tests/src/reasonReact.mjs +++ b/tests/tests/src/reasonReact.mjs @@ -92,6 +92,17 @@ function wrapReasonForJs(component, jsPropsToReason) { let dummyInteropComponent = basicComponent("interop"); function wrapJsForReason(reactClass, props, children) { + let jsElementWrapped = (extra, extra$1) => { + let props$1 = Object.assign(Object.assign({}, props), { + ref: extra$1, + key: extra + }); + let varargs = Js_array.concat(children, [ + reactClass, + props$1 + ]); + return React.createElement.apply(null, varargs); + }; return { debugName: dummyInteropComponent.debugName, reactClassInternal: dummyInteropComponent.reactClassInternal, @@ -106,17 +117,7 @@ function wrapJsForReason(reactClass, props, children) { initialState: dummyInteropComponent.initialState, retainedProps: dummyInteropComponent.retainedProps, reducer: dummyInteropComponent.reducer, - jsElementWrapped: (extra, extra$1) => { - let props$1 = Object.assign(Object.assign({}, props), { - ref: extra$1, - key: extra - }); - let varargs = Js_array.concat(children, [ - reactClass, - props$1 - ]); - return React.createElement.apply(null, varargs); - } + jsElementWrapped: jsElementWrapped }; } From cb98fbea79b0d8f9fbde4556645fca7fd0142cff Mon Sep 17 00:00:00 2001 From: Cristiano Calcagno Date: Mon, 22 Sep 2025 16:44:45 +0200 Subject: [PATCH 6/7] Document refine_let rewrite semantics Signed-off-by: Cristiano Calcagno --- compiler/core/lam_util.ml | 51 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/compiler/core/lam_util.ml b/compiler/core/lam_util.ml index 37dba9c647..9d7334930c 100644 --- a/compiler/core/lam_util.ml +++ b/compiler/core/lam_util.ml @@ -39,17 +39,60 @@ let add_required_modules ( x : Ident.t list) (meta : Lam_stats.t) = (* refine_let normalises let-bindings so we avoid redundant locals while - preserving evaluation semantics. *) -let refine_let ~kind param (arg : Lam.t) (l : Lam.t) : Lam.t = + preserving the semantics encoded by Lambda's let_kind. Downstream passes at + the JS backend interpret the k-tag as the shape of code they are allowed to + emit: + Strict --> emit `const x = e; body`, with `e` evaluated exactly once. + Reordering `e` or duplicating it would be incorrect. + StrictOpt --> emit either `const x = e; body` (when `x` is used) or drop + the declaration entirely (when DCE prunes `x`). Duplicating + `e` remains forbidden. + Alias --> emit `const x = e; body` or substitute `e` directly at each + use site, removing the binding if convenient. + Variable --> emit a thunked shape like `function() { return e; }` or keep + the original `let` without forcing; evaluation must stay + deferred. + + The function implements this contract through ordered rewrite clauses: + - (Return) [let[k] x = e in x] ⟶ e + - (Prim) [let[k] x = e in prim p x] ⟶ prim p e (p ≠ makeblock) + - (Call) [let[k] x = e in f x] ⟶ f e (x not captured in f) + - (Alias) [let[k] x = e in body] ⟶ let[Alias] x = e in body + when k ∈ {Strict, StrictOpt} and SafeAlias(e) + - (Strict λ) [let[Strict] x = fn in body] ⟶ let[StrictOpt] x = fn in body + - (Strict Pure) [let[Strict] x = e in body] ⟶ let[StrictOpt] x = e in body + when no_side_effects(e) + Falling through keeps the original binding. Only the Alias clause changes + evaluation strategy downstream, so we keep its predicate intentionally + syntactic and narrow. *) + let refine_let ~kind param (arg : Lam.t) (l : Lam.t) : Lam.t = let is_block_constructor = function | Lam_primitive.Pmakeblock _ -> true | _ -> false in + (* SafeAlias is the predicate that justifies the (Alias) rewrite + let[k] x = e in body --> let[Alias] x = e in body + for strict bindings. Turning a binding into [Alias] authorises JS codegen + to inline [e] at every use site or drop `const x = e` entirely, so every + clause below must ensure that duplicate evaluation of [e] is equivalent to + the single eager evaluation promised by [Strict]/[StrictOpt]. *) let rec is_safe_to_alias (lam : Lam.t) = match lam with - | Lvar _ | Lconst _ -> true - | Lprim { primitive = Pfield (_, Fld_module _); args = [ (Lglobal_module _ | Lvar _) ]; _ } -> true + | Lvar _ | Lconst _ -> + (* var/const --> emitting multiple `const` reads is identical to the + original eager evaluation, so codegen may inline them freely. *) + true + | Lprim { primitive = Pfield (_, Fld_module _); args = [ (Lglobal_module _ | Lvar _) ]; _ } -> + (* field read --> access hits an immutable module block; inlining emits + the same read the eager binding would have performed once. *) + true | Lprim { primitive = Psome_not_nest; args = [inner]; _ } -> + (* some_not_nest(inner) --> expands to two explicit rewrites: + let[k] x = inner --> let[Alias] x = inner + let[Alias] x = inner --> let[Alias] x = Some(inner) + The recursive call discharges the first arrow; the constructor wrap is + allocation-free in JS, so the second arrow preserves the single eager + evaluation promised by Strict/StrictOpt. *) is_safe_to_alias inner | _ -> false in From 418e733bb03c41f27232652a85dff11b86958387 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Mon, 22 Sep 2025 17:01:28 +0200 Subject: [PATCH 7/7] CHANGELOG --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a328bc56dc..037c6725b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,8 @@ #### :nail_care: Polish - Keep track of compiler info during build. https://github.com/rescript-lang/rescript/pull/7889 +- Improve option optimization for constants. https://github.com/rescript-lang/rescript/pull/7913 +- Option optimization: do not create redundant local vars. https://github.com/rescript-lang/rescript/pull/7915 #### :house: Internal @@ -48,7 +50,6 @@ - Add (dev-)dependencies to build schema. https://github.com/rescript-lang/rescript/pull/7892 - Dedicated error for dict literal spreads. https://github.com/rescript-lang/rescript/pull/7901 - Dedicated error message for when mixing up `:` and `=` in various positions. https://github.com/rescript-lang/rescript/pull/7900 -- Improve option optimization for constants. https://github.com/rescript-lang/rescript/pull/7913 # 12.0.0-beta.11