Semantic Diff

#9017 Resolve untagged declaration shapes and literals once

8 commits

66880b8db80f83ff7f07d1eb128fa9c4f49ee2e2..pr-9017

e8d8e6271 · cknitt + Claude · Oct 24, 2026, 10:30 UTC

Let builtin types state their runtime shapes

Why

Payload kinds had two sources: the shapes declarations state, and a list of builtin paths in get_block_type_from_typ. Builtins were outside the generic checks, so an annotation could not restate a builtin's kind and an interface could not state it over a builtin implementation.

The declarations of int, float, bigint, string, bool, dict, array and promise in Predef state their shapes, and only tuples and function types are classified structurally. @typeOf("number") type count = int is a restatement, @typeOf("string") type t = int a contradiction, and an interface's @typeOf("number") type t accepts type t = int.

modifiedDiagnostic annotation over a builtin

@typeOf("string") type t = int
diagnostic
Before
After
@typeOf on a type abbreviation must describe an abstract type, or a type with the same annotation. The abbreviated type already has a runtime representation the compiler knows, or can be undefined.This annotation, @typeOf("string"), contradicts the abbreviated type, whose values are recognized by @typeOf("number").

modifiedDiagnostic contradicting annotation

@typeOf("symbol") type sym@typeOf("number") type t = sym
diagnostic
Before
After
This annotation, @typeOf("number"), contradicts the abbreviated type's annotation, @typeOf("symbol").This annotation, @typeOf("number"), contradicts the abbreviated type, whose values are recognized by @typeOf("symbol").
041282ab0 · cknitt + Claude · Oct 24, 2026, 10:40 UTC

Resolve the literal of a constructor without @as once

Why

"A constructor without @as is its name" was re-derived wherever a tag was read: in the matching facts, constant folding, code generation, the duplicate-literal check, coercions and gentype, each with its own None case, and [] special-cased in several of them.

The layout stores every constructor's literal, the name when there is no @as and 0 for []. Readers take it as is, and matching compares tags without the widening to an optional literal. Messages that name the declared @as read the constructor's annotation. Since the literal is what the value is, A and @as("A") A now agree across an interface and in a coercion, and constant folding compares any two constructors by their runtime values.

modifiedRepresentation constructor tag in the layout

type t = A | @as("b") B
layout
Before
After
A -> {name: "A", literal: None}B -> {name: "B", literal: Some (String "b")}A -> {name: "A", literal: String "A"}B -> {name: "B", literal: String "b"}

Review

  • Does this change generated JavaScript? No snapshot changed: the resolved literal is the value every reader computed before.
9609b3c8b · cknitt + Claude · Oct 24, 2026, 10:46 UTC

Allow @notUndefined on type abbreviations

Why

@instanceOf and @typeOf can refine or restate the type an abbreviation stands for, while @notUndefined was rejected on every abbreviation, even of an abstract type it could refine.

@notUndefined on an abbreviation follows the rule of the other annotations: it can refine a type whose representation the compiler does not see, or restate that the type cannot be undefined, keeping a payload kind the type has. The check whether a type can be undefined is shared with interface inclusion (Typeopt.undefinedness); get_unboxed_type_representation moves to Ctype so that Typedecl can call it.

modifiedDiagnostic @notUndefined on an abbreviation

@notUndefinedtype t = option<int>
diagnostic
Before
After
@notUndefined can only be used on abstract types@notUndefined on a type abbreviation must describe an abstract type, or a type with the same annotation. The abbreviated type already has a runtime representation the compiler knows, or can be undefined.

modifiedDiagnostic @notUndefined on a record

@notUndefinedtype t = {x: int}
diagnostic
Before
After
@notUndefined can only be used on abstract types@notUndefined can only be used on abstract types and type abbreviations, whose runtime representation it describes.

modifiedJS refined abstract type

type opaque@notUndefined type refined = opaquelet wrap = (x: refined) => Some(x)
js
Before
After
error function wrap(x) { return x;}
956a1eadf · cknitt + Claude · Oct 24, 2026, 12:02 UTC

Fold matches of constructors by their runtime literals

Why

A coercion accepts constructors whose literals are spelled differently but are the same value, such as @as(1.) for @as(1.0), while a match on a constant constructor was folded by comparing tags structurally, so the coerced constant missed its arm.

Constant constructors and tagged blocks are now found by the runtime value of their literal, as the untagged dispatch already does.

modifiedJS folded match of a coerced constant

type source = | @as(1.) Onetype target = | @as(1.0) One | Two | Three | Fourlet isOne = (x: target) =>  switch x {  | One => true  | _ => false  }let r = isOne((One: source :> target))
js
Before
After
let r = false;let r = true;
e3b96dbb0 · cknitt + Claude · Oct 24, 2026, 16:46 UTC

Fold matches of numeric constants by their runtime value

Why

A constant constructor represented by an int is lowered to an int, and a match on it was folded by comparing with the int tags of the matched variant only. A coercion accepts @as(1) for @as(1.), so the coerced constant missed its arm.

The int is compared with every constant's literal by value.

modifiedJS folded match of a coerced int constant

type source = | @as(1) Onetype target = | @as(1.) One | Two | Three | Fourlet isOne = (x: target) =>  switch x {  | One => true  | _ => false  }let r = isOne((One: source) :> target)
js
Before
After
let r = false;let r = true;
c06743d2f · cknitt + Claude · Oct 24, 2026, 16:23 UTC

Move the variant invariant check next to the layout

check_invariant checks a layout, and Variant_layout, which derives the layout, is its only caller; Ast_untagged_variants keeps the attributes and errors. Its doc comment, which described another function, now says what it checks.

df535bf2d · cknitt + Claude · Oct 24, 2026, 16:25 UTC

Compare every runtime shape claim of an interface in one place

Interface inclusion compared classes and kinds in Includecore and @notUndefined in Includemod, the two levels of one declared shape. Includecore.compare_runtime_annotations now checks both; the messages are unchanged.

36bd1918d · cknitt + Claude · Oct 25, 2026, 05:11 UTC

Merge the @notUndefined rule into the rule on declared shapes

@notUndefined, @typeOf and @instanceOf declare levels of one stored shape, checked on abbreviations by one entry point and across interfaces by one comparer. Rule 5 now describes the three together, with their errors and interface checks; rule 13 is gone and the rule on @tag and interfaces becomes rule 13.