Normalize Lambda terms where they are built - #8615
Open
cristianoc wants to merge 4 commits into
Open
Conversation
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 Report❌ Patch coverage is
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
🚀 New features to boost your workflow:
|
rescript
@rescript/belt
@rescript/darwin-arm64
@rescript/darwin-x64
@rescript/linux-arm64
@rescript/linux-x64
@rescript/runtime
@rescript/win32-x64
commit: |
|
Developer playground preview: https://rescript-lang.github.io/rescript/dev-playground/?version=pr-8615 |
cknitt
approved these changes
Sep 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
Lstaticraise (0, [])inside a conditional andrecovered 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 andmk_builtin's primitive case built theirnodes directly; both now go through the folding constructors.
Routing
mk_builtinmoves folding from the optimizer passes to construction,which changes what the integer switcher sees.
switch_action_count_test.respins 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.