Skip to content

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

Draft
cknitt wants to merge 1 commit into
untagged-no-hardcoded-classesfrom
not-undefined-inclusion
Draft

cknitt wants to merge 1 commit into
untagged-no-hardcoded-classesfrom
not-undefined-inclusion

Conversation

@cknitt

@cknitt cknitt commented Oct 22, 2026

Copy link
Copy Markdown
Member

Fixes #8964. Stacked on #8949.

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 abstract implementation is accepted too, because it has no representation to contradict the claim. The stdlib's Lazy.res (type t<+'a> under @notUndefined in Lazy.resi) relies on that, and so may bindings.

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) and super_errors/not_undefined_functor_argument.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Fc7LEuyEfynjaKCVEBPmTF

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

Copy link
Copy Markdown

Semantic Diff

1 commit · Full page

Check @notUndefined of an interface against the implementation

5f2e4de · Christoph Knittel with Claude Opus 5.5 · Oct 22, 2026

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.

@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 90.90909% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 81.22%. Comparing base (7d2fd82) to head (df024f4).

Files with missing lines Patch % Lines
compiler/ml/includemod.ml 90.00% 1 Missing ⚠️
Additional details and impacted files
@@                      Coverage Diff                       @@
##           untagged-no-hardcoded-classes    #8966   +/-   ##
==============================================================
  Coverage                          81.22%   81.22%           
==============================================================
  Files                                457      457           
  Lines                              63987    63997   +10     
==============================================================
+ Hits                               51973    51982    +9     
- Misses                             12014    12015    +1     
Files with missing lines Coverage Δ
compiler/ml/includecore.ml 78.14% <100.00%> (+0.08%) ⬆️
compiler/ml/includemod.ml 76.25% <90.00%> (+0.36%) ⬆️
🚀 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

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-22T19:21:22.853237Z 5f2e4de 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.

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>
@github-actions

Copy link
Copy Markdown

Semantic Diff

1 commit · Full page

Check @notUndefined of an interface against the implementation

df024f4 · Christoph Knittel with Claude Opus 5.5 · Oct 22, 2026

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.

@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: df024f4

This branch has not been deployed

No deployments
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