Repository navigation
Check @notUndefined of an interface against the implementation - #8966
Conversation
|
@codex review |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## instanceof-typeof-annotations #8966 +/- ##
==============================================================
Coverage 81.31% 81.31%
==============================================================
Files 456 456
Lines 64130 64145 +15
==============================================================
+ Hits 52148 52161 +13
- Misses 11982 11984 +2
🚀 New features to boost your workflow:
|
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. |
5f2e4de to
df024f4
Compare
rescript
@rescript/belt
@rescript/darwin-arm64
@rescript/linux-arm64
@rescript/linux-x64
@rescript/runtime
@rescript/win32-x64
commit: |
df024f4 to
b19d240
Compare
|
@codex review |
b19d240 to
f8ee511
Compare
Changelog🐛 Bug fix
Semantic Diff3 commits · Full page ✨ Simplify the payload kinds and untagged checks31a67fe · Christoph Knittel + Claude · Oct 24, 2026, 09:16 UTC · new
Diagnostic modified (more than one argument) @unboxed
type t = A(int, int) | B(string)
-UntaggedConstructorMoreThanOneArg.res:2:1-32
+UntaggedConstructorMoreThanOneArg.res:2:10-20Check
|
f8ee511 to
a1622ab
Compare
25fc167 to
ecc1ff2
Compare
Why:
Interface inclusion ignored `@notUndefined`. An interface could declare
`@notUndefined type t` over `type t = option<int>`, and clients then
unboxed `option<t>`, so `Some(M.none)` was `None` (#8964).
When the second declaration of an inclusion is `@notUndefined`, the first
must be a type that cannot hold `undefined`, by the rule `option` unboxing
uses, or an abstract type, which has no representation to contradict it.
This covers `.resi` files, functor arguments and `with type` constraints.
The check runs in `Includemod`, since `Typeopt` depends on `Includecore`
through `Typedecl`.
Diagnostic added:
Example:
// Hidden.resi
@notundefined type t
// Hidden.res
type t = option<int>
After (diagnostic):
Type declarations do not match:
type t = option<int>
is not included in
type t
Their runtime representations differ:
the second declaration is @notundefined,
but the first declaration can hold undefined.
Review: Why accept an abstract implementation without the attribute? The
stdlib's `Lazy.res` declares `type t<+'a>` under `@notUndefined` in its
interface, and so may bindings; such a type has no representation that could
hold `undefined`, so requiring the attribute twice would only break them.
Fixes: #8964
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 check rejected bindings that are fine: an interface's
`@notUndefined type t` over `type t = Dom.element`, an abbreviation of an
abstract type, or over a JavaScript object type, which is never
undefined.
An implementation whose representation the compiler does not see has none
that contradicts the interface: an abstract type, also reached through an
abbreviation, or a JavaScript object type. A functor parameter's or a locally
abstract type stands for any type and does not count as abstract.
Diagnostic removed (an abbreviation of an abstract type):
Example:
// A.resi
@notundefined type t
// A.res
type t = Dom.element
Before (diagnostic):
Their runtime representations differ:
the second declaration is @notundefined,
but the first declaration can hold undefined.
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>
ecc1ff2 to
743b80b
Compare
…#8950) * Declare the runtime class or typeof of abstract types 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> * Check runtime annotations against the types they describe 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> * Compare runtime annotations and untagged payloads across signatures 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> * Document runtime annotations in the untagged variants spec 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> * Annotate the stdlib's runtime classes 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> * Document the stdlib's runtime annotations in the untagged variants spec 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> * Recognize untagged payloads only by annotations and builtin types Why: With the stdlib annotated, the classifier's list of stdlib type names only matched types that happen to be printed like a stdlib type, and its name-based dict check took any type named `dict` for an object. The list, matched before and after expansion, is gone, and so is the dict check: a payload is recognized by its `@instanceOf` or `@typeOf`, or by the builtin type it expands to. Generated code is unchanged for the stdlib's types. Diagnostic added (a local type named dict): Example: type dict<'a> @unboxed type t = D(dict<int>) | S(string) Before (js): if (typeof v === "object") { return "D"; } else { return "S"; } After (diagnostic): This untagged variant definition is invalid: Case D has a payload that is not of one of the recognized shapes (object, array, etc). Then it must be the only case with payloads. 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> * Test that payloads are not recognized by type name Why: The classifier used to recognize payloads by the printed path of their type; nothing pinned that it no longer does. An unannotated local `Stdlib_Date.t`, which the removed list would have taken for a date, is an unknown payload, and aliases of builtin types are recognized through their expansion. 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> * Drop the name list from the untagged variants spec Why: Payloads are now recognized only by annotations and builtin types, so the spec's list of stdlib type names and the fallback to it are gone. Rule 4 lists `promise<_>` as the one builtin class and says that a type is never recognized by its name; rule 5 keeps the stdlib's annotations. 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> * Store the resolved runtime shape on type declarations Why: Every reader re-derived what `@instanceOf`, `@typeOf` and `@notUndefined` state by walking a type's expansion chain in the reader's environment. Interface inclusion therefore evaluated an interface's `@instanceOf("Dog") type dog = private animal` against the implementation's annotated `animal`, while clients saw an unannotated `animal`: a superclass hidden that way was lost in clients. A declaration now carries `type_shape`: `Unstated`, `Never_undefined`, or `Recognized_as` a class with its superclasses or a `typeof` kind, resolved when the type is declared, in the environment it is declared in. The classifier, `option` unboxing and inclusion read it, walking only to the first declaration that states a shape; no attribute is parsed after declaration, and the deduplication of annotations repeated through module aliases is gone. Diagnostic added (a superclass hidden by its interface): Example: // Classes.res @instanceof("Animal") type animal @instanceof("Dog") type dog = private animal // Classes.resi type animal @instanceof("Dog") type dog = private animal After (diagnostic): Their runtime representations differ: the first declaration states instances of Dog, a subclass of Animal, the second declaration states instances of Dog. Review: Why a field rather than a normalized attribute? The facts are typed data, not text, and a field cannot be printed or forwarded by tools that handle attributes. 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> # Conflicts: # compiler/ext/config.ml * Close the gaps the re-review of runtime annotations found Why: An annotation could refine a functor parameter's or a locally abstract type, which stands for any type: `@instanceOf("Date") type d = X.t` in a functor applied to `option<int>` made `Some(None)` collapse to `None`. An abbreviation through an alias passed as a phantom subclass, an interface hiding a payload's class was rejected although master accepts it, and two `@instanceOf` attributes were reported as a conflict between the two kinds. Annotations on the type of a functor parameter, of a recursive module being defined, or on a locally abstract type are rejected. The phantom-hierarchy exception reads the abbreviation's arguments after expansion. A class can be declared over a `@typeOf("object")` type. An interface case with an unknown payload agrees with any payload of the implementation, since it is the only payload case and is never checked. Diagnostic added (a type that stands for another one): Example: module Make = (X: {type t}) => { @instanceof("Date") type date = X.t } After (diagnostic): @instanceof cannot describe the type of a functor parameter, of a recursive module being defined, or a locally abstract type: such a type stands for another one, whose values can be anything. Diagnostic added (public subclass through an alias): Example: @instanceof("Animal") type animal type phantom<'a> = animal @instanceof("Dog") type dog = phantom<int> 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 removed (an interface hiding a payload's class): Example: // A.resi type t @unboxed type v = | @as("n") N | P(t) // A.res type t = Date.t @unboxed type v = | @as("n") N | P(t) Before (diagnostic): The internal representations for case P are not equal 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> * Simplify the payload kinds and untagged checks Why: The architecture review found the same knowledge in several places: three tables of `typeof` names, an `IntType`/`FloatType` distinction that no consumer made, two functions for `@unboxed`, two checks of the arity of untagged constructors, and the opaque-abstract-type test written inline. `Variant_runtime` lists the kinds `typeof` recognizes once, and the names, the reverse lookup and the generated `typeof` strings come from it. `int` and `float` payloads have one kind, `NumberType`. `has_untagged` replaces its twin, the layout alone reports a constructor with more than one argument, now at the constructor, and `Variant_layout.abstractness` says whether an abstract type can be refined. `resolve_shapes` stops when a round changes nothing, `type_cannot_contain_undefined` no longer re-reads a shape it has read, and the doc of `type_shape` and rule 14 say what the shape holds and how classes compare. Diagnostic modified (more than one argument): Example: @unboxed type t = A(int, int) | B(string) Before (diagnostic): UntaggedConstructorMoreThanOneArg.res:2:1-32 After (diagnostic): UntaggedConstructorMoreThanOneArg.res:2:10-20 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> * Check @notundefined of an interface against the implementation Why: Interface inclusion ignored `@notUndefined`. An interface could declare `@notUndefined type t` over `type t = option<int>`, and clients then unboxed `option<t>`, so `Some(M.none)` was `None` (#8964). When the second declaration of an inclusion is `@notUndefined`, the first must be a type that cannot hold `undefined`, by the rule `option` unboxing uses, or an abstract type, which has no representation to contradict it. This covers `.resi` files, functor arguments and `with type` constraints. The check runs in `Includemod`, since `Typeopt` depends on `Includecore` through `Typedecl`. Diagnostic added: Example: // Hidden.resi @notundefined type t // Hidden.res type t = option<int> After (diagnostic): Type declarations do not match: type t = option<int> is not included in type t Their runtime representations differ: the second declaration is @notundefined, but the first declaration can hold undefined. Review: Why accept an abstract implementation without the attribute? The stdlib's `Lazy.res` declares `type t<+'a>` under `@notUndefined` in its interface, and so may bindings; such a type has no representation that could hold `undefined`, so requiring the attribute twice would only break them. Fixes: #8964 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> * Accept @notundefined over implementations the compiler cannot see into Why: The check rejected bindings that are fine: an interface's `@notUndefined type t` over `type t = Dom.element`, an abbreviation of an abstract type, or over a JavaScript object type, which is never undefined. An implementation whose representation the compiler does not see has none that contradicts the interface: an abstract type, also reached through an abbreviation, or a JavaScript object type. A functor parameter's or a locally abstract type stands for any type and does not count as abstract. Diagnostic removed (an abbreviation of an abstract type): Example: // A.resi @notundefined type t // A.res type t = Dom.element Before (diagnostic): Their runtime representations differ: the second declaration is @notundefined, but the first declaration can hold undefined. 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> * Require a private abbreviation for every subclass Why: A public abbreviation could declare a subclass when its arguments were not all type variables, meant for phantom type hierarchies. The test was syntactic, so `@instanceOf("Dog") type dog = animal<int>` passed, and every `animal<int>` was then a dog without a coercion. A subclass is always a private abbreviation, phantom hierarchies included; upcasts use `:>`. Diagnostic added (public subclass of an instantiated class): Example: @instanceof("Animal") type animal<'a> @instanceof("Dog") type dog = animal<int> Before (diagnostic): none 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. 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> * Keep callable instances out of a function arm Why: A class can extend Function, so its instances have typeof "function". The instance cases without an arm were only checked before the typeof switch when it had an object case, so a callable instance falling to the default was taken by a function arm. The instance cases without an arm are now checked whenever the typeof switch has an object or a function case. Generated code changed (a callable class in the default): Example: @instanceof("Callable") type callable @unboxed type t = C(callable) | D(animal) | F(int => int) | S(string) | N(float) | B(bool) let f = v => switch v { | F(_) => "function" | S(_) => "string" | _ => "other" } Before (JS): switch (typeof v) { case "function" : return "function"; case "string" : return "string"; default: return "other"; } After (JS): if (v instanceof Animal || v instanceof Callable) { return "other"; } switch (typeof v) { case "function" : return "function"; case "string" : return "string"; default: return "other"; } 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> --------- Signed-off-by: Christoph Knittel <ck@cca.io> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Fixes #8964. Stacked on #8950, whose stored type shapes it reads.
Interface inclusion ignored
@notUndefined. An interface could declare@notUndefined type tovertype t = option<int>, and clients then unboxedoption<t>, soSome(M.none)wasNone.Now, when an interface (or a functor parameter, or the original of a
with typeconstraint) declares a type@notUndefined, its implementation must be a type that cannot holdundefined. The rule is the oneoptionunboxing uses: a record, a builtin type other thanoption/unit, an annotated or@notUndefinedtype, and so on. An implementation whose representation the compiler does not see is accepted too, because it has none to contradict the claim: an abstract type, also through an abbreviation such astype t = Dom.element, or a JavaScript object type. The stdlib'sLazy.res(type t<+'a>under@notUndefinedinLazy.resi) relies on that, and so do bindings. The type of a functor parameter or a locally abstract type stands for any type and does not count as abstract.The check sits in
Includemod.type_declarations, which every inclusion goes through, becauseTypeoptdepends onIncludecorethroughTypedecl.UntaggedVariants.mdrule 13 now states it.New tests:
super_errors_multi/Cross_not_undefined_option_implementation(the issue's repro),super_errors/not_undefined_functor_argumentandnot_undefined_functor_parameter, andtests/tests/src/not_undefined_interface_test.res(an abbreviation of an abstract type and an object type, accepted).🤖 Generated with Claude Code
https://claude.ai/code/session_01Fc7LEuyEfynjaKCVEBPmTF