Semantic Diff

#9047 Decide the untagged dispatch's literal cases by routing them

2 commits

3cf406653f3d91b15a8b07eef3ff5a710af2989b..pr-9047

446dce1d8 · cknitt + Claude · Oct 25, 2026, 20:37 UTC

Decide the untagged dispatch's literal cases by routing them

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):

@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");
708529147 · cknitt + Claude · Oct 26, 2026, 06:11 UTC

Leave the cases falling through to the default of a complete typeof switch

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):

@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;}