Repository navigation
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #8950 +/- ##
==========================================
- Coverage 81.22% 81.19% -0.04%
==========================================
Files 457 457
Lines 63853 64027 +174
==========================================
+ Hits 51865 51987 +122
- Misses 11988 12040 +52
🚀 New features to boost your workflow:
|
b8c5606 to
dbe05fb
Compare
|
@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: Didn't find any major issues. Chef's kiss. 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". |
rescript
@rescript/belt
@rescript/darwin-arm64
@rescript/linux-arm64
@rescript/linux-x64
@rescript/runtime
@rescript/win32-x64
commit: |
|
Developer playground preview: https://rescript-lang.github.io/rescript/dev-playground/?version=pr-8950 |
|
This should be reviewed when we have more time. That would be the first PR to begin a review from. |
|
@cknitt, before reviewing this stack in detail: #8952 states the current rules of untagged variants, with tests pinning them, and lists open questions for a runtime-class mechanism. Rebasing #8950 → #8948 → #8949 on top of it would let each PR show its change as a diff to those rules. The main risks I see for the mechanism, each with an example:
#8952 also records five bugs reproduced on master (duplicate Could you share evidence of a detailed assessment of all the risks? For example, sub-agents that each take one risk or one related issue, try to break the mechanism with concrete programs, and report what they found, with the programs and their outputs. #8952's open questions, with their related issues, are a starting list, and the answers could go into it. 🤖 Generated with Claude Code |
|
@cknitt, the bugs in #8952 are filed: #8953 (number literals compared by spelling), #8954 ( |
dbe05fb to
16ee5bb
Compare
|
@cristianoc, thanks, this was the right ask. The stack is rebased on #8952: each PR now updates HowNine sub-agents, one per open question of #8952, plus one for the mid-stack states, the accuracy of the stdlib annotations and the known bugs. Each one wrote programs, compiled them with two compilers, and ran them in Node 22:
The subclass agent also generated programs: every ordered subset of the cases of a four-class tree (Animal > Dog > Puppy, Animal > Cat), with every subset of arms plus a default, with and without a literal and a 1. Unchecked claimsStack bug, fixed. An annotation on an abbreviation overrode what the compiler knows about the abbreviated type. The worst case is a type that can hold @typeOf("string") type s = option<string>
let x: option<option<string>> = Some(id(None))
let y: option<s> = x
switch y { | Some(Some(str)) => "Some(Some(" ++ str ++ "))" | Some(None) => "Some(None)" | None => "None" }
The same happened with
Now: an annotation on an abbreviation must agree with the abbreviated type. It may refine an abstract, non-builtin type without an annotation, or repeat that type's annotation. A subclass of the class annotated there needs a private abbreviation, or a public one that is a strict instance of the superclass type (the phantom hierarchy Still trusted, by design: a claim on an abstract type, like the type of an external, and a chain that is wrong about the runtime. With
2. Subclasses only through private abbreviationsWorks when the chain is visible.
Stack bugs, fixed:
Pre-existing in kind, documented: classes declared as independent abstract types are tested in declaration order, which is #8938 for every binding without a chain. Annotating the 3. Overlaps between class,
|
| Issue | Effect of the stack |
|---|---|
| #8938 (Blob/File order) | fixed by #8948, in both orders, also in a superclass-only switch |
| #8955 (typed arrays) | unchanged: Int8Array.t = Uint8Array.t still typechecks, and I8(u8) matches U8 on both compilers. Making the aliases private would need :> for every TypedArray call. The spec now names this, so it is the moment to decide |
| #8953, #8954, #8956 | unchanged; the spec's programs give the same output on both compilers |
The fixes
New commits at the top of #8950; #8948 and #8949 each gain a spec commit:
- Check runtime annotations against the types they describe: the abbreviation rule, rejected class paths, the function check, and unboxed wrappers.
- Compare runtime annotations and untagged payloads across signatures:
Includecore. - Document runtime annotations in the untagged variants spec: rules 4, 5, 6, 8, 10, 12, 13 and 14, and the decisions.
Each rejection has a super_errors fixture, the interface cases are in super_errors_multi, and the runtime cases (phantom hierarchy, callable class, boxed wrapper) are in untagged_runtime_annotations_test.res.
Still open, listed at the end of the spec:
- a class imported from a module cannot be named. A design would let the annotation name an external value instead of a string;
typeofbefore the instance checks;- Untagged variant puts one typed array in another typed array's case #8955.
🤖 Generated with Claude Code
Semantic Diff2 new commits (7 total) · Full page Annotate the stdlib's runtime classes95019ca · Christoph Knittel with Claude Opus 5.5 · Oct 21, 2026
Types whose values are instances of a global class now say so with JS modified (Blob and File cases) @unboxed
type upload = Blob(Blob.t) | File(File.t)
let kind = upload =>
switch upload {
| Blob(_) => "blob"
| File(_) => "file"
}
-if (upload instanceof Blob) {
+if (upload instanceof Blob && !(upload instanceof File)) {
return "blob";
} else {
return "file";
}Document the stdlib's runtime annotations in the untagged variants spec25a5ddc · Christoph Knittel with Claude Opus 5.5 · Oct 22, 2026
Rule 5 lists the stdlib's annotations, rule 12 unboxes Representation modified (option of a variant with a Blob payload) @unboxed type withBlob = BBlob(Blob.t) | BString(string)
let someWithBlob = (v: withBlob): option<withBlob> => Some(v)
-return Primitive_option.some(v);
+return v; |
Why:
Untagged variants recognize payloads of abstract types only from a
hard-coded list of stdlib type names, which knows no subclasses and is
closed to bindings such as WebAPI's (#7147).
`@instanceOf("Class")` and `@typeOf("…")` on an abstract type or a type
abbreviation state how its values are recognized at run time. The
classifier reads them along the type's expansion chain: the outermost one
decides, and the classes annotated further down a chain of private
abbreviations are its superclasses. A class check excludes the subclasses
that are cases too. Both imply that a value is never undefined. The name
list stays as fallback for types without annotations. Editor completion
offers both, and `@notUndefined`, which was listed with the file-level
`@@` decorators, now completes after a single `@`.
Representation added (untagged variant payload):
Example:
@instanceof("Animal") type animal
@instanceof("Dog") type dog = private animal
@typeof("symbol") type sym
@unboxed
type t = Animal(animal) | Dog(dog) | Sym(sym)
After (representation):
Animal(a) ↦ a, recognized by a instanceof Animal && !(a instanceof Dog)
Dog(d) ↦ d, recognized by d instanceof Dog
Sym(s) ↦ s, recognized by typeof s === "symbol"
Diagnostic added (invalid annotation):
Example:
@typeof("undefined") type t
After (diagnostic):
@typeof expects one of "string", "number", "bigint", "boolean", "symbol", "function" or "object", not "undefined".
Review: Why `@instanceOf`/`@typeOf` rather than one `@runtime(...)`
attribute? They read like the JavaScript checks they produce, take a single
string like `@as` and `@tag`, and sit next to the existing `@notUndefined`.
Review: Why also on public abbreviations? The typed array types are public
aliases of `TypedArray.t<'a>` that users pass to `TypedArray` functions, so
they can only be told apart by the name as written, as the name list does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fc7LEuyEfynjaKCVEBPmTF
Signed-off-by: Christoph Knittel <ck@cca.io>
Why:
An annotation on an abbreviation was trusted whatever the abbreviated
type was. `@typeOf("string") type s = option<string>` made `Some(None)`
of `s` the same value as `None`, and two spellings of one type were
recognized differently. Paths such as `Object`, `BigInt` or
`globalThis.Date` were accepted and misrecognized every value or
duplicated another class.
An annotation on an abbreviation must now refine an abstract type that is
not builtin, repeat the abbreviated type's annotation, or declare a subclass
of the class annotated there. A subclass needs a private abbreviation, or a
public one that is a strict instance of the superclass type (a phantom type
hierarchy), and must not repeat a class of its chain. Paths that name no
class to recognize values by are rejected. Payloads of `@unboxed` wrappers of
annotated types are never undefined either, as with `@notUndefined`, and the
function check excludes class cases as the object check does, so the
two-arm form agrees with the `switch` form.
Diagnostic added (annotation on a type that can be undefined):
Example:
@typeof("string") type s = option<string>
After (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.
Diagnostic added (subclass on a public abbreviation):
Example:
@instanceof("Animal") type animal
@instanceof("Dog") type dog = animal
After (diagnostic):
A subclass must be a private abbreviation: write `= private ...` so that instances of Animal cannot be used as instances of Dog without a coercion.
Diagnostic added (class path):
Example:
@instanceof("BigInt") type t
After (diagnostic):
Values of type bigint are primitives, not instances of BigInt. Use @typeof("bigint") instead.
JS modified (option of an unboxed wrapper):
Example:
@unboxed type boxedAnimal = BoxedAnimal(animal)
let x: option<boxedAnimal> = Some(BoxedAnimal(a))
Before (js): let x = Primitive_option.some(a);
After (js): let x = a;
JS modified (function case beside a class case):
Example:
@unboxed type handler = Function(int => int) | Callable(callable)
Before (js):
if (typeof handler === "function") {
return "function";
}
After (js):
if (typeof handler === "function" && !(handler instanceof UntaggedCallable)) {
return "function";
}
Review: Why allow public subclasses that are strict instances? `Dom.res`
and WebAPI model class hierarchies with phantom types, such as
`type element<'a> = node<_element<'a>>`, whose subclass types unify with the
superclass type only at the phantom argument. `type dog = animal` is the same
type as `animal` and is rejected.
Review: Why reject `globalThis.C` instead of normalizing it? The compiler
already reads a shadowed class through `globalThis`, so the prefix adds
nothing, and one spelling per class keeps the duplicate and subclass checks,
which compare paths, exact.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fc7LEuyEfynjaKCVEBPmTF
Signed-off-by: Christoph Knittel <ck@cca.io>
25a5ddc to
9a2bcf9
Compare
Semantic Diff6 commits · Full page Declare the runtime class or typeof of abstract types9b76592 · Christoph Knittel with Claude Opus 5.5 · Oct 21, 2026
Representation added (untagged variant payload) @instanceOf("Animal") type animal
@instanceOf("Dog") type dog = private animal
@typeOf("symbol") type sym
@unboxed
type t = Animal(animal) | Dog(dog) | Sym(sym)
Diagnostic added (invalid annotation) @typeOf("undefined") type t
Review
Check runtime annotations against the types they describe0d5496d · Christoph Knittel with Claude Opus 5.5 · Oct 22, 2026
An annotation on an abbreviation must now refine an abstract type that is not builtin, repeat the abbreviated type's annotation, or declare a subclass of the class annotated there. A subclass needs a private abbreviation, or a public one that is a strict instance of the superclass type (a phantom type hierarchy), and must not repeat a class of its chain. Paths that name no class to recognize values by are rejected. Payloads of Diagnostic added (annotation on a type that can be undefined) @typeOf("string") type s = option<string>
Diagnostic added (subclass on a public abbreviation) @instanceOf("Animal") type animal
@instanceOf("Dog") type dog = animal
Diagnostic added (class path) @instanceOf("BigInt") type t
JS modified (option of an unboxed wrapper) @unboxed type boxedAnimal = BoxedAnimal(animal)
let x: option<boxedAnimal> = Some(BoxedAnimal(a))
-let x = Primitive_option.some(a);
+let x = a;JS modified (function case beside a class case) @unboxed type handler = Function(int => int) | Callable(callable)
-if (typeof handler === "function") {
+if (typeof handler === "function" && !(handler instanceof UntaggedCallable)) {
return "function";
}Review
Compare runtime annotations and untagged payloads across signaturesda7480f · Christoph Knittel with Claude Opus 5.5 · Oct 22, 2026
When an interface states a class or Diagnostic added (hidden superclass) // Classes.res
@instanceOf("Animal") type animal
@instanceOf("Dog") type dog = private animal
// Classes.resi
@instanceOf("Animal") type animal
@instanceOf("Dog") type dog
Diagnostic added (claim the implementation does not make) // Claim.res
type t = option<string>
// Claim.resi
@typeOf("string") type t
Review
Document runtime annotations in the untagged variants spec21b516c · Christoph Knittel with Claude Opus 5.5 · Oct 22, 2026
Rule 5 now describes Annotate the stdlib's runtime classes359ae8f · Christoph Knittel with Claude Opus 5.5 · Oct 21, 2026
Types whose values are instances of a global class now say so with JS modified (Blob and File cases) @unboxed
type upload = Blob(Blob.t) | File(File.t)
let kind = upload =>
switch upload {
| Blob(_) => "blob"
| File(_) => "file"
}
-if (upload instanceof Blob) {
+if (upload instanceof Blob && !(upload instanceof File)) {
return "blob";
} else {
return "file";
}Document the stdlib's runtime annotations in the untagged variants spec9a2bcf9 · Christoph Knittel with Claude Opus 5.5 · Oct 22, 2026
Rule 5 lists the stdlib's annotations, rule 12 unboxes Representation modified (option of a variant with a Blob payload) @unboxed type withBlob = BBlob(Blob.t) | BString(string)
let someWithBlob = (v: withBlob): option<withBlob> => Some(v)
-return Primitive_option.some(v);
+return v; |
|
@codex review |
|
Codex Review: Didn't find any major issues. You're on a roll. 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:
Interface inclusion ignored `@instanceOf`/`@typeOf`. An interface could
claim a class its implementation does not have, or hide a private
superclass manifest; the implementation and its clients then compiled
different checks for the same values, and #8938 came back in the
clients. Functor arguments, `with type` constraints and module type
ascriptions had the same hole.
When an interface states a class or `typeof` for a type, through its own
annotation or the type it abbreviates, the implementation must state the same
one, superclasses included. An interface that states none hides it. The
payload kind of every untagged case is compared too, so a variant declared
in both is recognized the same way on both sides.
Diagnostic added (hidden superclass):
Example:
// Classes.res
@instanceof("Animal") type animal
@instanceof("Dog") type dog = private animal
// Classes.resi
@instanceof("Animal") type animal
@instanceof("Dog") type dog
After (diagnostic):
Type declarations do not match:
type dog = animal
is not included in
type dog
Their runtime representations differ:
the first declaration states instances of Dog, a subclass of Animal,
the second declaration states instances of Dog.
Diagnostic added (claim the implementation does not make):
Example:
// Claim.res
type t = option<string>
// Claim.resi
@typeof("string") type t
After (diagnostic):
Their runtime representations differ:
the first declaration states no runtime class or typeof,
the second declaration states values whose typeof is "string".
Review: Why compare payload kinds when the payload types are already
compared? Equal payload types are classified alike only when both sides see
the same annotations; an interface that hides one (stating none, which is
allowed) gives its variant another layout.
Review: Why not compare `@notUndefined` too? It has the same hole on master
(#8964), fixed separately in #8966; the implied never-undefined of the new
annotations is covered here, because the classification includes it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fc7LEuyEfynjaKCVEBPmTF
Signed-off-by: Christoph Knittel <ck@cca.io>
Why:
The spec states the rules the compiler implements, and its open
questions asked what a mechanism for declaring classes and kinds on
types has to decide.
Rule 5 now describes `@instanceOf` and `@typeOf`: where they can appear,
how they are read along a chain of abbreviations, which abbreviations and
class paths are rejected, and the name list that still covers types without
annotations. Rules 4, 6, 8, 10, 12, 13 and 14 gain what the annotations
change: declared kinds, one case per class and per symbol, subclass and
function exclusions, missing classes, implied never-undefined, and inclusion
checks. The open questions become the decisions taken, with what is still
open.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fc7LEuyEfynjaKCVEBPmTF
Signed-off-by: Christoph Knittel <ck@cca.io>
Why:
Untagged variants recognized stdlib classes only by type name, with no
relation between them, so a `Blob` case declared before a `File` case
also caught files (#8938).
Types whose values are instances of a global class now say so with
`@instanceOf` instead of `@notUndefined`, `Symbol.t` with
`@typeOf("symbol")`, and `File.t` is a private abbreviation of `Blob.t`.
`JsError.t`, `Symbol.t` and the `Intl` classes can be untagged variant
cases too. Fixes #8938.
JS modified (Blob and File cases):
Example:
@unboxed
type upload = Blob(Blob.t) | File(File.t)
let kind = upload =>
switch upload {
| Blob(_) => "blob"
| File(_) => "file"
}
Before (js):
if (upload instanceof Blob) {
return "blob";
} else {
return "file";
}
After (js):
if (upload instanceof Blob && !(upload instanceof File)) {
return "blob";
} else {
return "file";
}
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fc7LEuyEfynjaKCVEBPmTF
Signed-off-by: Christoph Knittel <ck@cca.io>
Why:
With the stdlib annotated, `Blob.t` and `File.t` are never undefined
and #8938 is fixed, which the spec and its test still stated the other
way round.
Rule 5 lists the stdlib's annotations, rule 12 unboxes `option` of a
variant with a `Blob.t` payload, and the known bug of overlapping classes is
gone. The typed-array bug stays, filed as #8955.
JS modified (option of a variant with a Blob payload):
Example:
@unboxed type withBlob = BBlob(Blob.t) | BString(string)
let someWithBlob = (v: withBlob): option<withBlob> => Some(v)
Before (js): return Primitive_option.some(v);
After (js): return v;
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fc7LEuyEfynjaKCVEBPmTF
Signed-off-by: Christoph Knittel <ck@cca.io>
9a2bcf9 to
63ef81c
Compare
Semantic Diff6 commits · Full page Declare the runtime class or typeof of abstract types9b76592 · Christoph Knittel with Claude Opus 5.5 · Oct 21, 2026
Representation added (untagged variant payload) @instanceOf("Animal") type animal
@instanceOf("Dog") type dog = private animal
@typeOf("symbol") type sym
@unboxed
type t = Animal(animal) | Dog(dog) | Sym(sym)
Diagnostic added (invalid annotation) @typeOf("undefined") type t
Review
Check runtime annotations against the types they describe0d5496d · Christoph Knittel with Claude Opus 5.5 · Oct 22, 2026
An annotation on an abbreviation must now refine an abstract type that is not builtin, repeat the abbreviated type's annotation, or declare a subclass of the class annotated there. A subclass needs a private abbreviation, or a public one that is a strict instance of the superclass type (a phantom type hierarchy), and must not repeat a class of its chain. Paths that name no class to recognize values by are rejected. Payloads of Diagnostic added (annotation on a type that can be undefined) @typeOf("string") type s = option<string>
Diagnostic added (subclass on a public abbreviation) @instanceOf("Animal") type animal
@instanceOf("Dog") type dog = animal
Diagnostic added (class path) @instanceOf("BigInt") type t
JS modified (option of an unboxed wrapper) @unboxed type boxedAnimal = BoxedAnimal(animal)
let x: option<boxedAnimal> = Some(BoxedAnimal(a))
-let x = Primitive_option.some(a);
+let x = a;JS modified (function case beside a class case) @unboxed type handler = Function(int => int) | Callable(callable)
-if (typeof handler === "function") {
+if (typeof handler === "function" && !(handler instanceof UntaggedCallable)) {
return "function";
}Review
Compare runtime annotations and untagged payloads across signatures11185e9 · Christoph Knittel with Claude Opus 5.5 · Oct 22, 2026
When an interface states a class or Diagnostic added (hidden superclass) // Classes.res
@instanceOf("Animal") type animal
@instanceOf("Dog") type dog = private animal
// Classes.resi
@instanceOf("Animal") type animal
@instanceOf("Dog") type dog
Diagnostic added (claim the implementation does not make) // Claim.res
type t = option<string>
// Claim.resi
@typeOf("string") type t
Review
Document runtime annotations in the untagged variants specc8c58b4 · Christoph Knittel with Claude Opus 5.5 · Oct 22, 2026
Rule 5 now describes Annotate the stdlib's runtime classes09ec86b · Christoph Knittel with Claude Opus 5.5 · Oct 21, 2026
Types whose values are instances of a global class now say so with JS modified (Blob and File cases) @unboxed
type upload = Blob(Blob.t) | File(File.t)
let kind = upload =>
switch upload {
| Blob(_) => "blob"
| File(_) => "file"
}
-if (upload instanceof Blob) {
+if (upload instanceof Blob && !(upload instanceof File)) {
return "blob";
} else {
return "file";
}Document the stdlib's runtime annotations in the untagged variants spec63ef81c · Christoph Knittel with Claude Opus 5.5 · Oct 22, 2026
Rule 5 lists the stdlib's annotations, rule 12 unboxes JS modified (option of a variant with a Blob payload) @unboxed type withBlob = BBlob(Blob.t) | BString(string)
let someWithBlob = (v: withBlob): option<withBlob> => Some(v)
-return Primitive_option.some(v);
+return v; |
Fixes #8938. Includes the stdlib annotations of the former #8948, so no state exists in which annotated and unannotated stdlib classes are mixed. Based on #8952, whose
compiler/ml/UntaggedVariants.mdeach commit updates.Untagged variants recognize payloads of abstract types only through a hard-coded list of stdlib type names (
Stdlib_Date.t,Stdlib.Map.t, …). The list is closed to bindings such as WebAPI's (#7147:CanvasGradient,CanvasPattern, …) and knows no subclasses, so aBlobcase declared before aFilecase also catches files (#8938).This PR lets a type declaration state how its values are recognized at run time:
The mechanism
TypedArray.t<'a>that can only be told apart by the name as written.type element<'a> = node<_element<'a>>), and no class may appear twice in a chain. So@typeOf("string") type s = option<string>or@instanceOf("Dog") type dog = animalare errors.switchmatches change which case a value lands in. The object and function checks exclude every class case. A private abbreviation also gives:>upcasts.undefined: both annotations imply that a value is neverundefined, as@notUndefineddoes, sooptionstays unboxed, also for@unboxedwrappers of such a type.@notUndefinedstays for types whose values have no single class ortypeof, such asJsx.element.typeoffor a type, the implementation must state the same one, superclasses included; an interface that states none hides it. The payload kind of every untagged case is compared too. This covers.resifiles, functor arguments,with typeand module type ascriptions.globalThiswhen a binding of the same name is in scope (Read JS globals through globalThis wherever a same-named binding is in scope #8785). Dotted paths are emitted as property accesses on the first segment, so that also coversFoo.Bar.typeof, both annotations on one type, or an annotation on a record or variant are errors. So are paths that name no class to recognize values by:globalThis.C,Object,Function,Array, the primitive wrappers (String,Number,Boolean,BigInt,Symbol) and non-constructors (Math,JSON,Intl, …). A variant can have at most one case per class and one symbol case.@notUndefinedwas listed with the file-level@@decorators; it now completes after a single@.The
.cmiand.cmtmagic numbers are bumped, since variant layouts are stored in.cmifiles.The stdlib
The stdlib's class-backed types now declare their class instead of only
@notUndefined:ArrayBuffer,DataView,Date,RegExp,Map,Set,WeakMap,WeakSet,JsError(Error), and theIntlclasses (Intl.Collator,Intl.DateTimeFormat,Intl.ListFormat,Intl.Locale,Intl.NumberFormat,Intl.PluralRules,Intl.RelativeTimeFormat,Intl.Segmenter) get@instanceOf.Symbol.tgets@typeOf("symbol").@instanceOf.FileandBlob:Blob.tgets@instanceOf("Blob"), andFile.tbecomes@instanceOf("File") type t = private Blob.t. A file is now known to be a blob, so aBlobcase excludes files, and(file :> Blob.t)upcasts.option<Blob.t>andoption<File.t>are now unboxed.JsError.t,Symbol.tand theIntlclasses can now be untagged variant cases, which they couldn't before. These keep@notUndefined, because their values have no single class ortypeof:Jsx.elementandJsx.component,Iterator/AsyncIterator,TypedArray.t<'a>(%TypedArray%is not a global),Intl.Segments,Intl.NumberFormat.GroupingandLazy.Generated code changes only where
BlobandFilecases meet: inUntaggedVariants.mjs, theBlobcase now excludes files.Design
Syntax considered: a grouped
@runtime(instanceof("Date"))(one name, but a call inside an attribute payload is unusual in ReScript), a string mini-language (@runtime("instanceof Date")), and per-constructor predicates (| @check("$0 instanceof X") Gradient(g), #7147), which repeat raw JS at every use and know nothing about inheritance orundefined.@instanceOf/@typeOfread like the checks they produce and take a single string, like@asand@tag.The risk assessment, with the programs that found the holes fixed here, is in this comment.
Not covered: telling records apart by their properties (#7988), classes imported from modules rather than globals, and the typed arrays sharing one type (#8955).
Tests
tests/tests/src/untagged_runtime_annotations_test.res: real JS classes, a subclass declared as a private abbreviation, both declaration orders, a superclass case matched alone, types reached through a module alias, upcasts,typeOfsymbol and string, a dotted class path, a phantom type hierarchy, a callable class beside a function case, and option unboxing, also of an@unboxedwrapper.untagged_runtime_annotations_shadow_test.res: class checks when local modules shadow the global and the first segment of a dotted path.untagged_file_blob_test.res:Blobdeclared beforeFile, aBlobcase matched alone, and the upcast.instance_of_invalid_class,instance_of_keyword,instance_of_global_this,instance_of_object,instance_of_array,instance_of_primitive_wrapper,instance_of_not_a_class,instance_of_public_subclass,instance_of_class_twice_in_chain,type_of_invalid,runtime_annotation_conflict,runtime_annotation_on_record,runtime_annotation_on_option,runtime_annotation_on_type_variable,runtime_annotation_contradicts_manifest,untagged_same_class_twice,untagged_two_symbols.Cross_runtime_annotation_hidden_superclass,Cross_runtime_annotation_claimed_by_interface,Cross_runtime_annotation_functor_argument.untagged_variants_spec_test.res:optionof a variant with aBlobpayload is unboxed.@instanceOf,@typeOfand@notUndefined.🤖 Generated with Claude Code
https://claude.ai/code/session_01Fc7LEuyEfynjaKCVEBPmTF