Repository navigation
Conversation
Changelog💅 Polish
Semantic Diff13 commits · Full page Let builtin types state their runtime shapes601022f · 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
|
| Before | After |
|---|---|
|
|
|
Diagnostic changed (an abbreviation of a function type):
@typeOf("string") type cb = int => intdiagnostic
-@typeOf on a type abbreviation must describe an abstract type, or a type
-with the same annotation. ...
+This annotation, @typeOf("string"), contradicts the abbreviated type,
+whose values are recognized by @typeOf("function").Read the three runtime annotations on one path
50e1ecb · cknitt + Claude · Oct 25, 2026, 14:09 UTC
Why
@notUndefinedwas parsed, checked on declarations and checked on abbreviations apart from@instanceOfand@typeOf, so each step had two versions, and a type could carry it beside the others.
process_runtime_annotation returns Not_undefined as well, so one parse feeds declaration_shape, the check for represented types and Typedecl.check_manifest_annotation, which now checks all three on an abbreviation. Typeopt.abstractness takes a type and is what undefinedness falls back to.
Diagnostic changed (two of the three annotations):
@notUndefined @typeOf("string") type tdiagnostic
-none
+A type can have only one of @notUndefined, @instanceOf and @typeOf.
+@instanceOf and @typeOf already imply @notUndefined.Count untagged payload kinds in one table
33b9f36 · cknitt + Claude · Oct 25, 2026, 14:11 UTC
Why
check_invariantkept a counter per payload kind and rechecked all of them after every case, with an error variant per kind.
The kinds seen so far are one table, classes keyed by name, and a second case of a kind is reported where it is added. AtMostOneOfKind replaces the eight AtMostOne* errors and keeps their messages.
See through private abbreviations and JavaScript object types
da8baf1 · cknitt + Claude · Oct 25, 2026, 15:00 UTC
Why
The structural classifier did not expand private abbreviations, so
private rof a record was unknown whileprivate stringwas a string, and it called JavaScript object types unknown although their typeof is always "object". Contradictions with an array type printed it as@typeOf("object").
payload_kind expands private abbreviations and classifies {"x": int} as an object. An annotation contradicting an array type names Array.isArray, and @instanceOf over an array type has an error of its own.
Typing rules changed (payload kinds of rule 4):
type r = {x: int}
type pr = private r
@unboxed type t = A(pr) | B({"y": int}) | S(string)rule
| Before | After |
|---|---|
|
|
|
Diagnostic changed (an annotation over a tuple):
@typeOf("object") type t = private (int, int)diagnostic
This annotation, @typeOf("object"), contradicts the abbreviated type,
-whose values are recognized by @typeOf("object").
+whose values are recognized by Array.isArray.Diagnostic added (a class over an array type)
@instanceOf("MyArr") type myArr = private array<int>diagnostic
-This annotation, @instanceOf("MyArr"), contradicts the abbreviated type,
-whose values are recognized by @typeOf("object").
+@instanceOf("MyArr") cannot describe the values of an array type: they
+are recognized by Array.isArray, and subclasses of Array are not
+supported.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## untagged-dispatch #9017 +/- ##
=====================================================
- Coverage 81.32% 81.31% -0.01%
=====================================================
Files 456 456
Lines 64181 64135 -46
=====================================================
- Hits 52193 52151 -42
+ Misses 11988 11984 -4
🚀 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". |
7cb6fa9 to
f42bd93
Compare
|
@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". |
f42bd93 to
36bd191
Compare
|
@codex review |
36bd191 to
b43a9a5
Compare
|
@codex review |
|
Codex Review: Didn't find any major issues. Nice work! 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". |
55c756b to
8d7b161
Compare
8d7b161 to
6f87e5b
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". |
6f87e5b to
c7b1bb5
Compare
|
@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:
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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMkrYaktctdKc4M41WF36t
Signed-off-by: Christoph Knittel <[email protected]>
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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMkrYaktctdKc4M41WF36t
Signed-off-by: Christoph Knittel <[email protected]>
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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMkrYaktctdKc4M41WF36t
Signed-off-by: Christoph Knittel <[email protected]>
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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMkrYaktctdKc4M41WF36t
Signed-off-by: Christoph Knittel <[email protected]>
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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMkrYaktctdKc4M41WF36t
Signed-off-by: Christoph Knittel <[email protected]>
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. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01CMkrYaktctdKc4M41WF36t Signed-off-by: Christoph Knittel <[email protected]>
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. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01CMkrYaktctdKc4M41WF36t Signed-off-by: Christoph Knittel <[email protected]>
`@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. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01CMkrYaktctdKc4M41WF36t Signed-off-by: Christoph Knittel <[email protected]>
Why:
Untagged payloads, annotations on abbreviations and interfaces each
classified types their own way, so the latter two rejected annotations
that only restate what a function type or a record is.
`Variant_layout.payload_kind` gives the declared shape of a type, or the
kind its structure gives: a tuple is an array, a function type a function, a
record an object. `get_block_type`, `check_manifest_annotation` and
`Includecore.compare_runtime_annotations` all use it.
Typing rules changed (an annotation restating a structural kind):
Example:
@typeof("function") type callback = int => int
module M: { @typeof("object") type point } = { type point = {py: int} }
Before (rule): an annotation on an abbreviation agrees with the type's
declared shape; an implementation states the shape it declares or
abbreviates.
After (rule): both compare with the payload kind of rule 4, which also
covers tuples, function types and records.
Diagnostic changed (an abbreviation of a function type):
Example:
@typeof("string") type cb = int => int
Before (diagnostic):
@typeof on a type abbreviation must describe an abstract type, or a type
with the same annotation. ...
After (diagnostic):
This annotation, @typeof("string"), contradicts the abbreviated type,
whose values are recognized by @typeof("function").
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMkrYaktctdKc4M41WF36t
Signed-off-by: Christoph Knittel <[email protected]>
Why:
`@notUndefined` was parsed, checked on declarations and checked on
abbreviations apart from `@instanceOf` and `@typeOf`, so each step had
two versions, and a type could carry it beside the others.
`process_runtime_annotation` returns `Not_undefined` as well, so one parse
feeds `declaration_shape`, the check for represented types and
`Typedecl.check_manifest_annotation`, which now checks all three on an
abbreviation. `Typeopt.abstractness` takes a type and is what
`undefinedness` falls back to.
Diagnostic changed (two of the three annotations):
Example:
@notundefined @typeof("string") type t
Before (diagnostic): none
After (diagnostic):
A type can have only one of @notundefined, @instanceof and @typeof.
@instanceof and @typeof already imply @notundefined.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMkrYaktctdKc4M41WF36t
Signed-off-by: Christoph Knittel <[email protected]>
Why:
`check_invariant` kept a counter per payload kind and rechecked all of
them after every case, with an error variant per kind.
The kinds seen so far are one table, classes keyed by name, and a second
case of a kind is reported where it is added. `AtMostOneOfKind` replaces the
eight `AtMostOne*` errors and keeps their messages.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMkrYaktctdKc4M41WF36t
Signed-off-by: Christoph Knittel <[email protected]>
Why:
The structural classifier did not expand private abbreviations, so
`private r` of a record was unknown while `private string` was a string,
and it called JavaScript object types unknown although their typeof is
always "object". Contradictions with an array type printed it as
@typeof("object").
`payload_kind` expands private abbreviations and classifies `{"x": int}` as
an object. An annotation contradicting an array type names Array.isArray,
and @instanceof over an array type has an error of its own.
Typing rules changed (payload kinds of rule 4):
Example:
type r = {x: int}
type pr = private r
@unboxed type t = A(pr) | B({"y": int}) | S(string)
Before (rule): `pr` and `{"y": int}` are unknown payloads
After (rule): both are object payloads, so only one of them may be a case
Diagnostic changed (an annotation over a tuple):
Example:
@typeof("object") type t = private (int, int)
Before (diagnostic):
This annotation, @typeof("object"), contradicts the abbreviated type,
whose values are recognized by @typeof("object").
After (diagnostic):
This annotation, @typeof("object"), contradicts the abbreviated type,
whose values are recognized by Array.isArray.
Diagnostic added (a class over an array type):
Example:
@instanceof("MyArr") type myArr = private array<int>
Before (diagnostic):
This annotation, @instanceof("MyArr"), contradicts the abbreviated type,
whose values are recognized by @typeof("object").
After (diagnostic):
@instanceof("MyArr") cannot describe the values of an array type: they
are recognized by Array.isArray, and subclasses of Array are not
supported.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMkrYaktctdKc4M41WF36t
Signed-off-by: Christoph Knittel <[email protected]>
Why:
The test added to the bottom of the stack is compiled by this pull
request's dispatch, which checks the function arm against the classes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMkrYaktctdKc4M41WF36t
Signed-off-by: Christoph Knittel <[email protected]>
c7b1bb5 to
828bbc4
Compare
Stacked on #9015. Declaration-side follow-ups from the architecture reviews 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.Lambda.switch. A coercion now accepts@as(1.)for@as(1.0)or@as(1), and a folded match on the coerced constant used to pick the wrong arm (found by Codex).@notUndefinedinIncludecore.compare_runtime_annotations;Includemod.not_undefined_mismatchis gone.check_invariantmoves fromAst_untagged_variantstoVariant_layout, its only caller.The spec (
compiler/ml/UntaggedVariants.md) is updated in rules 2, 4, 5 and 14. Rule 5, now "Declared runtime shapes", covers@notUndefined,@typeOfand@instanceOftogether, with their errors and interface checks; the former rule 13 on@notUndefinedis merged into it, and the rule on@tagand interfaces becomes rule 13.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, and folded matches of coerced constants.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