Repository navigation
Conversation
…ants
Why:
`bool` and `unit` are declared as variants, with the constructors
`false`, `true` and `()` and no `@as`, but their values are `false`,
`true` and `undefined`. The coercion between a variant and a primitive
left out `bool` but took `unit` for a variant whose constructor is the
string "()", so `(() :> string)` compiled to nothing and left
`undefined` typed as a `string`. The coercion from a closed polymorphic
variant left out neither type and matched `#"true"` to the constructor
`true` by name. A variant that spread either type built `true`, `false`
and `undefined`, but matched and coerced them as strings.
`Variant_coercion.is_bool_or_unit` now keeps both types out of the
coercions between a variant and a primitive, the coercion from a
polymorphic variant (`type_is_variant`, renamed
`can_try_coerce_polyvariant_to_variant`) and variant spreads.
`Ctype.subtype_rec` also rejects a coercion between either type and another
variant or record before the variant checks, whose messages described them
as variants.
Diagnostic added (unit coerced to string):
Example:
let s: string = (() :> string)
let n = s->String.length
Before (js): throws a TypeError when the module loads
let n = undefined.length;
let s;
After (diagnostic):
A.res:1:18-29
Type unit is not a subtype of string
Diagnostic added (polymorphic variant coerced to bool):
Example:
let toBool = (p: [#"true" | #"false"]): bool => (p :> bool)
let isFalse = toBool(#"false") == false
Before (js):
function toBool(p) {
return p;
}
let isFalse = "false" === false;
After (diagnostic):
A.res:1:50-58
Type [#"false" | #"true"] is not a subtype of bool
Diagnostic added (bool spread into a variant):
Example:
type t = | ...bool | Other
let x: t = true
let r = switch x { | true => "yes" | false => "no" | Other => "other" }
Before (js): `r` stays undefined
switch (true) {
case "false" :
r = "no";
break;
case "true" :
r = "yes";
break;
…
}
After (diagnostic):
A.res:1:15-18
This type is not a valid type to spread. It's only possible to spread other variants.
Diagnostic modified (unit coerced to int):
Example:
let i: int = (() :> int)
Before (diagnostic):
Type unit is not a subtype of int
The constructor () of variant unit has no @as payload, which makes it a string at runtime.
That runtime representation is not compatible with the expected runtime representation of int.
Fix this by making sure all constructors in variant unit has a runtime representation of int.
After (diagnostic):
Type unit is not a subtype of int
Diagnostic modified (coercion suggested for unit):
Example:
let u = (): unit => ()
let s: string = u()
Before (diagnostic):
This has type: unit
But it's expected to have type: string
Possible solutions:
- These types are compatible at runtime. You can use the coercion operator to convert to the expected type: (u() :> string)
After (diagnostic):
This has type: unit
But it's expected to have type: string
Review: Does a working program stop compiling? One that coerces `unit` to
`string` or a polymorphic variant to `bool` or `unit`, which gave a
value of another type than its own, or that spreads `bool` or `unit`
into a variant, whose switches never matched `true`, `false` or `()`.
A variant that only used its other constructors can declare them
without the spread.
Review: Which coercions still work? `bool` and `unit` coerce to themselves,
from their aliases and from a private type defined as one, and a
variant `False | True` still coerces to `string`. `option`, `list` and
`result` have type parameters, so these coercions never applied to
them. No existing snapshot or test output changes.
Signed-off-by: mununki <woonki.moon@gmail.com>
Changelog🐛 Bug fix
💅 Polish
Semantic Diff1 commit · Full page Reject coercions and variant spreads that treat bool and unit as variantsfa1b7f1 · mununki · Nov 04, 2026, 05:00 UTC
Diagnostic added (unit coerced to string) let s: string = (() :> string)
let n = s->String.length
let n = undefined.length;
let s;
Diagnostic added (polymorphic variant coerced to bool) let toBool = (p: [#"true" | #"false"]): bool => (p :> bool)
let isFalse = toBool(#"false") == false
function toBool(p) {
return p;
}
let isFalse = "false" === false;
Diagnostic added (bool spread into a variant) type t = | ...bool | Other
let x: t = true
let r = switch x { | true => "yes" | false => "no" | Other => "other" }
switch (true) {
case "false" :
r = "no";
break;
case "true" :
r = "yes";
break;
…
}
Diagnostic modified (unit coerced to int) let i: int = (() :> int)
Type unit is not a subtype of int
-
-The constructor () of variant unit has no @as payload, which makes it a string at runtime.
-That runtime representation is not compatible with the expected runtime representation of int.
-
-Fix this by making sure all constructors in variant unit has a runtime representation of int.Diagnostic modified (coercion suggested for unit) let u = (): unit => ()
let s: string = u()
This has type: unit
But it's expected to have type: string
-
-Possible solutions:
-- These types are compatible at runtime. You can use the coercion operator to convert to the expected type: (u() :> string)Review
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #9218 +/- ##
=======================================
Coverage 81.75% 81.75%
=======================================
Files 457 457
Lines 65332 65339 +7
=======================================
+ Hits 53413 53419 +6
- Misses 11919 11920 +1
🚀 New features to boost your workflow:
|
rescript
@rescript/belt
@rescript/darwin-arm64
@rescript/linux-arm64
@rescript/linux-x64
@rescript/runtime
@rescript/win32-x64
commit: |
|
Developer playground preview: https://rescript-lang.github.io/rescript/dev-playground/?version=pr-9218 |
|
@codex review |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Codex Review: Didn't find any major issues. Delightful! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
Fixes: #9216
unitcould be coerced tostring, and a closed polymorphic variant whose tags are"true","false"or"()"toboolorunit.(() :> string)compiled to nothing, solet s: string = (() :> string)leftsasundefined, ands->String.lengththrew a TypeError;(p :> bool)forp: [#"true" | #"false"]returned the string"false", which is truthy. Type errors suggested both coercions as fixes. A variant could also spreadboolorunit:type t = | ...bool | Otherbuilttrueandfalseas booleans, but its switch compared them with the strings"true"and"false"and never matched, and(x :> string)left a boolean typed as astring.boolandunitare declared as variants with the constructorsfalse,trueand()and no@as, but their values arefalse,trueandundefined.Variant_coercion.can_try_coerce_variant_to_primitiveleft outboolbut notunit, so the check took()for its name as a string, andVariant_coercion.type_is_variant, which guards the coercion from a polymorphic variant, left out neither type. A newVariant_coercion.is_bool_or_unitnow keeps both types out of these two coercions (type_is_variantis renamedcan_try_coerce_polyvariant_to_variant), andVariant_type_spread.map_constructorsrejects a spread of either as an invalid spread.Ctype.subtype_recalso rejects a coercion between either type and another variant or record before the variant checks, whose messages described them as variants that could take@asor@unboxed.(() :> string), also through an alias or a private abbreviation ofunitand wherever the subtype check comparesunitwithstring(function parameters and results,promise<unit>, tuples, record and object fields, constructor payloads), is an error, and type errors no longer suggest it.boolorunit, and spreadingboolorunitinto a variant, are errors.(() :> int),("a" :> unit)and(() :> unknown)are still errors, now with the plain "Type unit is not a subtype of int" and no advice about@asor@unboxed.(true :> string)was already rejected and is unchanged.boolandunitstill coerce to themselves and from their aliases and private abbreviations, and| ...booland| ...unitstill work as patterns on aboolorunit. A variant with constructors such asFalse | Truestill coerces tostring, and variants with@aspayloads, unboxed variants and coercions from polymorphic variants to other variants are unchanged.option,listandresulthave type parameters, so these coercions never applied to them. No existing super_errors snapshot or test output changes.Tests: super_errors fixtures for
unitcoerced tostring,intandunknown,stringcoerced tounit, polymorphic variants coerced toboolandunit, the missing coercion suggestion forunit, andboolandunitspread into a variant. A test inVariantCoercion.reskeeps a variantFalse | Truecoercing tostringand from a polymorphic variant.Checklist
changes/README.md), or no release notes are needed and a maintainer should applychangelog:skip.CONTRIBUTING.md).git commit -s).