Skip to content

Compare constructor representations in interfaces and coercions alike - #9039

Closed
cknitt wants to merge 1 commit into
masterfrom
fix-per-constructor-unboxed
Closed

cknitt wants to merge 1 commit into
masterfrom
fix-per-constructor-unboxed

Conversation

@cknitt

@cknitt cknitt commented Oct 25, 2026 •

Copy link
Copy Markdown
Member

Fixes #9038.

An interface or a coercion accepted a tagged constructor for an @unboxed one. Interface inclusion compared only the tags of tagged constructors, and the coercion check only the declared @as, so a value built as {TAG: "A", _0: 3} was read by clients as another case.

  • Variant_runtime.same_representation compares two constructors at run time: both constant with the same literal, both tagged with the same tag, or both untagged. Includecore.compare_variants and the coercion check in Ctype both use it. Variant_coercion.variant_representation_matches is gone.
  • The stricter check showed that the predefined unknown, an @unboxed variant, had a tagged layout. It now has the untagged layout that a declaration of it gets, so @unboxed type rec t = unknown = Unknown(_): t still agrees with it.
  • Type declarations in messages now print the @unboxed of an untagged constructor when the type isn't @unboxed as a whole. Before, both sides of the mismatch printed as type t = A(int) | B({x: int}). The mismatch now says "case A is @unboxed only in the second declaration".
  • The coercion error about different @unboxed configurations named the wrong variant as unboxed. The existing snapshot variant_to_variant_coercion_unboxed showed this, and it is corrected.

New tests: fixtures untagged_constructor_interface_mismatch.res and untagged_constructor_coercion_mismatch.res.

Found by a review of the untagged variant work (#8950).

🤖 Generated with Claude Code

https://claude.ai/code/session_01CMkrYaktctdKc4M41WF36t

@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Changelog

🐛 Bug fix

  • Reject interfaces and coercions in which an @unboxed constructor meets a tagged one, and name the unboxed variant correctly in the coercion error.

Semantic Diff

1 commit · Full page

✏️ Compare constructor representations in interfaces and coercions alike

594ff28 · cknitt + Claude · Oct 25, 2026, 06:54 UTC · edited

Why

An interface or a coercion accepted a tagged constructor for an @unboxed one: inclusion compared only the tags of tagged constructors, and the coercion check only the declared @as (#9038). The coercion error also said that the wrong variant was unboxed.

Variant_runtime.same_representation compares two constructors: constant with the same literal, tagged with the same tag, or both untagged. Interface inclusion and the coercion check use it. Type declarations in messages print the @unboxed of an untagged constructor of a type that is not @unboxed itself, and an interface mismatch names the constructor that is @unboxed in only one declaration. The predefined unknown, an @unboxed variant, now has the untagged layout a declaration of it gets, so @unboxed type rec t = unknown = Unknown(_): t still agrees with it.

Diagnostic added (interface with an @unboxed constructor)

module M: {
  type t = | @unboxed A(int) | B({x: int})
} = {
  type t = | A(int) | B({x: int})
}

diagnostic

-none; clients read {TAG: "A", _0: 3} as B
+Type declarations do not match:
+  type t = A(int) | B({x: int})
+is not included in
+  type t = @unboxed A(int) | B({x: int})
+Their internal representations differ:
+case A is @unboxed only in the second declaration.

Diagnostic added (coercion between representations)

type a = | @unboxed A(int) | B({x: int})
type b = | A(int) | B({x: int}) | C
let x: b = (A(3): a :> b)

diagnostic

-none; the coerced 3 matches C
+Type a is not a subtype of b

Diagnostic modified (unboxed configurations)

@unboxed type x = One(bool) | Two
type y = One(bool) | Two

diagnostic

-- Variant x is not unboxed.
-- Variant y is unboxed.
+- Variant x is unboxed.
+- Variant y is not unboxed.

Fixes #9038

@cknitt
cknitt marked this pull request as ready for review October 25, 2026 06:56
@cknitt

cknitt commented Oct 25, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 25, 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 ✅ Completed 2026-10-25T06:58:14.405992Z 8db3240 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.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

Reviewed commit: 8db3240cc5

ℹ️ 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".

@codecov

codecov Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.88889% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.32%. Comparing base (d48d577) to head (594ff28).
⚠️ Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
compiler/ml/includecore.ml 80.00% 2 Missing ⚠️
compiler/ml/variant_runtime.ml 80.00% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##           master    #9039   +/-   ##
=======================================
  Coverage   81.32%   81.32%           
=======================================
  Files         456      456           
  Lines       64042    64046    +4     
=======================================
+ Hits        52082    52087    +5     
+ Misses      11960    11959    -1     
Files with missing lines Coverage Δ
compiler/ml/ctype.ml 67.21% <100.00%> (ø)
compiler/ml/predef.ml 100.00% <100.00%> (ø)
compiler/ml/printtyp.ml 70.58% <100.00%> (+0.03%) ⬆️
compiler/ml/variant_coercion.ml 92.04% <ø> (-0.35%) ⬇️
compiler/ml/variant_runtime.ml 93.90% <80.00%> (-0.91%) ⬇️
compiler/ml/includecore.ml 77.60% <80.00%> (-0.86%) ⬇️

... and 1 file with indirect coverage changes

🚀 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.

@cknitt
cknitt enabled auto-merge (squash) October 25, 2026 07:04
@cknitt
cknitt requested a review from cristianoc October 25, 2026 07:04
@pkg-pr-new

pkg-pr-new Bot commented Oct 25, 2026

Copy link
Copy Markdown

Open in StackBlitz

rescript

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

@rescript/belt

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

@rescript/darwin-arm64

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

@rescript/linux-arm64

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

@rescript/linux-x64

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

@rescript/runtime

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

@rescript/win32-x64

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

commit: 8db3240

Why:
    An interface or a coercion accepted a tagged constructor for an
    `@unboxed` one: inclusion compared only the tags of tagged constructors,
    and the coercion check only the declared `@as` (#9038). The coercion
    error also said that the wrong variant was unboxed.

`Variant_runtime.same_representation` compares two constructors: constant
with the same literal, tagged with the same tag, or both untagged. Interface
inclusion and the coercion check use it. Type declarations in messages print
the `@unboxed` of an untagged constructor of a type that is not `@unboxed`
itself, and an interface mismatch names the constructor that is `@unboxed`
in only one declaration. The predefined `unknown`, an
`@unboxed` variant, now has the untagged layout a declaration of it gets, so
`@unboxed type rec t = unknown = Unknown(_): t` still agrees with it.

Diagnostic added (interface with an @unboxed constructor):
Example:
    module M: {
      type t = | @unboxed A(int) | B({x: int})
    } = {
      type t = | A(int) | B({x: int})
    }
Before (diagnostic): none; clients read {TAG: "A", _0: 3} as B
After (diagnostic):
    Type declarations do not match:
      type t = A(int) | B({x: int})
    is not included in
      type t = @unboxed A(int) | B({x: int})
    Their internal representations differ:
    case A is @unboxed only in the second declaration.

Diagnostic added (coercion between representations):
Example:
    type a = | @unboxed A(int) | B({x: int})
    type b = | A(int) | B({x: int}) | C
    let x: b = (A(3): a :> b)
Before (diagnostic): none; the coerced 3 matches C
After (diagnostic):
    Type a is not a subtype of b

Diagnostic modified (unboxed configurations):
Example:
    @unboxed type x = One(bool) | Two
    type y = One(bool) | Two
Before (diagnostic):
    - Variant x is not unboxed.
    - Variant y is unboxed.
After (diagnostic):
    - Variant x is unboxed.
    - Variant y is not unboxed.

Fixes #9038

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMkrYaktctdKc4M41WF36t
Signed-off-by: Christoph Knittel <[email protected]>
@cknitt
cknitt force-pushed the fix-per-constructor-unboxed branch from 8db3240 to 594ff28 Compare October 25, 2026 07:09

@cristianoc cristianoc left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we better remove per-case unboxed.
It obviously never worked and is probably not needed anymore.

@cknitt

cknitt commented Oct 25, 2026

Copy link
Copy Markdown
Member Author

Superseded by #9040, which removes @unboxed on constructors altogether, so a tagged and an untagged constructor can no longer meet in an interface or a coercion. The fix for the coercion error that named the wrong variant as unboxed moved there as its own commit.

@cknitt cknitt closed this Oct 25, 2026
auto-merge was automatically disabled October 25, 2026 08:38

Pull request was closed

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.

Per-constructor @unboxed is not compared across interfaces or coercions

2 participants