Skip to content

Check variants with some @unboxed constructors like untagged ones - #9021

Merged
cknitt merged 1 commit into
masterfrom
fix-mixed-variant-checks
Oct 24, 2026
Merged

cknitt merged 1 commit into
masterfrom
fix-mixed-variant-checks

Conversation

@cknitt

@cknitt cknitt commented Oct 24, 2026

Copy link
Copy Markdown
Member

Fixes #9020.

A variant whose type isn't @unboxed, but which has @unboxed on some of its constructors, skipped the checks that keep untagged cases apart. check_invariant ran them only for @unboxed types. Three wrong declarations compiled, and their matches picked the wrong case:

  • two number cases;
  • an unknown payload beside another payload;
  • an untagged object beside a tagged block.

Meanwhile, the @as of an untagged case was compared with the tags of tagged cases, although it doesn't set one.

Now:

  • the payload kinds are checked as soon as one case is untagged;
  • tagged blocks count together as one object case;
  • only tagged cases' tags are compared for duplicates.

New tests:

  • fixtures untagged_mixed_two_numbers.res, untagged_mixed_unknown_payload.res and untagged_mixed_object_and_tagged.res (each now an error);
  • untagged_mixed_test.res: an untagged case's @as equal to a tagged case's tag compiles and matches correctly.

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

🤖 Generated with Claude Code

https://claude.ai/code/session_01CMkrYaktctdKc4M41WF36t

Why:
    The checks that keep the cases of an untagged variant apart ran only
    when the type was `@unboxed`. A variant with `@unboxed` on some
    constructors could have two number cases, an unknown payload beside
    another, or an untagged object beside a tagged block, and its matches
    then picked the wrong case (#9020). The `@as` of an untagged case was
    meanwhile compared with tags, which it does not set.

The payload kinds are checked as soon as one case is untagged, tagged
blocks count as one object case, and only tagged cases' tags are compared
for duplicates.

Diagnostic added (two number cases in a mixed variant):
Example:
    type t = | @unboxed A(int) | @unboxed B(int) | C
Before (diagnostic): none; `B(5)` matches `A`
After (diagnostic):
    This untagged variant definition is invalid: At most one case can be a number type (int or float).

Diagnostic added (untagged object beside a tagged block):
Example:
    type t = | @unboxed A({x: int}) | B({y: int}) | C
Before (diagnostic): none; `A({x: 1})` matches `B`
After (diagnostic):
    This untagged variant definition is invalid: At most one case can be an object type.

Diagnostic removed (an untagged case's @as):
Example:
    type t = | @unboxed @as("x") A(int) | @as("x") B({y: int}) | C
Before (diagnostic):
    This untagged variant definition is invalid: Duplicate literal x.
After (diagnostic): none

Fixes #9020

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

Copy link
Copy Markdown

Changelog

🐛 Bug fix

  • Check variants with some @unboxed constructors like untagged variants, so that cases a match cannot tell apart are rejected, and an untagged case's @as no longer clashes with a tagged case's tag.

Semantic Diff

1 commit · Full page

Check variants with some @unboxed constructors like untagged ones

ef7c8c1 · cknitt + Claude · Oct 24, 2026, 12:33 UTC

Why

The checks that keep the cases of an untagged variant apart ran only when the type was @unboxed. A variant with @unboxed on some constructors could have two number cases, an unknown payload beside another, or an untagged object beside a tagged block, and its matches then picked the wrong case (#9020). The @as of an untagged case was meanwhile compared with tags, which it does not set.

The payload kinds are checked as soon as one case is untagged, tagged blocks count as one object case, and only tagged cases' tags are compared for duplicates.

Diagnostic added (two number cases in a mixed variant)

type t = | @unboxed A(int) | @unboxed B(int) | C

diagnostic

-none; `B(5)` matches `A`
+This untagged variant definition is invalid: At most one case can be a number type (int or float).

Diagnostic added (untagged object beside a tagged block)

type t = | @unboxed A({x: int}) | B({y: int}) | C

diagnostic

-none; `A({x: 1})` matches `B`
+This untagged variant definition is invalid: At most one case can be an object type.

Diagnostic removed (an untagged case's @as)

type t = | @unboxed @as("x") A(int) | @as("x") B({y: int}) | C

diagnostic

-This untagged variant definition is invalid: Duplicate literal x.
+none

Fixes #9020

@codecov

codecov Bot commented Oct 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 81.31%. Comparing base (d53b8ae) to head (ef7c8c1).

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #9021   +/-   ##
=======================================
  Coverage   81.31%   81.31%           
=======================================
  Files         456      456           
  Lines       64003    64010    +7     
=======================================
+ Hits        52044    52052    +8     
+ Misses      11959    11958    -1     
Files with missing lines Coverage Δ
compiler/ml/ast_untagged_variants.ml 89.03% <100.00%> (+0.60%) ⬆️
🚀 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.

@pkg-pr-new

pkg-pr-new Bot commented Oct 24, 2026

Copy link
Copy Markdown

Open in StackBlitz

rescript

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

@rescript/belt

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

@rescript/darwin-arm64

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

@rescript/linux-arm64

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

@rescript/linux-x64

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

@rescript/runtime

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

@rescript/win32-x64

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

commit: ef7c8c1

@github-actions

Copy link
Copy Markdown

@cknitt
cknitt marked this pull request as ready for review October 24, 2026 13:28
@cknitt

cknitt commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 24, 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-24T16:22:36.933581Z ef7c8c1 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.

@cknitt
cknitt requested a review from cristianoc October 24, 2026 16:20
@cknitt

cknitt commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

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

Reviewed commit: ef7c8c1bc6

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

@cknitt
cknitt enabled auto-merge (squash) October 24, 2026 16:32
cknitt added a commit that referenced this pull request Oct 24, 2026
Why:
    The checks that keep the cases of an untagged variant apart ran only
    when the type was `@unboxed`. A variant with `@unboxed` on some
    constructors could have two number cases, an unknown payload beside
    another, or an untagged object beside a tagged block, and its matches
    then picked the wrong case (#9020). The `@as` of an untagged case was
    meanwhile compared with tags, which it does not set.

The payload kinds are checked as soon as one case is untagged, tagged
blocks count as one object case, and only tagged cases' tags are compared
for duplicates.

Diagnostic added (two number cases in a mixed variant):
Example:
    type t = | @unboxed A(int) | @unboxed B(int) | C
Before (diagnostic): none; `B(5)` matches `A`
After (diagnostic):
    This untagged variant definition is invalid: At most one case can be a number type (int or float).

Diagnostic added (untagged object beside a tagged block):
Example:
    type t = | @unboxed A({x: int}) | B({y: int}) | C
Before (diagnostic): none; `A({x: 1})` matches `B`
After (diagnostic):
    This untagged variant definition is invalid: At most one case can be an object type.

Diagnostic removed (an untagged case's @as):
Example:
    type t = | @unboxed @as("x") A(int) | @as("x") B({y: int}) | C
Before (diagnostic):
    This untagged variant definition is invalid: Duplicate literal x.
After (diagnostic): none

The same fix as #9021 on master, for the stack's check_invariant; rule 6
says so.

Fixes #9020

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>

@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 guess @unboxed on single cases should go.
Now seems redundant. It was needed when we did not have untagged variants.

@cknitt
cknitt merged commit 41a2860 into master Oct 24, 2026
26 checks passed
@cknitt
cknitt deleted the fix-mixed-variant-checks branch October 24, 2026 17:55
cknitt added a commit that referenced this pull request Oct 24, 2026
#9021 checks a variant with some `@unboxed` constructors like an untagged
one: tagged blocks count as one object case, and an untagged case's `@as`
is not a tag. Rule 6 now says so and names its tests.

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

Variants with some @unboxed constructors skip the untagged variant checks

2 participants