Skip to content

Normalize Lambda terms where they are built - #8615

Open
cristianoc wants to merge 4 commits into
masterfrom
lambda/normalize-at-construction
Open

Normalize Lambda terms where they are built#8615
cristianoc wants to merge 4 commits into
masterfrom
lambda/normalize-at-construction

Conversation

@cristianoc

Copy link
Copy Markdown
Collaborator

Lambda has smart constructors that normalize as they build, but a few terms
were still assembled by hand and so entered the IR unnormalized. This closes
the two that remained, and removes the one place where a foldable shape was
the only record of a decision.

  • A match guard was encoded as Lstaticraise (0, []) inside a conditional and
    recovered later by recognising that shape. Normalization could erase it. The
    guard is now carried as structured data until its fallthrough is known.
  • Lambda.apply's eta reduction and mk_builtin's primitive case built their
    nodes directly; both now go through the folding constructors.

Routing mk_builtin moves folding from the optimizer passes to construction,
which changes what the integer switcher sees. switch_action_count_test.res
pins both directions: one switch gets a shorter plan, another loses its jump
table. Both are correct; the snapshots exist so a future change to when
folding happens shows its win and its cost in the same diff.

Part of #8573. First of a six-PR stack.

cristianoc and others added 4 commits September 4, 2026 14:30
Matching encoded a guard as a term and recovered it afterwards by shape:
translcore emitted [if cond then body else Lstaticraise (0, [])], and
is_guarded / patch_guarded recognised that shape to substitute the real
fallthrough. Exit zero was a sentinel, not an exit.

Normalization cannot know a shape is a message. Fold the condition of
[x if 1 > 2] and if_ correctly returns the else branch, so the action becomes
a bare raise to an exit that has no catch, is_guarded stops recognising it,
nothing patches it, and the raise reaches codegen alone. That is what broke
the analysis corpus when mk_builtin started folding.

Carry the guard as data instead. A case's right-hand side is now

  type action = {
    binds: (let_kind * Ident.t * Lambda.t) list;
    guard: Lambda.t option;
    body: Lambda.t;
  }

lowered at the single point where the fallthrough exists - the row that
matches in compile_match. The bindings are there because simplification
brings pattern variables into scope with lets that must cover the guard as
well as the body; keeping them as data preserves that without a term to
recurse through. staticfail, is_guarded and patch_guarded are deleted, and
exit zero is no longer magic.

Comparing actions needs a stand-in for the fallthrough, and it must be a
fresh variable rather than a constant: with unit, [when g => e] and
[_ => if g then e else ()] produce the same key, and merging them loses one
evaluation of g. The variable is created per comparison, so its freshness
does not depend on ident stamps surviving Ident.reinit between units.

guard_action_test pins both: a guard that folds to false is not a missing
guard, and two actions that differ only in where the condition sits are not
the same action. Each fails on the unfixed compiler.

Generated JavaScript is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W8g8qwBARAcvW9MyuKQq8H
apply sits above prim and so cannot call it. Nothing between the two refers
to apply except its own recursion, so it moves down. This commit changes no
content: the file's lines are identical to before, only their position
differs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W8g8qwBARAcvW9MyuKQq8H
lambda.mli says a term is built through the constructors, seven of which
normalize as they build. apply did not: its eta reduction substituted the
call's arguments into the inner primitive and then rebuilt the result with a
raw Lprim, so a primitive applied to constants was left unfolded.

    ((a, b) => a + b)(1, 2)

left translation as (+ 1 2) rather than 3. It could only do this by owning
the type; the reason it did was position, which the preceding commit fixed.

Generated JavaScript is unchanged: the optimizer passes were folding this on
their way past, so the same code comes out. It now happens once, at
construction, instead of being rediscovered on every full-tree rebuild.

Two of the three raw constructions in lambda.ml remain - offset_ref and
mk_builtin. Neither is a tidy-up: mk_builtin makes constant guards fold at
production, which is only safe since guards became data, and offset_ref is
untested.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W8g8qwBARAcvW9MyuKQq8H
mk_builtin's primitive case built its node directly, so a builtin was the
one place a term entered Lambda without passing the constructor that
normalizes it. It now goes through prim.

That moves folding from the optimizer passes to construction, and the
integer switcher plans over the set of distinct actions it is handed, so
what it sees changes. The two cases added here are what that cost and
bought. In the first, `10 + 10` merges with two `20` arms and four
branches with both results duplicated collapse to two with neither. In
the second the same merge drops the action count below the density the
switcher wants for a jump table, and a switch becomes a chain of
comparisons. Both plans are correct in both cases; only the emitted code
differs, and the snapshots are here so a future change to when folding
happens shows its win and its cost in the same diff.

Signed-off-by: Cristiano Calcagno <ccrisccris@gmail.com>

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W8g8qwBARAcvW9MyuKQq8H
@codecov

codecov Bot commented Sep 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.88889% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.33%. Comparing base (56fe3bc) to head (0b18d7a).

Files with missing lines Patch % Lines
compiler/ml/lambda.ml 68.42% 6 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##           master    #8615   +/-   ##
=======================================
  Coverage   77.32%   77.33%           
=======================================
  Files         467      467           
  Lines       63313    63326   +13     
=======================================
+ Hits        48957    48970   +13     
  Misses      14356    14356           
Files with missing lines Coverage Δ
compiler/ml/matching.ml 77.58% <100.00%> (+0.25%) ⬆️
compiler/ml/translcore.ml 86.13% <100.00%> (+0.02%) ⬆️
compiler/ml/lambda.ml 72.39% <68.42%> (-0.13%) ⬇️
🚀 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 Sep 4, 2026

Copy link
Copy Markdown

Open in StackBlitz

rescript

npm i https://pkg.pr.new/rescript-lang/rescript@8615

@rescript/belt

npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/belt@8615

@rescript/darwin-arm64

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

@rescript/darwin-x64

npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/darwin-x64@8615

@rescript/linux-arm64

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

@rescript/linux-x64

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

@rescript/runtime

npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/runtime@8615

@rescript/win32-x64

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

commit: 0b18d7a

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown

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.

2 participants