Skip to content

Evaluate a let before the callee of the call it feeds - #9296

Open
mununki wants to merge 2 commits into
masterfrom
evaluate-let-before-callee
Open

mununki wants to merge 2 commits into
masterfrom
evaluate-let-before-callee

Conversation

@mununki

@mununki mununki commented Nov 13, 2026

Copy link
Copy Markdown
Member

Fixes #9228.

let x = e; f(x) compiled to f(e) whenever f did not mention x. JavaScript evaluates the callee of a call before the arguments, so e ran after f:

type counter = {mutable n: int}
let c = {n: 0}
let next = () => {
  c.n = c.n + 1
  (x: int) => x
}
let readBeforeCall = () => {
  let before = c.n
  next()(before)
}
// before
function readBeforeCall() {
  return next()(c.n);
}

// after
function readBeforeCall() {
  let before = c.n;
  return next()(before);
}

The same rewrite, Lam_util.refine_let, is applied to the binding the inliner makes for an argument and to the binding Lam_pass_sroa makes for a field of a local record, so those are fixed too, as is a let with an effect before an awaited callee or before a callee read from a field that the let assigns.

The change

  • The (Call) clause folds let x = e; f(x) only when e and f commute: one of them has no side effects and reads nothing that may change, or neither has side effects. let x = swap(); cb(x) with a parameter cb is still cb(swap()), and let x = c.n; h.call(x) is still h.call(c.n).
  • The (Prim) clause did the same to the call of an @val or @new external, which JavaScript reads from its global or module before the argument. It now folds such a call only when e has no side effects.
  • refine_let takes the assigned variables, which its callers compute, and motion and conflict move from Lam_beta_reduce_util to Lam_analysis so that both use them.

Output

One checked-in output changes: in Stdlib_PromiseTest, let s = switch e {...}; resolve(s) keeps s instead of a tmp.

A call through a record field keeps the binding when the value has side effects, as in let x = f(); r.call(x), even when the field is immutable. #9223 proposed to treat immutable fields as fixed, but a record with a mutable field can be coerced to a type where the field is immutable, so the field can change.

Not changed

An inlined function whose body is the call of an external, as in let callHook = x => hook(x) and callHook(install()), still compiles to hook(install()), which reads hook before install() runs. That is the fast path of beta reduction, not refine_let, and binding there affects every inlined wrapper around an external, so it is left for a separate change.

Tests

let_before_callee_test has nine tests. Compiled with master, eight fail. inline_arg_order_cross_module_test gains one for a function of another module inlined under -bs-cross-module-opt, which fails with master too.

Why:
    `Lam_util.refine_let` rewrote `let x = e; f(x)` to `f(e)` whenever `f`
    did not mention `x`. JavaScript evaluates the callee of a call before
    the arguments, so `e` then ran after `f`: a `let` that read state saw
    what the callee had changed, and an effect of the `let` came after the
    effects of the callee, or after its `await`. The same happened to the
    binding `Lam_beta_reduce` makes for an argument of an inlined function,
    and to the binding `Lam_pass_sroa` makes for a field of a local record.
    The rewrite now applies only when the two commute: one of them has no
    side effects and reads nothing that may change, or neither has side
    effects. The rewrite of `let x = e; p(x)` for a primitive `p` did the
    same to the call of an external function or class, which JavaScript
    reads from its global or module before the argument; it now applies to
    such a call only when `e` has no side effects. `refine_let` takes the
    assigned variables for this, and `motion` and `conflict` move from
    `Lam_beta_reduce_util` to `Lam_analysis`.

JS modified (let read before a callee that changes it):
Example:
    type counter = {mutable n: int}
    let c = {n: 0}
    let next = () => {
      c.n = c.n + 1
      (x: int) => x
    }
    let readBeforeCall = () => {
      let before = c.n
      next()(before)
    }
Before (js):
    function readBeforeCall() {
      return next()(c.n);
    }
After (js):
    function readBeforeCall() {
      let before = c.n;
      return next()(before);
    }

JS modified (let with an effect before an awaited callee):
Example:
    let effectBeforeAwait = async () => {
      let x = log("argument")
      (await getHandler())(x)
    }
Before (js):
    async function effectBeforeAwait() {
      return (await getHandler())(log("argument"));
    }
After (js):
    async function effectBeforeAwait() {
      let x = log("argument");
      return (await getHandler())(x);
    }

JS modified (let that replaces the field the callee is read from):
Example:
    type handler = {mutable call: int => int}
    let h = {call: x => x + 1}
    let swap = () => {
      h.call = x => x + 100
      1
    }
    let effectBeforeFieldRead = () => {
      let x = swap()
      h.call(x)
    }
Before (js):
    function effectBeforeFieldRead() {
      return h.call(swap());
    }
After (js):
    function effectBeforeFieldRead() {
      let x = swap();
      return h.call(x);
    }

JS modified (field of a local record):
Example:
    type snapshot = {mutable unused: int, saved: int}
    let recordField = () => {
      let s = {unused: 0, saved: c.n}
      next()(s.saved)
    }
Before (js):
    function recordField() {
      return next()(c.n);
    }
After (js):
    function recordField() {
      let s_saved = c.n;
      return next()(s_saved);
    }

JS modified (let that replaces an external function):
Example:
    @scope("globalThis") @Val external hook: int => int = "letBeforeCalleeHook"
    let effectBeforeExternal = () => {
      let x = install()
      hook(x)
    }
Before (js):
    function effectBeforeExternal() {
      return globalThis.letBeforeCalleeHook(install());
    }
After (js):
    function effectBeforeExternal() {
      let x = install();
      return globalThis.letBeforeCalleeHook(x);
    }

Review: How were the changes tested? `let_before_callee_test` has nine
    tests. Compiled with master, eight fail. One of them is for an `@new`
    external. `inline_arg_order_cross_module_test` gains a test that fails
    with master as well.
Review: Which calls still take the value in place? Those where the order
    cannot matter: `let x = swap(); cb(x)` with a parameter `cb` compiles to
    `cb(swap())`, and `let x = c.n; h.call(x)` to `h.call(c.n)`, since
    neither the field read nor `c.n` has side effects.
Review: Which checked-in output changes? One, in `Stdlib_PromiseTest`:
    `let s = switch e {...}; resolve(s)` keeps `s` instead of assigning the
    result of the `switch` to a `tmp`, because a `switch` counts as having
    side effects and `resolve` is the external `Promise.resolve`.
Review: Why does a call through a field of an immutable record keep the
    binding, as in `let x = f(); r.call(x)`? The field may still change: a
    record with a mutable field can be coerced to a type where the field
    is immutable, so `f()` may assign it through the other type.
Review: Where do the assigned variables matter? `Lam_pass_sroa` makes the
    `Lassign`s and is the last Lambda pass, so the set is empty in
    `Lam_pass_deep_flatten`, `Lam_pass_lets_dce` and the inliner today;
    they compute it anyway, as `Lam_pass_remove_alias` does. It is not
    empty where `Lam_compile` inlines a function of another module under
    `-bs-cross-module-opt`, which happens after `Lam_pass_sroa`. There an
    argument that reads a local ref, by then an assigned variable, stays
    bound before a callee that assigns it; the new test in
    `inline_arg_order_cross_module_test` covers that.

Signed-off-by: mununki <[email protected]>
@github-actions

github-actions Bot commented Nov 13, 2026 •

Copy link
Copy Markdown

Changelog

🐛 Bug fix

  • Fix let x = e; f(x) running e after it evaluates the callee f, as in let x = c.n; next()(x).

Semantic Diff

2 commits · Full page

Evaluate a let before the callee of the call it feeds

195aca5 · mununki · Nov 13, 2026, 13:35 UTC

Why

Lam_util.refine_let rewrote let x = e; f(x) to f(e) whenever f did not mention x. JavaScript evaluates the callee of a call before the arguments, so e then ran after f: a let that read state saw what the callee had changed, and an effect of the let came after the effects of the callee, or after its await. The same happened to the binding Lam_beta_reduce makes for an argument of an inlined function, and to the binding Lam_pass_sroa makes for a field of a local record. The rewrite now applies only when the two commute: one of them has no side effects and reads nothing that may change, or neither has side effects. The rewrite of let x = e; p(x) for a primitive p did the same to the call of an external function or class, which JavaScript reads from its global or module before the argument; it now applies to such a call only when e has no side effects. refine_let takes the assigned variables for this, and motion and conflict move from Lam_beta_reduce_util to Lam_analysis.

JS modified (let read before a callee that changes it)

type counter = {mutable n: int}
let c = {n: 0}
let next = () => {
  c.n = c.n + 1
  (x: int) => x
}
let readBeforeCall = () => {
  let before = c.n
  next()(before)
}

js

 function readBeforeCall() {
-  return next()(c.n);
+  let before = c.n;
+  return next()(before);
 }

JS modified (let with an effect before an awaited callee)

let effectBeforeAwait = async () => {
  let x = log("argument")
  (await getHandler())(x)
}

js

 async function effectBeforeAwait() {
-  return (await getHandler())(log("argument"));
+  let x = log("argument");
+  return (await getHandler())(x);
 }

JS modified (let that replaces the field the callee is read from)

type handler = {mutable call: int => int}
let h = {call: x => x + 1}
let swap = () => {
  h.call = x => x + 100
  1
}
let effectBeforeFieldRead = () => {
  let x = swap()
  h.call(x)
}

js

 function effectBeforeFieldRead() {
-  return h.call(swap());
+  let x = swap();
+  return h.call(x);
 }

JS modified (field of a local record)

type snapshot = {mutable unused: int, saved: int}
let recordField = () => {
  let s = {unused: 0, saved: c.n}
  next()(s.saved)
}

js

 function recordField() {
-  return next()(c.n);
+  let s_saved = c.n;
+  return next()(s_saved);
 }

JS modified (let that replaces an external function)

@scope("globalThis") @val external hook: int => int = "letBeforeCalleeHook"
let effectBeforeExternal = () => {
  let x = install()
  hook(x)
}

js

 function effectBeforeExternal() {
-  return globalThis.letBeforeCalleeHook(install());
+  let x = install();
+  return globalThis.letBeforeCalleeHook(x);
 }

Review

  • How were the changes tested? let_before_callee_test has nine tests. Compiled with master, eight fail. One of them is for an @new external. inline_arg_order_cross_module_test gains a test that fails with master as well.
  • Which calls still take the value in place? Those where the order cannot matter: let x = swap(); cb(x) with a parameter cb compiles to cb(swap()), and let x = c.n; h.call(x) to h.call(c.n), since neither the field read nor c.n has side effects.
  • Which checked-in output changes? One, in Stdlib_PromiseTest: let s = switch e {...}; resolve(s) keeps s instead of assigning the result of the switch to a tmp, because a switch counts as having side effects and resolve is the external Promise.resolve.
  • Why does a call through a field of an immutable record keep the binding, as in let x = f(); r.call(x)? The field may still change: a record with a mutable field can be coerced to a type where the field is immutable, so f() may assign it through the other type.
  • Where do the assigned variables matter? Lam_pass_sroa makes the Lassigns and is the last Lambda pass, so the set is empty in Lam_pass_deep_flatten, Lam_pass_lets_dce and the inliner today; they compute it anyway, as Lam_pass_remove_alias does. It is not empty where Lam_compile inlines a function of another module under -bs-cross-module-opt, which happens after Lam_pass_sroa. There an argument that reads a local ref, by then an assigned variable, stays bound before a callee that assigns it; the new test in inline_arg_order_cross_module_test covers that.
✨ Collect the assigned variables without allocating for each node

b54982a · mununki · Nov 13, 2026, 14:14 UTC · new

Lambda_traverse.assigned_variables walked a term with iter, which allocates a closure for every node. The previous commit calls it in each Lam_pass_deep_flatten pass, which made the compiler allocate 0.6% more on the benchmarks. The walk now passes one callback to shallow_exists. With it, compiling Stdlib_List.res allocates 2,549,873 words where master allocates 2,588,255, since Lam_pass_remove_alias and Lam_pass_lets_dce already called it.

@github-actions

github-actions Bot commented Nov 13, 2026 •

Copy link
Copy Markdown

🎉 Compiler benchmarks

Allocated words

In total: 640,953,418 → 636,265,418 (-0.73%).

Generated projects: 920,899,498 → 920,079,781 (-0.09%).

🎉 Improvement: 23 benchmarks allocate more than 1% less.

Benchmark Base Head Change
stress/InlineChain.res 3,586,473 3,511,059 -2.10%
stress/LetRec.res 32,165,671 31,619,634 -1.70%
stress/NestedClosures.res 10,458,075 10,289,321 -1.61%
build/leaf-edit 13,199,551 13,011,629 -1.42%
packages/@rescript/runtime/Stdlib_List.res 2,726,309 2,688,404 -1.39%
packages/@rescript/belt/src/Belt_List.res 3,219,561 3,176,825 -1.33%
packages/@rescript/belt/src/Belt_Array.res 2,186,895 2,158,108 -1.32%
packages/@rescript/belt/src/Belt_SortArray.res 1,468,211 1,448,941 -1.31%
packages/@rescript/runtime/Primitive_object.res 1,171,634 1,156,308 -1.31%
packages/@rescript/belt/src/Belt_SortArrayInt.res 1,361,176 1,343,466 -1.30%
94 more changes
Benchmark Base Head Change
packages/@rescript/belt/src/Belt_SortArrayString.res 1,362,972 1,345,262 -1.30%
build/shared-edit 72,920,853 71,982,366 -1.29%
packages/@rescript/belt/src/Belt_internalAVLset.res 2,912,931 2,876,919 -1.24%
packages/@rescript/belt/src/Belt_internalAVLtree.res 3,550,767 3,506,941 -1.23%
build/no-op 9,307,729 9,196,893 -1.19%
packages/@rescript/belt/src/Belt_MutableSetInt.res 1,691,616 1,673,094 -1.09%
packages/@rescript/belt/src/Belt_MutableSetString.res 1,692,859 1,674,337 -1.09%
packages/@rescript/belt/src/Belt_MutableSet.res 1,934,212 1,913,596 -1.07%
packages/@rescript/runtime/Stdlib_Option.res 762,298 754,270 -1.05%
packages/@rescript/belt/src/Belt_internalMapInt.res 1,432,014 1,417,128 -1.04%
packages/@rescript/belt/src/Belt_internalMapString.res 1,433,864 1,418,978 -1.04%
packages/@rescript/belt/src/Belt_MutableQueue.res 799,705 791,495 -1.03%
packages/@rescript/runtime/Primitive_hash.res 976,384 966,396 -1.02%
packages/@rescript/belt/src/Belt_SetInt.res 1,308,967 1,296,639 -0.94%
packages/@rescript/belt/src/Belt_SetString.res 1,309,696 1,297,368 -0.94%
packages/@rescript/belt/src/Belt_internalSetInt.res 740,601 733,638 -0.94%
packages/@rescript/belt/src/Belt_internalSetString.res 741,792 734,829 -0.94%
packages/@rescript/belt/src/Belt_HashMapInt.res 1,076,526 1,066,803 -0.90%
packages/@rescript/belt/src/Belt_HashMapString.res 1,077,238 1,067,515 -0.90%
packages/@rescript/belt/src/Belt_MutableMap.res 1,320,213 1,308,311 -0.90%
packages/@rescript/belt/src/Belt_SetDict.res 1,468,191 1,454,964 -0.90%
packages/@rescript/belt/src/Belt_internalBuckets.res 977,537 968,749 -0.90%
packages/@rescript/belt/src/Belt_HashMap.res 1,217,499 1,206,761 -0.88%
packages/@rescript/runtime/Primitive_module.res 340,357 337,365 -0.88%
packages/@rescript/belt/src/Belt_HashSetInt.res 862,510 854,943 -0.88%
packages/@rescript/belt/src/Belt_HashSetString.res 862,829 855,262 -0.88%
packages/@rescript/belt/src/Belt_MapDict.res 1,654,983 1,640,516 -0.87%
build/Namespace/edit 7,943,518 7,875,599 -0.86%
packages/@rescript/belt/src/Belt_MutableMapInt.res 1,170,097 1,160,143 -0.85%
packages/@rescript/belt/src/Belt_MutableMapString.res 1,170,378 1,160,424 -0.85%
packages/@rescript/belt/src/Belt_HashSet.res 991,667 983,276 -0.85%
packages/@rescript/runtime/Stdlib_Array.res 1,293,092 1,282,258 -0.84%
packages/@rescript/belt/src/Belt_Set.res 869,516 862,260 -0.83%
packages/@rescript/runtime/Stdlib_Result.res 1,279,829 1,269,550 -0.80%
packages/@rescript/belt/src/Belt_Map.res 993,336 985,396 -0.80%
packages/@rescript/runtime/Primitive_list.res 205,182 203,550 -0.80%
packages/@rescript/belt/src/Belt_MapInt.res 1,196,915 1,187,640 -0.77%
packages/@rescript/belt/src/Belt_MapString.res 1,197,451 1,188,176 -0.77%
packages/@rescript/belt/src/Belt_internalSetBuckets.res 705,944 700,482 -0.77%
packages/@rescript/belt/src/Belt_Range.res 290,830 288,597 -0.77%
packages/@rescript/runtime/Primitive_array.res 193,451 191,967 -0.77%
packages/@rescript/belt/src/Belt_MutableStack.res 390,358 387,372 -0.76%
build/clean 238,622,662 236,842,310 -0.75%
packages/@rescript/belt/src/Belt_Option.res 391,673 388,828 -0.73%
packages/@rescript/runtime/Primitive_option.res 299,113 297,230 -0.63%
packages/@rescript/belt/src/Belt_Result.res 411,989 409,410 -0.63%
packages/@rescript/runtime/Stdlib_Int.res 457,244 454,467 -0.61%
packages/@rescript/runtime/Primitive_int.res 172,514 171,561 -0.55%
packages/@rescript/runtime/Primitive_bigint.res 177,966 177,013 -0.54%
packages/@rescript/runtime/Primitive_float.res 147,391 146,612 -0.53%
packages/@rescript/runtime/Primitive_string.res 160,147 159,311 -0.52%
packages/@rescript/runtime/Stdlib_Nullable.res 331,850 330,119 -0.52%
packages/@rescript/runtime/Stdlib_Null.res 334,315 332,584 -0.52%
packages/@rescript/runtime/Primitive_exceptions.res 201,869 200,824 -0.52%
stress/RefsInLoop.res 16,572,549 16,495,448 -0.47%
packages/@rescript/runtime/Stdlib_Lazy.res 222,162 221,140 -0.46%
packages/@rescript/runtime/Stdlib_Pair.res 165,640 164,914 -0.44%
packages/@rescript/runtime/Stdlib_JsError.res 230,039 229,187 -0.37%
packages/@rescript/belt/src/Belt_internalBucketsType.res 292,515 291,432 -0.37%
packages/@rescript/runtime/Stdlib_Ordering.res 152,140 151,623 -0.34%
packages/@rescript/runtime/Stdlib_AsyncIterator.res 295,783 294,834 -0.32%
packages/@rescript/runtime/Stdlib_JsExn.res 173,265 172,719 -0.32%
packages/@rescript/runtime/Primitive_bool.res 147,350 146,937 -0.28%
stress/PolyVariantSwitch.res 14,314,067 14,274,186 -0.28%
packages/@rescript/belt/src/Belt.res 188,177 187,667 -0.27%
packages/@rescript/runtime/Stdlib_AsyncIterableIterator.res 228,980 228,373 -0.27%
packages/@rescript/runtime/Dom_storage.res 166,901 166,473 -0.26%
packages/@rescript/runtime/Stdlib_JSON.res 297,562 296,799 -0.26%
packages/@rescript/runtime/Stdlib_String.res 504,429 503,148 -0.25%
packages/@rescript/runtime/Stdlib_Intl_NumberFormat_Grouping.res 159,846 159,451 -0.25%
packages/@rescript/runtime/Stdlib_Bool.res 146,054 145,695 -0.25%
packages/@rescript/runtime/Stdlib_AsyncGenerator.res 211,504 210,996 -0.24%
packages/@rescript/runtime/Stdlib.res 982,387 980,095 -0.23%
packages/@rescript/runtime/Stdlib_Generator.res 200,658 200,190 -0.23%
packages/@rescript/runtime/Stdlib_Type.res 201,446 200,977 -0.23%
packages/@rescript/runtime/Stdlib_Iterator.res 235,191 234,662 -0.22%
packages/@rescript/belt/src/Belt_Id.res 289,988 289,354 -0.22%
packages/@rescript/runtime/Stdlib_Float.res 218,563 218,090 -0.22%
stress/ObjectFields.res 20,692,937 20,649,376 -0.21%
packages/@rescript/runtime/Stdlib_Temporal_PlainMonthDay.res 268,042 267,535 -0.19%
stress/Record.res 26,163,126 26,117,565 -0.17%
packages/@rescript/runtime/Stdlib_Temporal_Instant.res 307,218 306,711 -0.17%
packages/@rescript/runtime/Stdlib_Temporal_PlainTime.res 319,810 319,303 -0.16%
packages/@rescript/runtime/Stdlib_Intl.res 173,941 173,669 -0.16%
packages/@rescript/runtime/Stdlib_Temporal_Duration.res 372,680 372,173 -0.14%
packages/@rescript/runtime/Stdlib_BigInt.res 206,835 206,556 -0.13%
packages/@rescript/runtime/Stdlib_Temporal_PlainYearMonth.res 381,518 381,011 -0.13%
packages/@rescript/runtime/Stdlib_Math.res 326,844 326,429 -0.13%
packages/@rescript/runtime/Stdlib_Temporal_PlainDate.res 402,875 402,368 -0.13%
packages/@rescript/runtime/Stdlib_Temporal.res 223,954 223,682 -0.12%
packages/@rescript/runtime/Stdlib_IterableIterator.res 178,852 178,641 -0.12%
packages/@rescript/runtime/Stdlib_Temporal_PlainDateTime.res 436,952 436,445 -0.12%
build/WideDeps/clean 495,966,292 495,445,173 -0.11%
stress/IntSwitch.res 29,805,072 29,775,051 -0.10%

Instructions

In total: 9,209,714,034 → 9,216,006,858 (+0.07%).

Benchmark Base Head Change
stress/InlineChain.res 137,106,248 137,524,233 +0.30%
stress/NestedClosures.res 528,585,490 529,990,301 +0.27%
stress/LetRec.res 1,371,741,696 1,374,657,056 +0.21%
packages/@rescript/belt/src/Belt_List.res 117,684,665 117,920,948 +0.20%
packages/@rescript/belt/src/Belt_internalAVLtree.res 135,852,262 136,109,364 +0.19%
stress/RefsInLoop.res 389,341,061 389,869,666 +0.14%
packages/@rescript/runtime/Stdlib_Math.resi 21,221,553 21,195,841 -0.12%
packages/@rescript/runtime/Stdlib_List.resi 24,554,373 24,528,149 -0.11%

Output size

In total: 25,843,406 → 25,843,830 (±0.00%).

None of the 13 benchmarks changed by 0.1% or more.

Pull request b54982a compared with base ed08a8c in this run.

@codecov

codecov Bot commented Nov 13, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.05882% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 82.07%. Comparing base (ed12abd) to head (b54982a).
⚠️ Report is 3 commits behind head on master.

Files with missing lines Patch % Lines
compiler/core/lam_beta_reduce.ml 0.00% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##           master    #9296   +/-   ##
=======================================
  Coverage   82.07%   82.07%           
=======================================
  Files         455      455           
  Lines       65361    65414   +53     
=======================================
+ Hits        53642    53690   +48     
- Misses      11719    11724    +5     
Files with missing lines Coverage Δ
compiler/core/lam_analysis.ml 69.58% <100.00%> (+3.10%) ⬆️
compiler/core/lam_beta_reduce_util.ml 98.24% <100.00%> (+1.42%) ⬆️
compiler/core/lam_pass_deep_flatten.ml 89.02% <100.00%> (+0.27%) ⬆️
compiler/core/lam_pass_lets_dce.ml 85.71% <100.00%> (+0.38%) ⬆️
compiler/core/lam_pass_sroa.ml 85.71% <100.00%> (ø)
compiler/core/lam_util.ml 95.45% <100.00%> (+0.37%) ⬆️
compiler/ml/lambda_traverse.ml 93.49% <100.00%> (+0.11%) ⬆️
compiler/core/lam_beta_reduce.ml 76.66% <0.00%> (ø)

... and 11 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@pkg-pr-new

pkg-pr-new Bot commented Nov 13, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

rescript

npm i https://pkg.pr.new/rescript@9296

@rescript/belt

npm i https://pkg.pr.new/@rescript/belt@9296

@rescript/darwin-arm64

npm i https://pkg.pr.new/@rescript/darwin-arm64@9296

@rescript/linux-arm64

npm i https://pkg.pr.new/@rescript/linux-arm64@9296

@rescript/linux-x64

npm i https://pkg.pr.new/@rescript/linux-x64@9296

@rescript/runtime

npm i https://pkg.pr.new/@rescript/runtime@9296

@rescript/win32-x64

npm i https://pkg.pr.new/@rescript/win32-x64@9296

commit: b54982a

`Lambda_traverse.assigned_variables` walked a term with `iter`, which
allocates a closure for every node. The previous commit calls it in each
`Lam_pass_deep_flatten` pass, which made the compiler allocate 0.6% more
on the benchmarks. The walk now passes one callback to `shallow_exists`.
With it, compiling `Stdlib_List.res` allocates 2,549,873 words where
master allocates 2,588,255, since `Lam_pass_remove_alias` and
`Lam_pass_lets_dce` already called it.

Signed-off-by: mununki <[email protected]>
@github-actions

Copy link
Copy Markdown

@mununki
mununki requested review from cknitt and cristianoc November 13, 2026 14:33
@mununki

mununki commented Nov 13, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-13T14:35:22.868407Z b54982a Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

Reviewed commit: b54982abbc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

let x = e; f(x) evaluates e after the callee f

1 participant