Skip to content

Check @notUndefined of an interface against the implementation - #8966

Merged
cknitt merged 2 commits into
instanceof-typeof-annotationsfrom
not-undefined-inclusion
Oct 24, 2026
Merged

cknitt merged 2 commits into
instanceof-typeof-annotationsfrom
not-undefined-inclusion

Conversation

@cknitt

@cknitt cknitt commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

Fixes #8964. Stacked on #8950, whose stored type shapes it reads.

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.

Now, when an interface (or a functor parameter, or the original of a with type constraint) declares a type @notUndefined, its implementation must be a type that cannot hold undefined. The rule is the one option unboxing uses: a record, a builtin type other than option/unit, an annotated or @notUndefined type, 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 as type t = Dom.element, or a JavaScript object type. The stdlib's Lazy.res (type t<+'a> under @notUndefined in Lazy.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.

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.

The check sits in Includemod.type_declarations, which every inclusion goes through, because Typeopt depends on Includecore through Typedecl. UntaggedVariants.md rule 13 now states it.

New tests: super_errors_multi/Cross_not_undefined_option_implementation (the issue's repro), super_errors/not_undefined_functor_argument and not_undefined_functor_parameter, and tests/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

@cknitt
cknitt added this pull request to stack #8968 October 22, 2026 19:15
@cknitt

cknitt commented Oct 22, 2026

Copy link
Copy Markdown
Member Author

@codex review

@codecov

codecov Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.50000% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.31%. Comparing base (31a67fe) to head (743b80b).
⚠️ Report is 2 commits behind head on instanceof-typeof-annotations.

Files with missing lines Patch % Lines
compiler/ml/includemod.ml 86.66% 2 Missing ⚠️
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     
Files with missing lines Coverage Δ
compiler/ml/includecore.ml 79.06% <100.00%> (+0.07%) ⬆️
compiler/ml/includemod.ml 76.30% <86.66%> (+0.42%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review 🔄 Running since 2026-10-23T11:48:15.038351Z b19d240 Manual request
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@pkg-pr-new

pkg-pr-new Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

rescript

npm i https://pkg.pr.new/rescript@8966

@rescript/belt

npm i https://pkg.pr.new/@rescript/belt@8966

@rescript/darwin-arm64

npm i https://pkg.pr.new/@rescript/darwin-arm64@8966

@rescript/linux-arm64

npm i https://pkg.pr.new/@rescript/linux-arm64@8966

@rescript/linux-x64

npm i https://pkg.pr.new/@rescript/linux-x64@8966

@rescript/runtime

npm i https://pkg.pr.new/@rescript/runtime@8966

@rescript/win32-x64

npm i https://pkg.pr.new/@rescript/win32-x64@8966

commit: ecc1ff2

Base automatically changed from untagged-no-hardcoded-classes to instanceof-typeof-annotations October 23, 2026 11:16
@cknitt
cknitt force-pushed the not-undefined-inclusion branch from df024f4 to b19d240 Compare October 23, 2026 11:47
@cknitt

cknitt commented Oct 23, 2026

Copy link
Copy Markdown
Member Author

@codex review

@rescript-lang rescript-lang deleted a comment from github-actions Bot Oct 23, 2026
@rescript-lang rescript-lang deleted a comment from github-actions Bot Oct 23, 2026
@rescript-lang rescript-lang deleted a comment from github-actions Bot Oct 23, 2026
@cknitt
cknitt force-pushed the not-undefined-inclusion branch from b19d240 to f8ee511 Compare October 23, 2026 17:28
@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Changelog

🐛 Bug fix

  • Reject an implementation that can hold undefined for a type its interface declares @notUndefined, which made Some(None) of the type None in clients.

Semantic Diff

3 commits · Full page

✨ Simplify the payload kinds and untagged checks

31a67fe · Christoph Knittel + Claude · Oct 24, 2026, 09:16 UTC · new

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)

@unboxed
type t = A(int, int) | B(string)

diagnostic

-UntaggedConstructorMoreThanOneArg.res:2:1-32
+UntaggedConstructorMoreThanOneArg.res:2:10-20
Check @notUndefined of an interface against the implementation

b767069 · cknitt + Claude · Oct 22, 2026, 19:04 UTC

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

// Hidden.resi
@notUndefined type t
// Hidden.res
type t = option<int>

diagnostic · After

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.
Accept @notUndefined over implementations the compiler cannot see into

743b80b · cknitt + Claude · Oct 23, 2026, 11:34 UTC

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)

// A.resi
@notUndefined type t
// A.res
type t = Dom.element

diagnostic · Before

Their runtime representations differ:
the second declaration is @notUndefined,
but the first declaration can hold undefined.

@cknitt
cknitt force-pushed the not-undefined-inclusion branch from f8ee511 to a1622ab Compare October 23, 2026 17:31
@cknitt
cknitt force-pushed the not-undefined-inclusion branch 2 times, most recently from 25fc167 to ecc1ff2 Compare October 24, 2026 08:56
cknitt and others added 2 commits October 24, 2026 09:16
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>
@cknitt
cknitt force-pushed the not-undefined-inclusion branch from ecc1ff2 to 743b80b Compare October 24, 2026 09:18
@cknitt
cknitt merged commit 743b80b into master Oct 24, 2026
14 of 17 checks passed
@cknitt
cknitt deleted the not-undefined-inclusion branch October 24, 2026 09:24
@cknitt

cknitt commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

Folded into #8950: the @notUndefined inclusion check reads the stored type_shape of #8950 and lands with it. GitHub shows this PR as merged because #8950's branch now contains its commits; nothing reached master. #8964 is fixed by #8950.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

@notUndefined in an interface is not checked against the implementation

1 participant