From da7aaa94cf907f12a7436bc3a8e3b487ab4381c6 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Sat, 10 Oct 2026 15:46:52 +0000 Subject: [PATCH 1/2] Allow boolean literal and payload overlap in untagged variants Reject a bool payload only when both true and false are literal cases. Fixes #8632. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Christoph Knittel --- CHANGELOG.md | 1 + compiler/ml/ast_untagged_variants.ml | 25 ++- compiler/ml/variant_runtime.ml | 7 +- tests/ERROR_VARIANTS.md | 1 + .../UntaggedUnreachableBoolean.res.expected | 9 ++ .../fixtures/UntaggedUnreachableBoolean.res | 2 + .../unboxed_bool_with_const/input.js | 2 +- tests/tests/src/unboxed_variant_fold_test.mjs | 145 ++++++++++++------ tests/tests/src/unboxed_variant_fold_test.res | 36 +++++ 9 files changed, 169 insertions(+), 59 deletions(-) create mode 100644 tests/build_tests/super_errors/expected/UntaggedUnreachableBoolean.res.expected create mode 100644 tests/build_tests/super_errors/fixtures/UntaggedUnreachableBoolean.res diff --git a/CHANGELOG.md b/CHANGELOG.md index 94a7708ed91..a377382e462 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ - Support Windows 11 on ARM by installing the x64 binaries, which Windows runs through its x64 emulation. https://github.com/rescript-lang/rescript/pull/8776 - Allow `$` in identifiers, e.g. `let count$ = signal(0)`. https://github.com/rescript-lang/rescript/pull/8791 +- Allow an untagged variant to have both a boolean literal case and a boolean payload case, e.g. `@unboxed type t = | @as(true) A | B(bool)`, like for strings and numbers. https://github.com/rescript-lang/rescript/pull/PRNUM #### :bug: Bug fix diff --git a/compiler/ml/ast_untagged_variants.ml b/compiler/ml/ast_untagged_variants.ml index ffd6a8a6de5..f95b6074840 100644 --- a/compiler/ml/ast_untagged_variants.ml +++ b/compiler/ml/ast_untagged_variants.ml @@ -9,6 +9,7 @@ type untagged_error = | AtMostOneNumber | AtMostOneBigint | AtMostOneBoolean + | UnreachableBoolean of string | DuplicateLiteral of string | ConstructorMoreThanOneArg of string type error = @@ -50,6 +51,10 @@ let report_error ppf = | AtMostOneFunction -> "At most one case can be a function type." | AtMostOneString -> "At most one case can be a string type." | AtMostOneBoolean -> "At most one case can be a boolean type." + | UnreachableBoolean name -> + "Case " ^ name + ^ " can never match: both true and false are already cases of this \ + variant." | AtMostOneNumber -> "At most one case can be a number type (int or float)." | AtMostOneBigint -> "At most one case can be a bigint type." @@ -259,11 +264,6 @@ let check_invariant ~is_untagged_def ~(consts : (Location.t * tag) list) raise (Error (loc, InvalidUntaggedVariantDefinition AtMostOneBigint)); if !boolean_types > 1 then raise (Error (loc, InvalidUntaggedVariantDefinition AtMostOneBoolean)); - if - !boolean_types > 0 - && (String_set.mem "true" !nonstring_literals_consts - || String_set.mem "false" !nonstring_literals_consts) - then raise (Error (loc, InvalidUntaggedVariantDefinition AtMostOneBoolean)); () in let check_literal ~is_const ~loc (tag : tag) = @@ -296,7 +296,20 @@ let check_invariant ~is_untagged_def ~(consts : (Location.t * tag) list) | FunctionType -> incr function_types | IntType | FloatType -> incr number_types | BigintType -> incr bigint_types - | BooleanType -> incr boolean_types + | BooleanType -> + (* Literal cases are matched before payload cases, so a boolean + payload only receives the booleans that are not literal cases. + A partial overlap is allowed, like for strings and numbers. *) + if + String_set.mem "true" !nonstring_literals_consts + && String_set.mem "false" !nonstring_literals_consts + then + raise + (Error + ( loc, + InvalidUntaggedVariantDefinition + (UnreachableBoolean tag.name) )); + incr boolean_types | StringType -> incr string_types); invariant loc tag.name | Tagged _ -> ()) diff --git a/compiler/ml/variant_runtime.ml b/compiler/ml/variant_runtime.ml index 83c6cc8714f..59fc20c9b0c 100644 --- a/compiler/ml/variant_runtime.ml +++ b/compiler/ml/variant_runtime.ml @@ -116,7 +116,12 @@ type matching_facts = { (** Runtime shapes of constructors represented directly by their payload. Tagged object constructors do not appear here. *) literal_tags: literal_tag list; - (** Runtime values of all nullary constructors. *) + (** Runtime values of all nullary constructors. In an untagged variant + these may overlap the values of a payload constructor, as in + [@as("a") A | S(string)]. Literals are always tested first, so the + payload constructor only receives values that are not literals, and + [S("a")] matches as [A]: constructor identity is not observable at + runtime for such values. *) has_null: bool; has_undefined: bool; has_other_literal: bool; diff --git a/tests/ERROR_VARIANTS.md b/tests/ERROR_VARIANTS.md index 89899c6c136..4449cd3a240 100644 --- a/tests/ERROR_VARIANTS.md +++ b/tests/ERROR_VARIANTS.md @@ -451,6 +451,7 @@ Untagged-variant validation errors. Source: `type untagged_error` and `type erro | `AtMostOneNumber` | ✓ | `UntaggedAtMostOneNumber.res` | `int` and `float` payloads collide on the number runtime check. | | `AtMostOneBigint` | ✓ | `UntaggedAtMostOneBigint.res` | Two bigint payloads. | | `AtMostOneBoolean` | ✓ | `UntaggedAtMostOneBoolean.res` | Two boolean payloads. | +| `UnreachableBoolean` | ✓ | `UntaggedUnreachableBoolean.res` | Boolean payload alongside both `@as(true)` and `@as(false)` cases. | | `DuplicateLiteral` | ✓ | `UntaggedDuplicateLiteral.res` | `@as("x")` on two different constructors. | | `ConstructorMoreThanOneArg` | ✓ | `UntaggedConstructorMoreThanOneArg.res`, `UntaggedConstructorAttributeMoreThanOneArg.res` | Multiple payload arguments with type-level or constructor-level `@unboxed`. | diff --git a/tests/build_tests/super_errors/expected/UntaggedUnreachableBoolean.res.expected b/tests/build_tests/super_errors/expected/UntaggedUnreachableBoolean.res.expected new file mode 100644 index 00000000000..0a8432dfe08 --- /dev/null +++ b/tests/build_tests/super_errors/expected/UntaggedUnreachableBoolean.res.expected @@ -0,0 +1,9 @@ + + We've found a bug for you! + /.../fixtures/UntaggedUnreachableBoolean.res:2:42-53 + + 1 │ @unboxed + 2 │ type t = | @as(true) Yes | @as(false) No | Flag(bool) + 3 │ + + This untagged variant definition is invalid: Case Flag can never match: both true and false are already cases of this variant. \ No newline at end of file diff --git a/tests/build_tests/super_errors/fixtures/UntaggedUnreachableBoolean.res b/tests/build_tests/super_errors/fixtures/UntaggedUnreachableBoolean.res new file mode 100644 index 00000000000..cc2eb8fbfe6 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/UntaggedUnreachableBoolean.res @@ -0,0 +1,2 @@ +@unboxed +type t = | @as(true) Yes | @as(false) No | Flag(bool) diff --git a/tests/build_tests/unboxed_bool_with_const/input.js b/tests/build_tests/unboxed_bool_with_const/input.js index 623e0ab0977..cf80313f917 100644 --- a/tests/build_tests/unboxed_bool_with_const/input.js +++ b/tests/build_tests/unboxed_bool_with_const/input.js @@ -13,7 +13,7 @@ const stderr = normalizeNewlines(stripVTControlCharacters(out.stderr)); assert.ok(stderr.includes("Main.res:3:3-14")); assert.ok( stderr.includes( - "This untagged variant definition is invalid: At most one case can be a boolean type.", + "This untagged variant definition is invalid: Case Bool can never match: both true and false are already cases of this variant.", ), ); assert.ok(stderr.includes("Failed to Compile")); diff --git a/tests/tests/src/unboxed_variant_fold_test.mjs b/tests/tests/src/unboxed_variant_fold_test.mjs index 3c83c7e569f..5397459c88f 100644 --- a/tests/tests/src/unboxed_variant_fold_test.mjs +++ b/tests/tests/src/unboxed_variant_fold_test.mjs @@ -47,6 +47,34 @@ function flagName(v) { } } +function answerName(v) { + if (v === true) { + return "yes"; + } else if (v) { + return "answer true"; + } else { + return "answer false"; + } +} + +function mixedName(v) { + if (v === false || v === null) { + if (v === false) { + return "off"; + } else { + return "unset"; + } + } else if (typeof v === "boolean") { + if (v) { + return "toggle true"; + } else { + return "toggle false"; + } + } else { + return v; + } +} + function numName(v) { if (v === 1) { return "one"; @@ -135,75 +163,88 @@ function makeRecord(value) { Mocha.describe("Unboxed_variant_fold_test", () => { Mocha.test("constructors preserve dynamic payloads and effects", () => { - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 142, characters 7-14", colorName(makeColor("primary")), "not Color"); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 143, characters 7-14", colorName(makeColor("blue")), "blue"); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 144, characters 7-14", recName(makeRecord(1)), "rec"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 165, characters 7-14", colorName(makeColor("primary")), "not Color"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 166, characters 7-14", colorName(makeColor("blue")), "blue"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 167, characters 7-14", recName(makeRecord(1)), "rec"); let calls = 0; calls = calls + 1 | 0; - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 150, characters 7-14", colorName("primary"), "not Color"); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 151, characters 7-14", calls, 1); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 173, characters 7-14", colorName("primary"), "not Color"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 174, characters 7-14", calls, 1); }); Mocha.test("folding agrees with runtime dispatch", () => { - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 154, characters 7-14", "not Color", colorName(id("primary"))); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 155, characters 7-14", "not Color", colorName(id("secondary"))); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 156, characters 7-14", "blue", colorName(id("blue"))); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 157, characters 7-14", "not Color", colorName(id("primary"))); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 158, characters 7-14", "one", numberName(id(1))); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 159, characters 7-14", "number", numberName(id(2))); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 160, characters 7-14", "one", numberName(id(1))); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 161, characters 7-14", "int", pureName(id(1))); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 162, characters 7-14", "string", pureName(id("x"))); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 163, characters 7-14", "obj", optName(id({ + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 177, characters 7-14", "not Color", colorName(id("primary"))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 178, characters 7-14", "not Color", colorName(id("secondary"))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 179, characters 7-14", "blue", colorName(id("blue"))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 180, characters 7-14", "not Color", colorName(id("primary"))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 181, characters 7-14", "one", numberName(id(1))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 182, characters 7-14", "number", numberName(id(2))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 183, characters 7-14", "one", numberName(id(1))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 184, characters 7-14", "int", pureName(id(1))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 185, characters 7-14", "string", pureName(id("x"))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 186, characters 7-14", "obj", optName(id({ x: 1 }))); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 164, characters 7-14", "null", optName(id(null))); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 165, characters 7-14", "undef", optName(id(undefined))); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 166, characters 7-14", "str", flagName(id("true"))); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 167, characters 7-14", "yes", flagName(id(true))); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 168, characters 7-14", "one", numName(id(1.0))); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 169, characters 7-14", "float", numName(id(2.5))); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 170, characters 7-14", "one", numName(id(1))); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 171, characters 7-14", "rec", recName(id({ + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 187, characters 7-14", "null", optName(id(null))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 188, characters 7-14", "undef", optName(id(undefined))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 189, characters 7-14", "str", flagName(id("true"))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 190, characters 7-14", "yes", flagName(id(true))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 191, characters 7-14", "yes", answerName(id(true))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 192, characters 7-14", "answer false", answerName(id(false))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 193, characters 7-14", "yes", answerName(id(true))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 194, characters 7-14", "toggle true", mixedName(id(true))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 195, characters 7-14", "off", mixedName(id(false))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 196, characters 7-14", "off", mixedName(id(false))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 197, characters 7-14", "unset", mixedName(id(null))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 198, characters 7-14", "false", mixedName(id("false"))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 199, characters 7-14", "one", numName(id(1.0))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 200, characters 7-14", "float", numName(id(2.5))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 201, characters 7-14", "one", numName(id(1))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 202, characters 7-14", "rec", recName(id({ y: 1 }))); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 172, characters 7-14", "empty", recName(id("empty"))); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 173, characters 7-14", "lit", pick(id("a"))); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 174, characters 7-14", "z", pick(id("z"))); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 175, characters 7-14", "lit", pick(id("a"))); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 176, characters 7-14", "w", outerName(id("primary"))); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 177, characters 7-14", "x", outerName(id("x"))); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 178, characters 7-14", "x", outerName(id("x"))); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 179, characters 7-14", "primary", boxedName(id({ + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 203, characters 7-14", "empty", recName(id("empty"))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 204, characters 7-14", "lit", pick(id("a"))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 205, characters 7-14", "z", pick(id("z"))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 206, characters 7-14", "lit", pick(id("a"))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 207, characters 7-14", "w", outerName(id("primary"))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 208, characters 7-14", "x", outerName(id("x"))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 209, characters 7-14", "x", outerName(id("x"))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 210, characters 7-14", "primary", boxedName(id({ TAG: "BoxedColor", _0: "primary" }))); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 180, characters 7-14", "not Color", boxedName(id("primary"))); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 181, characters 7-14", "tuple", objectName(id([ + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 211, characters 7-14", "not Color", boxedName(id("primary"))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 212, characters 7-14", "tuple", objectName(id([ 1, 2 ]))); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 182, characters 7-14", "record", objectName(id({ + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 213, characters 7-14", "record", objectName(id({ x: 1 }))); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 183, characters 7-14", "zero", listName(id(/* [] */0))); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 184, characters 7-14", "list", listName(id({hd: 1, tl: /* [] */0}))); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 185, characters 7-14", bigintName(10n), bigintName(id(10n))); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 186, characters 7-14", "one", wideName(id(1))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 214, characters 7-14", "zero", listName(id(/* [] */0))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 215, characters 7-14", "list", listName(id({hd: 1, tl: /* [] */0}))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 216, characters 7-14", bigintName(10n), bigintName(id(10n))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 217, characters 7-14", "one", wideName(id(1))); }); Mocha.test("the folded answers themselves are correct", () => { - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 190, characters 7-14", "not Color", "not Color"); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 191, characters 7-14", "blue", "blue"); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 192, characters 7-14", "one", "one"); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 193, characters 7-14", "number", "number"); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 194, characters 7-14", "int", "int"); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 195, characters 7-14", "one", "one"); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 196, characters 7-14", "lit", "lit"); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 197, characters 7-14", "x", "x"); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 198, characters 7-14", "primary", "primary"); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 199, characters 7-14", "tuple", "tuple"); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 200, characters 7-14", "zero", "zero"); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 201, characters 7-14", bigintName(10n), "one"); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 202, characters 7-14", "one", "one"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 221, characters 7-14", "not Color", "not Color"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 222, characters 7-14", "blue", "blue"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 223, characters 7-14", "one", "one"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 224, characters 7-14", "number", "number"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 225, characters 7-14", "int", "int"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 226, characters 7-14", "yes", "yes"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 227, characters 7-14", "answer false", "answer false"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 228, characters 7-14", "off", "off"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 229, characters 7-14", "toggle true", "toggle true"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 230, characters 7-14", "false", "false"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 231, characters 7-14", "one", "one"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 232, characters 7-14", "lit", "lit"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 233, characters 7-14", "x", "x"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 234, characters 7-14", "primary", "primary"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 235, characters 7-14", "tuple", "tuple"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 236, characters 7-14", "zero", "zero"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 237, characters 7-14", bigintName(10n), "one"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 238, characters 7-14", "one", "one"); }); }); @@ -213,6 +254,8 @@ export { pureName, optName, flagName, + answerName, + mixedName, numName, recName, pick, diff --git a/tests/tests/src/unboxed_variant_fold_test.res b/tests/tests/src/unboxed_variant_fold_test.res index a83553cdd7b..50801ff9ca3 100644 --- a/tests/tests/src/unboxed_variant_fold_test.res +++ b/tests/tests/src/unboxed_variant_fold_test.res @@ -57,6 +57,29 @@ let flagName = v => | Str(_) => "str" } +// A boolean payload only receives the boolean that is not a literal case. +@unboxed type answer = | @as(true) Yes2 | Answer(bool) +let answerName = v => + switch v { + | Answer(true) => "answer true" + | Yes2 => "yes" + | Answer(false) => "answer false" + } + +@unboxed +type mixed = + | @as(false) Off + | @as(null) Unset + | Toggle(bool) + | Label(string) +let mixedName = v => + switch v { + | Off => "off" + | Unset => "unset" + | Toggle(b) => b ? "toggle true" : "toggle false" + | Label(s) => s + } + // @as(1) and a float payload of 1.0 are one JavaScript number. @unboxed type num = | @as(1) One2 | F(float) let numName = v => @@ -165,6 +188,14 @@ describe(__MODULE__, () => { eq(__LOC__, optName(Missing), optName(id(Missing))) eq(__LOC__, flagName(Str("true")), flagName(id(Str("true")))) eq(__LOC__, flagName(Yes), flagName(id(Yes))) + eq(__LOC__, answerName(Answer(true)), answerName(id(Answer(true)))) + eq(__LOC__, answerName(Answer(false)), answerName(id(Answer(false)))) + eq(__LOC__, answerName(Yes2), answerName(id(Yes2))) + eq(__LOC__, mixedName(Toggle(true)), mixedName(id(Toggle(true)))) + eq(__LOC__, mixedName(Toggle(false)), mixedName(id(Toggle(false)))) + eq(__LOC__, mixedName(Off), mixedName(id(Off))) + eq(__LOC__, mixedName(Unset), mixedName(id(Unset))) + eq(__LOC__, mixedName(Label("false")), mixedName(id(Label("false")))) eq(__LOC__, numName(F(1.0)), numName(id(F(1.0)))) eq(__LOC__, numName(F(2.5)), numName(id(F(2.5)))) eq(__LOC__, numName(One2), numName(id(One2))) @@ -192,6 +223,11 @@ describe(__MODULE__, () => { eq(__LOC__, numberName(Number(1)), "one") eq(__LOC__, numberName(Number(2)), "number") eq(__LOC__, pureName(I(1)), "int") + eq(__LOC__, answerName(Answer(true)), "yes") + eq(__LOC__, answerName(Answer(false)), "answer false") + eq(__LOC__, mixedName(Toggle(false)), "off") + eq(__LOC__, mixedName(Toggle(true)), "toggle true") + eq(__LOC__, mixedName(Label("false")), "false") eq(__LOC__, numName(F(1.0)), "one") eq(__LOC__, pick(C("a")), "lit") eq(__LOC__, outerName(W(I2("x"))), "x") From b1fd7a00d1a031ff8e7f16add4586e4f554a6869 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Sat, 10 Oct 2026 15:47:09 +0000 Subject: [PATCH 2/2] Add PR link to CHANGELOG Co-Authored-By: Claude Opus 5.5 Signed-off-by: Christoph Knittel --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a377382e462..1b4f1ada152 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ - Support Windows 11 on ARM by installing the x64 binaries, which Windows runs through its x64 emulation. https://github.com/rescript-lang/rescript/pull/8776 - Allow `$` in identifiers, e.g. `let count$ = signal(0)`. https://github.com/rescript-lang/rescript/pull/8791 -- Allow an untagged variant to have both a boolean literal case and a boolean payload case, e.g. `@unboxed type t = | @as(true) A | B(bool)`, like for strings and numbers. https://github.com/rescript-lang/rescript/pull/PRNUM +- Allow an untagged variant to have both a boolean literal case and a boolean payload case, e.g. `@unboxed type t = | @as(true) A | B(bool)`, like for strings and numbers. https://github.com/rescript-lang/rescript/pull/8777 #### :bug: Bug fix