Repository navigation
Conversation
Changelog💅 Polish
Semantic Diff5 commits · Full page Let builtin types state their runtime shapes2271e3e · cknitt + Claude · Oct 24, 2026, 10:30 UTC
The declarations of Diagnostic modified (annotation over a builtin) @typeOf("string") type t = int
-@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").Diagnostic modified (contradicting annotation) @typeOf("symbol") type sym
@typeOf("number") type t = sym
-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").Resolve the literal of a constructor without
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## untagged-dispatch #9017 +/- ##
==================================================
Coverage 81.30% 81.30%
==================================================
Files 456 456
Lines 64220 64210 -10
==================================================
- Hits 52211 52208 -3
+ Misses 12009 12002 -7
🚀 New features to boost your workflow:
|
|
@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. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a16ad0b18f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
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".
rescript
@rescript/belt
@rescript/darwin-arm64
@rescript/linux-arm64
@rescript/linux-x64
@rescript/runtime
@rescript/win32-x64
commit: |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 899176311f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
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".
|
@codex review |
|
Codex Review: Didn't find any major issues. Bravo. 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:
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`.
Diagnostic modified (annotation over a builtin):
Example:
@typeof("string") type t = int
Before (diagnostic):
@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.
After (diagnostic):
This annotation, @typeof("string"), contradicts the abbreviated type, whose values are recognized by @typeof("number").
Diagnostic modified (contradicting annotation):
Example:
@typeof("symbol") type sym
@typeof("number") type t = sym
Before (diagnostic):
This annotation, @typeof("number"), contradicts the abbreviated type's annotation, @typeof("symbol").
After (diagnostic):
This annotation, @typeof("number"), contradicts the abbreviated type, whose values are recognized by @typeof("symbol").
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>
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.
Representation modified (constructor tag in the layout):
Example:
type t = A | @as("b") B
Before (layout):
A -> {name: "A", literal: None}
B -> {name: "B", literal: Some (String "b")}
After (layout):
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.
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>
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.
Diagnostic modified (@notundefined on an abbreviation):
Example:
@notundefined
type t = option<int>
Before (diagnostic):
@notundefined can only be used on abstract types
After (diagnostic):
@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.
Diagnostic modified (@notundefined on a record):
Example:
@notundefined
type t = {x: int}
Before (diagnostic):
@notundefined can only be used on abstract types
After (diagnostic):
@notundefined can only be used on abstract types and type abbreviations, whose runtime representation it describes.
JS modified (refined abstract type):
Example:
type opaque
@notundefined type refined = opaque
let wrap = (x: refined) => Some(x)
Before (js): error
After (js):
function wrap(x) {
return x;
}
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>
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.
JS modified (folded match of a coerced constant):
Example:
type source = | @as(1.) One
type target = | @as(1.0) One | Two | Three | Four
let isOne = (x: target) =>
switch x {
| One => true
| _ => false
}
let r = isOne((One: source :> target))
Before (js): let r = false;
After (js): let r = true;
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>
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.
JS modified (folded match of a coerced int constant):
Example:
type source = | @as(1) One
type target = | @as(1.) One | Two | Three | Four
let isOne = (x: target) =>
switch x {
| One => true
| _ => false
}
let r = isOne((One: source) :> target)
Before (js): let r = false;
After (js): let r = true;
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>
7cb6fa9 to
f42bd93
Compare
Stacked on #9015. Declaration-side follow-ups from the architecture review of the untagged variant work, one commit each:
Predefdeclaresint,float,bigint,string,bool,dict,arrayandpromisewith thetype_shapean annotation would give them. The list of builtin paths inget_block_type_from_typis gone.@typeOf("number") type count = intis now a restatement.@typeOf("string") type t = intis now reported as a contradiction.@typeOf("number") type tacceptstype t = int.@asis resolved once.0for[]. Every reader takes it as is, with noNonecase each.Variant_runtime.matchable_tagandto_matchable_tagare gone.@asread the constructor's annotation.Aand@as("A") Aagree across an interface and in a coercion, and constant folding compares any two constructors by runtime value.@notUndefinedon type abbreviations.@instanceOf/@typeOf: it can refine a type whose representation the compiler doesn't see, or restate that a type can't be undefined. A restatement keeps the payload kind, so@notUndefined type count = intis still a number case.Typeopt.undefinedness. For that,get_unboxed_type_representationmoves fromTypedecltoCtype.The spec (
compiler/ml/UntaggedVariants.md) is updated in rules 2, 4, 5, 13 and 14.New tests:
untagged_builtin_alias_test.res: an annotation restatingint, and an interface stating@typeOf("number")overint.as_constructor_name_test.res: coercion and interface inclusion betweenAand@as("A") A.not_undefined_abbreviation_test.res: a refined abstract type isn't boxed inoption, and a restated builtin keeps its kind.ounit_lambda_constant_tests.ml: constructors compare by runtime value when folded.runtime_annotation_contradicts_builtin.res;not_undefined_on_option.res;not_undefined_on_functor_parameter_type.res;invalid_attribute_not_undefined.res, now a record, since@notUndefined type t = intis valid.🤖 Generated with Claude Code
https://claude.ai/code/session_01CMkrYaktctdKc4M41WF36t