Skip to content

ES module output: for's iterators call themselves through a letfn local - #15

Closed
k7d wants to merge 2 commits into
esm-constfrom
esm-for-self-ref
Closed

k7d wants to merge 2 commits into
esm-constfrom
esm-for-self-ref

Conversation

@k7d

@k7d k7d commented Oct 29, 2026

Copy link
Copy Markdown
Member

Stacked on #14 (esm-const).

In ES module output, for's iterator fns are bound with letfn* to an anonymous fn and call themselves through that local, instead of being named function expressions that call themselves by name. React Compiler rejects the named self-calls ("Expected value kind to be initialized"), so any component using for was skipped.

  • cljs.core/for (macro): in ES module output it emits (letfn* [iter (fn [s] ...)] iter) instead of (fn iter [s] ...). Classic output is unchanged.
  • Analyzer, letfn*: a fn named after its binding (letfn's) still refers to itself by its own name. An anonymous fn now refers to itself through the binding. Before, the self-call resolved to a namespace var that doesn't exist.

React Compiler

On a test component using for, case, loop and useState (babel-plugin-react-compiler 1.0), the component compiles with 8 memo slots, where before it was rejected. The compiled output, with components marked "use memo", renders the same HTML through React's server renderer as without React Compiler.

I also tried hoisting the letfn into the function's hoisted lets instead of an IIFE. React Compiler accepted the component but memoized nothing (0 slots), so the IIFE stays.

Next blocker, in a separate PR: sequential destructuring ((let [[a b] (useState ..)] ..)) compiles to the v?.nth ? v.nth(..) : nth(..) fast path, which React Compiler treats as an unknown call; it memoizes much less (0 instead of 10 slots in a two-useState component).

Performance

for micro-benchmarks against esm-const (release build, rolldown-minified, Node 26, 3 alternating runs):

Benchmark esm-const this branch
for over 1,000 items ~24.5 ms ~24.5 ms
for with nested :when ~23.8 ms ~24.5 ms (+3%)
small for repeated many times ~20.4 ms ~21.9 ms (+6%)
for inside a loop ~99 ms ~106 ms (+6%)

The cost is per for expression (the IIFE binding the letfn), not per item.

Tests

  • script/test-esm: 0 failures, 0 errors. It includes a new build test of for with :let, :when and :while, nested bindings, for in a loop and an infinite sequence, and that there are no named iterator function expressions.
  • Compiler tests: 0 failures, 0 errors.

@k7d

k7d commented Oct 29, 2026

Copy link
Copy Markdown
Member Author

Fix from review (fb3768b): every for in expression position became an IIFE binding its letfn, an extra closure and call per evaluation. Now for uses letfn iterators only inside ^:memo fns (React Compiler's; ^:memo marks the analyzer env :memo). Everywhere else it emits named fn expressions, as before, with no IIFE.

The IIFE stays inside ^:memo fns: with the iterator hoisted into the function's locals instead, React Compiler stopped caching the seq separately (no guard keyed on its input in a probe).

Merged #14's fixes in. script/test-esm and the compiler tests pass.

k7d added 2 commits October 30, 2026 09:14
… ES module output, not as named function expressions
…ir env 📝 elsewhere named fn expressions, without the IIFE
@k7d

k7d commented Oct 30, 2026

Copy link
Copy Markdown
Member Author

Restacked on main with #21 merged (312dfa40), force-pushed:

The review-fix merges are folded into the branches' own commits. The code is unchanged apart from #21's changes. Only CHANGELOG.md and the build tests needed conflicts resolved, and each branch's [Next] section now has main's entries plus its own.

On every branch, script/test-esm and the compiler tests pass, including #21's new test-locals-munged-to-the-same-name. On #17 the Node tests pass too, and the react-compiler template works in the dev server (memoized rows).

@k7d
k7d marked this pull request as ready for review October 30, 2026 12:29
@k7d

k7d commented Oct 30, 2026

Copy link
Copy Markdown
Member Author

Closing: this change only mattered for React Compiler. Since the review fix (for's letfn iterators only inside ^:memo fns), it does nothing without ^:memo, so it's moved into #17 with the rest of the React Compiler logic (^:memo marking the analyzer env, for's iterators in ^:memo fns, its test). #16 is now based on main.

@k7d k7d closed this Oct 30, 2026
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.

1 participant