Repository navigation
Decide the untagged dispatch's literal cases by routing them - #9047
Conversation
Changelog💅 Polish
Semantic Diff2 commits · Full page Decide the untagged dispatch's literal cases by routing them446dce1 · cknitt + Claude · Oct 25, 2026, 20:37 UTC
A literal without an arm skips the literal switch when the payload dispatch, evaluated on it with Generated code changed (a literal without an arm beside a class case): @unboxed type t = | @as("a") A | S(string) | N(float) | D(Date.t) | O({x: int})
let f = (x: t) => switch x { | D(_) => 1 | O(_) => 2 | _ => 3 }Before (JS): if (x === "a") {
return 3;
}
switch (typeof x) {
case "string" :
case "number" :
return 3;
default:
...
}After (JS): switch (typeof x) {
case "string" :
case "number" :
return 3;
default:
...
}Generated code changed (arms of different kinds with one body): @unboxed type t = | @as(null) NoPoint | Point({x: int}) | Name(string)
switch x {
| NoPoint => Console.log("null")
| Point(_) => Console.log("payload")
| Name(_) => Console.log("payload")
}Before (JS): if (x === null) {
console.log("null");
return;
}
if (typeof x === "string") {
console.log("payload");
return;
}
console.log("payload");After (JS): if (x === null) {
console.log("null");
return;
}
console.log("payload");✨ Leave the cases falling through to the default of a complete typeof switch7085291 · cknitt + Claude · Oct 26, 2026, 06:11 UTC · new
The cases falling through to the last arm are left out with it. Generated code changed (kinds sharing the last arm's body): @unboxed type t = Num(int) | Text(string) | Flag(bool)
let f = (x: t) => switch x { | Num(_) => 1 | Text(_) => 2 | Flag(_) => 2 }Before (JS): switch (typeof x) {
case "number" :
return 1;
case "string" :
default:
return 2;
}After (JS): if (typeof x === "number") {
return 1;
} else {
return 2;
} |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## dispatch-plan-fold #9047 +/- ##
=====================================================
Coverage ? 81.37%
=====================================================
Files ? 458
Lines ? 64499
Branches ? 0
=====================================================
Hits ? 52485
Misses ? 12014
Partials ? 0
🚀 New features to boost your workflow:
|
rescript
@rescript/belt
@rescript/darwin-arm64
@rescript/linux-arm64
@rescript/linux-x64
@rescript/runtime
@rescript/win32-x64
commit: |
|
@codex review |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
@codex review |
|
Codex Review: Didn't find any major issues. Hooray! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
Why:
A literal without an arm got a case whenever the variant had a class
case, even where no class check runs on it, and arms of different kinds
with one body kept the checks telling them apart. The literal arm left
to a complete payload dispatch with one body assumed that dispatch runs
no check, which held only because matching shares such bodies.
A literal without an arm skips the literal switch when the payload dispatch,
evaluated on it with `select`, takes it to the default without a class check.
The plan is simplified: a test whose branches run the same, and a case that
runs what its default runs, are left out, and a literal with the body of a
payload dispatch that runs one body unchecked gets no case. The property test
also checks that no literal or primitive reaches a class check.
`Variant_runtime.literal_passes` is gone.
Generated code changed (a literal without an arm beside a class case):
Example:
@unboxed type t = | @as("a") A | S(string) | N(float) | D(Date.t) | O({x: int})
let f = (x: t) => switch x { | D(_) => 1 | O(_) => 2 | _ => 3 }
Before (JS):
if (x === "a") {
return 3;
}
switch (typeof x) {
case "string" :
case "number" :
return 3;
default:
...
}
After (JS):
switch (typeof x) {
case "string" :
case "number" :
return 3;
default:
...
}
Generated code changed (arms of different kinds with one body):
Example:
@unboxed type t = | @as(null) NoPoint | Point({x: int}) | Name(string)
switch x {
| NoPoint => Console.log("null")
| Point(_) => Console.log("payload")
| Name(_) => Console.log("payload")
}
Before (JS):
if (x === null) {
console.log("null");
return;
}
if (typeof x === "string") {
console.log("payload");
return;
}
console.log("payload");
After (JS):
if (x === null) {
console.log("null");
return;
}
console.log("payload");
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CMkrYaktctdKc4M41WF36t
Signed-off-by: Christoph Knittel <ck@cca.io>
…witch
Why:
In a complete dispatch on primitive kinds only, the last arm becomes the
default, but the cases of other kinds sharing its body stayed in the
switch, falling through into `default:`. That also kept the dispatch
from being one unchecked body, so a literal with that body kept its case.
The cases falling through to the last arm are left out with it.
Generated code changed (kinds sharing the last arm's body):
Example:
@unboxed type t = Num(int) | Text(string) | Flag(bool)
let f = (x: t) => switch x { | Num(_) => 1 | Text(_) => 2 | Flag(_) => 2 }
Before (JS):
switch (typeof x) {
case "number" :
return 1;
case "string" :
default:
return 2;
}
After (JS):
if (typeof x === "number") {
return 1;
} else {
return 2;
}
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CMkrYaktctdKc4M41WF36t
Signed-off-by: Christoph Knittel <ck@cca.io>
83f1543 to
7085291
Compare
|
@codex review |
|
Codex Review: Didn't find any major issues. Breezy! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
Stacked on #9046.
With the dispatch as a plan, two decisions can use the facts they were missing.
Literal cases by routing. A literal without an arm used to get a
casewhenever the variant had a class case, even where no class check runs on it. Now it skips the literal switch exactly when the payload dispatch, evaluated on it withselect, takes it to the default without a class check. A class check is avoided because it throws when the class does not exist at run time. This replaceshas_class_caseandreaches_a_payload_arm, andVariant_runtime.literal_passesis gone.Simplification. A test whose branches run the same body, and a
casethat runs what its default runs, are left out. Arms of different kinds with one body thus become one unchecked branch. The literal arm left to "a complete payload dispatch with one body" now requires the payload plan to really be one unchecked body. Before, that held only because matching usually shares such bodies: the property test found plans where a literal reached aninstanceof.Generated code changes:
if (x === "a") return 3;is no longer emitted ahead of aswitch (typeof x)that already sends"a"toreturn 3.if (typeof x === "string") { log; return } log;becomeslog;.UntaggedVariants.res,preserveDiscriminatorGroupsmerges three equal loop bodies.Num(_) => 1 | Text(_) => 2 | Flag(_) => 2becomesif (typeof x === "number") return 1; else return 2;, and a literal with that body loses its case.New tests:
untagged_variants_heuristic.resaddsdateOrObject,logPointOrName,numberOrTextandletterOrNumber.Spec rule 7 now states the routing rule and the simplification.
Deliberately not included: dropping the
caseof a literal that has its own arm when the payload dispatch would run the same body anyway; it would save little and add a rule.🤖 Generated with Claude Code
https://claude.ai/code/session_01CMkrYaktctdKc4M41WF36t