Skip to content

Compare variant literals by their JavaScript value - #8965

Merged
cknitt merged 1 commit into
masterfrom
untagged-literal-values
Oct 23, 2026
Merged

cknitt merged 1 commit into
masterfrom
untagged-literal-values

Conversation

@cknitt

@cknitt cknitt commented Oct 22, 2026

Copy link
Copy Markdown
Member

Fixes #8953.

The duplicate-literal check of variants compared @as literals by how they are spelled, not by the JavaScript value they become. That fails both ways:

Declaration JS values Before After
@as(1) A | @as(1.0) B 1, 1 accepted; a dispatched B matches A error
@as(1) A | @as(4294967297) B 1, 1 (ints are emitted as 32-bit) accepted; same error
@as(0) A | @as(-0.) B equal for === accepted error
@as(1n) A | @as(001n) B 1n, 1n accepted error
@as(1) One | @as(1n) OneN | S(string) 1, 1n (different) "Duplicate literal 1." accepted

Literals are now keyed by the value a switch compares with ===: ints as the 32-bit number they are emitted as, floats as the number they parse to, bigints by their normalized digits, apart from numbers. When two spellings meet, the error names both:

This untagged variant definition is invalid: The literal 1.0 is the same JavaScript value as 1.

The key lives in Variant_runtime.literal_value_key, and constant folding (Lambda.literal_denotes_same) now uses it too. It already compared numbers by value, but treated two equal bigints as different, so a constant bigint case was never folded.

UntaggedVariants.md rule 6 now states the comparison, and the known bug is removed.

Breaking: a variant with two @as literals that are one JavaScript value is now an error. Such a variant already matched the second case as the first at run time.

New tests:

  • super_errors: untagged_literal_same_number, untagged_literal_same_int32, untagged_literal_same_bigint.
  • unboxed_variant_fold_test.res: a number and a bigint literal with the same digits, dispatched and folded.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Fc7LEuyEfynjaKCVEBPmTF

Why:
    The duplicate-literal check compared `@as` literals by spelling, so it
    accepted `@as(1) A | @as(1.0) B`, two cases with the same runtime value
    that a dispatched `B` matches as `A` (#8953), and rejected
    `@as(1) A | @as(1n) B`, two different values.

Literals other than strings are now keyed by the value a `switch` compares
with `===`: an int as the 32-bit number it is emitted as, a float as the
number it parses to, with `-0.` and `0` equal, and a bigint by its
normalized digits, apart from numbers. Constant folding uses the same key,
so a constant bigint case now folds too. The spec's rule 6 states the
comparison, and the known bug is gone.

Diagnostic added (same value, different spelling):
Example:
    @unboxed type t = | @as(1) A | @as(1.0) B
After (diagnostic):
    This untagged variant definition is invalid: The literal 1.0 is the same JavaScript value as 1.

Diagnostic removed (a number and a bigint):
Example:
    @unboxed type t = | @as(1) One | @as(1n) OneN | S(string)
Before (diagnostic):
    This untagged variant definition is invalid: Duplicate literal 1.

Breaking: a variant with two `@as` literals that are the same JavaScript
value, such as `@as(1)` and `@as(4294967297)`, is now an error. Such a
variant already matched the second case as the first at run time.

Fixes: #8953
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

Compare variant literals by their JavaScript value

69f57c0 · Christoph Knittel with Claude Opus 5.5 · Oct 22, 2026

Why

The duplicate-literal check compared @as literals by spelling, so it accepted @as(1) A | @as(1.0) B, two cases with the same runtime value that a dispatched B matches as A (#8953), and rejected @as(1) A | @as(1n) B, two different values.

Literals other than strings are now keyed by the value a switch compares with ===: an int as the 32-bit number it is emitted as, a float as the number it parses to, with -0. and 0 equal, and a bigint by its normalized digits, apart from numbers. Constant folding uses the same key, so a constant bigint case now folds too. The spec's rule 6 states the comparison, and the known bug is gone.

Diagnostic added (same value, different spelling)

@unboxed type t = | @as(1) A | @as(1.0) B

diagnostic · After

This untagged variant definition is invalid: The literal 1.0 is the same JavaScript value as 1.

Diagnostic removed (a number and a bigint)

@unboxed type t = | @as(1) One | @as(1n) OneN | S(string)

diagnostic · Before

This untagged variant definition is invalid: Duplicate literal 1.

[!CAUTION]
Breaking: a variant with two @as literals that are the same JavaScript value, such as @as(1) and @as(4294967297), is now an error. Such a variant already matched the second case as the first at run time.

@codecov

codecov Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.33333% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.22%. Comparing base (b123b7c) to head (69f57c0).

Files with missing lines Patch % Lines
compiler/ml/variant_runtime.ml 83.33% 2 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##           master    #8965   +/-   ##
=======================================
  Coverage   81.22%   81.22%           
=======================================
  Files         457      457           
  Lines       63853    63862    +9     
=======================================
+ Hits        51865    51875   +10     
+ Misses      11988    11987    -1     
Files with missing lines Coverage Δ
compiler/ml/ast_untagged_variants.ml 87.20% <100.00%> (+0.26%) ⬆️
compiler/ml/lambda.ml 68.02% <ø> (+0.02%) ⬆️
compiler/ml/variant_runtime.ml 94.80% <83.33%> (-2.12%) ⬇️
🚀 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 22, 2026

Copy link
Copy Markdown

Open in StackBlitz

rescript

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

@rescript/belt

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

@rescript/darwin-arm64

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

@rescript/linux-arm64

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

@rescript/linux-x64

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

@rescript/runtime

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

@rescript/win32-x64

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

commit: 69f57c0

@github-actions

Copy link
Copy Markdown

@cknitt

cknitt commented Oct 22, 2026

Copy link
Copy Markdown
Member Author

@codex review

@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 ✅ Completed 2026-10-22T19:25:08.157100Z 69f57c0 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. Keep them coming!

Reviewed commit: 69f57c00ef

ℹ️ 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 marked this pull request as ready for review October 22, 2026 19:31
@cknitt
cknitt requested a review from cristianoc October 22, 2026 19:31

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

Great.
I would not call it a breaking change: it follows what has always been the intended spec.

@cknitt
cknitt merged commit 9e2f371 into master Oct 23, 2026
26 checks passed
@cknitt
cknitt deleted the untagged-literal-values branch October 23, 2026 05:10
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.

Untagged variant accepts @as number literals that are the same JavaScript number

2 participants