From 69f57c00ef829d003e87afd0cbe2f9d7d4d77a54 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Thu, 22 Oct 2026 18:57:55 +0000 Subject: [PATCH] Compare variant literals by their JavaScript value 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 Claude-Session: https://claude.ai/code/session_01Fc7LEuyEfynjaKCVEBPmTF Signed-off-by: Christoph Knittel --- changes/as-literals-by-value.fix.md | 1 + compiler/ml/UntaggedVariants.md | 30 ++-- compiler/ml/ast_untagged_variants.ml | 55 +++++-- compiler/ml/lambda.ml | 13 +- compiler/ml/variant_runtime.ml | 19 +++ compiler/ml/variant_runtime.mli | 7 + .../untagged_literal_same_bigint.res.expected | 8 + .../untagged_literal_same_int32.res.expected | 8 + .../untagged_literal_same_number.res.expected | 8 + .../fixtures/untagged_literal_same_bigint.res | 1 + .../fixtures/untagged_literal_same_int32.res | 1 + .../fixtures/untagged_literal_same_number.res | 1 + tests/tests/src/unboxed_variant_fold_test.mjs | 147 ++++++++++-------- tests/tests/src/unboxed_variant_fold_test.res | 16 ++ 14 files changed, 204 insertions(+), 111 deletions(-) create mode 100644 changes/as-literals-by-value.fix.md create mode 100644 tests/build_tests/super_errors/expected/untagged_literal_same_bigint.res.expected create mode 100644 tests/build_tests/super_errors/expected/untagged_literal_same_int32.res.expected create mode 100644 tests/build_tests/super_errors/expected/untagged_literal_same_number.res.expected create mode 100644 tests/build_tests/super_errors/fixtures/untagged_literal_same_bigint.res create mode 100644 tests/build_tests/super_errors/fixtures/untagged_literal_same_int32.res create mode 100644 tests/build_tests/super_errors/fixtures/untagged_literal_same_number.res diff --git a/changes/as-literals-by-value.fix.md b/changes/as-literals-by-value.fix.md new file mode 100644 index 00000000000..08aeb71f1d6 --- /dev/null +++ b/changes/as-literals-by-value.fix.md @@ -0,0 +1 @@ +- Reject `@as` literals of one variant that are the same JavaScript value, such as `@as(1)` and `@as(1.0)`, and accept `@as(1)` beside `@as(1n)`. diff --git a/compiler/ml/UntaggedVariants.md b/compiler/ml/UntaggedVariants.md index ba1b48e6e4d..cec70e49753 100644 --- a/compiler/ml/UntaggedVariants.md +++ b/compiler/ml/UntaggedVariants.md @@ -149,10 +149,15 @@ The type checker rejects an untagged variant, with | an unknown case beside another case with a payload | Case A has a payload that is not of one of the recognized shapes ... | `UntaggedOnlyOneUnknown.res`, `UntaggedUnknown.res` | | a `bool` case when both `true` and `false` are literal cases | Case B can never match ... | `UntaggedUnreachableBoolean.res` | | two nullary cases with the same literal | Duplicate literal x. | `UntaggedDuplicateLiteral.res` | +| two nullary cases whose literals are spelled differently but are the same JavaScript value | The literal 1.0 is the same JavaScript value as 1. | `untagged_literal_same_number.res`, `untagged_literal_same_int32.res`, `untagged_literal_same_bigint.res` | Literals are compared per kind: string literals with each other (a case without `@as` counts as its name), and all other literals with each other by -their spelling. The duplicate-literal check applies to every variant, tagged +the JavaScript value they denote, as the `===` of a `switch` compares them. +An int is the 32-bit number it is emitted as (rule 3), so `1`, `1.0` and +`4294967297` are one number, and so are `0` and `-0.`; `1n` is a bigint and +differs from `1`, and `001n` is `1n`. Constant folding (rule 11) compares +literals the same way. The duplicate-literal check applies to every variant, tagged or not; in a tagged variant it also compares the `@as` tags of the cases with a payload. @@ -161,10 +166,12 @@ A literal may equal a value of a payload case: `@as("a") A | S(string)`, `@as(true) A | B(bool)` are accepted. Rule 7 says which case such a value belongs to. -- Implementation: `ast_untagged_variants.ml:check_invariant`. -- Tests: the fixtures above; `TaggedDuplicateLiteral.res`, +- Implementation: `ast_untagged_variants.ml:check_invariant`, + `variant_runtime.ml:literal_value_key`. +- Tests: the fixtures above; `tests/tests/src/unboxed_variant_fold_test.res` + (a number and a bigint literal with the same digits); `TaggedDuplicateLiteral.res`, `multiple_tag_1.res`, `multiple_tag_2.res` (tagged variants). -- Shaped by #6128, #6136, #7441, #8777. +- Shaped by #6128, #6136, #7441, #8777, #8953. ### 7. Literal cases are tested first @@ -349,21 +356,6 @@ cases with a payload (see Known bugs). Each was observed with the compiler built from this tree; the programs are compiled with `bsc` and run in Node 25. -**Numeric literals are compared by spelling.** Rule 6 compares `1` and `1.0`, -and `1` and `4294967297`, as different literals, while they are one -JavaScript number. - -```rescript -@unboxed type t = | @as(1) A | @as(1.0) B -let name = v => switch v { | A => "A" | B => "B" } -@inline(never) let id = x => x -Console.log2(name(B), name(id(B))) // B A -``` - -The declaration is accepted; `B` is `1` at run time and matches as `A`, while -the folded `name(B)` returns `"B"`. With `@as(4294967297) B`, `name(id(B))` -returns `"A"` as well. - **`Some` of a constant `undefined` case is `None`.** Rule 12 boxes `Some(v)` for a variant with an `@as(undefined)` case, but a constant argument skips the boxing. diff --git a/compiler/ml/ast_untagged_variants.ml b/compiler/ml/ast_untagged_variants.ml index cfded53fe56..37807fd4455 100644 --- a/compiler/ml/ast_untagged_variants.ml +++ b/compiler/ml/ast_untagged_variants.ml @@ -11,6 +11,7 @@ type untagged_error = | AtMostOneBoolean | UnreachableBoolean of string | DuplicateLiteral of string + | SameLiteralValue of string * string | ConstructorMoreThanOneArg of string type error = | InvalidVariantAsAnnotation @@ -59,6 +60,9 @@ let report_error ppf = "At most one case can be a number type (int or float)." | AtMostOneBigint -> "At most one case can be a bigint type." | DuplicateLiteral s -> "Duplicate literal " ^ s ^ "." + | SameLiteralValue (literal, earlier) -> + "The literal " ^ literal ^ " is the same JavaScript value as " ^ earlier + ^ "." | ConstructorMoreThanOneArg name -> "Constructor " ^ name ^ " has more than one argument.") | TagFieldNameConflict (constructor_name, field_name, runtime_value) -> @@ -210,13 +214,18 @@ let is_nullary_variant (x : Types.constructor_arguments) = | Types.Cstr_tuple [] -> true | _ -> false +(* A literal's JavaScript value, which a [switch] compares with [===]. Ints + are emitted as 32-bit numbers, so [1], [1.0] and [4294967297] are one + number, while [1n] is a bigint. *) let check_invariant ~is_untagged_def ~(consts : (Location.t * tag) list) ~(blocks : (Location.t * block) list) = let module String_set = Set.Make (String) in + let module String_map = Map.Make (String) in let string_literals_consts = ref String_set.empty in let string_literals_blocks = ref String_set.empty in - let nonstring_literals_consts = ref String_set.empty in - let nonstring_literals_blocks = ref String_set.empty in + (* Keyed by [literal_key], with the spelling of the literal *) + let nonstring_literals_consts = ref String_map.empty in + let nonstring_literals_blocks = ref String_map.empty in let instance_types = Hashtbl.create 1 in let function_types = ref 0 in let object_types = ref 0 in @@ -233,13 +242,21 @@ let check_invariant ~is_untagged_def ~(consts : (Location.t * tag) list) raise (Error (loc, InvalidUntaggedVariantDefinition (DuplicateLiteral s))); set := String_set.add s !set in - let add_nonstring_literal ~is_const ~loc s = - let set = + let add_nonstring_literal ~is_const ~loc literal s = + let map = if is_const then nonstring_literals_consts else nonstring_literals_blocks in - if String_set.mem s !set then - raise (Error (loc, InvalidUntaggedVariantDefinition (DuplicateLiteral s))); - set := String_set.add s !set + let key = Variant_runtime.literal_value_key literal in + (match String_map.find_opt key !map with + | Some earlier when earlier = s -> + raise (Error (loc, InvalidUntaggedVariantDefinition (DuplicateLiteral s))) + | Some earlier -> + raise + (Error + ( loc, + InvalidUntaggedVariantDefinition (SameLiteralValue (s, earlier)) )) + | None -> ()); + map := String_map.add key s !map in let invariant loc name = if !unknown_types <> 0 && List.length blocks <> 1 then @@ -269,13 +286,19 @@ let check_invariant ~is_untagged_def ~(consts : (Location.t * tag) list) match tag.literal with | None -> add_string_literal ~is_const ~loc tag.name | Some (String s) -> add_string_literal ~is_const ~loc s - | Some (Int i) -> add_nonstring_literal ~is_const ~loc (string_of_int i) - | Some (Float f) -> add_nonstring_literal ~is_const ~loc f - | Some (BigInt i) -> add_nonstring_literal ~is_const ~loc i - | Some (Bool b) -> - add_nonstring_literal ~is_const ~loc (if b then "true" else "false") - | Some Null -> add_nonstring_literal ~is_const ~loc "null" - | Some Undefined -> add_nonstring_literal ~is_const ~loc "undefined" + | Some (Int i as literal) -> + add_nonstring_literal ~is_const ~loc literal (string_of_int i) + | Some (Float f as literal) -> + add_nonstring_literal ~is_const ~loc literal f + | Some (BigInt i as literal) -> + add_nonstring_literal ~is_const ~loc literal (i ^ "n") + | Some (Bool b as literal) -> + add_nonstring_literal ~is_const ~loc literal + (if b then "true" else "false") + | Some (Null as literal) -> + add_nonstring_literal ~is_const ~loc literal "null" + | Some (Undefined as literal) -> + add_nonstring_literal ~is_const ~loc literal "undefined" in Ext_list.rev_iter consts (fun (loc, literal) -> @@ -300,8 +323,8 @@ let check_invariant ~is_untagged_def ~(consts : (Location.t * tag) list) 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 + String_map.mem "true" !nonstring_literals_consts + && String_map.mem "false" !nonstring_literals_consts then raise (Error diff --git a/compiler/ml/lambda.ml b/compiler/ml/lambda.ml index 275103d415f..b27e16e2410 100644 --- a/compiler/ml/lambda.ml +++ b/compiler/ml/lambda.ml @@ -841,18 +841,7 @@ let runtime_value_kind (c : structured_constant) = [1.0] are the same JavaScript number. Bigint spellings are not compared; both bigint payloads and bigint constructors are classified as unknown. Integer tags are emitted through [Int32.of_int], just like [Const_int]. *) -let literal_denotes_same (a : Variant_runtime.literal_tag) - (b : Variant_runtime.literal_tag) = - match (a, b) with - | String x, String y -> x = y - | Int x, Int y -> Int32.of_int x = Int32.of_int y - | Float x, Float y -> float_of_string x = float_of_string y - | Int x, Float y | Float y, Int x -> - Int32.to_float (Int32.of_int x) = float_of_string y - | Bool x, Bool y -> x = y - | Null, Null | Undefined, Undefined -> true - | (String _ | Int _ | Float _ | Bool _ | BigInt _ | Null | Undefined), _ -> - false +let literal_denotes_same = Variant_runtime.same_literal_value (* Mirror [Dynamic_checks]: literals take precedence; the object case excludes arrays when an array case exists. Int and float share one runtime type. *) diff --git a/compiler/ml/variant_runtime.ml b/compiler/ml/variant_runtime.ml index 59fc20c9b0c..e5db19f22ae 100644 --- a/compiler/ml/variant_runtime.ml +++ b/compiler/ml/variant_runtime.ml @@ -79,6 +79,25 @@ type literal_tag = | Null | Undefined +(* [-0.] and [0.] are equal for [===]; [%h] is exact for every other number *) +let number_key x = "number:" ^ if x = 0. then "0" else Printf.sprintf "%h" x + +let literal_value_key = function + | Int i -> number_key (Int32.to_float (Int32.of_int i)) + | Float f -> ( + match float_of_string_opt f with + | Some x -> number_key x + | None -> "number:" ^ f) + | BigInt i -> + let positive, digits = Bigint_utils.parse_bigint i in + "bigint:" ^ Bigint_utils.to_string (positive || digits = "0") digits + | Bool b -> if b then "true" else "false" + | Null -> "null" + | Undefined -> "undefined" + | String s -> "string:" ^ s + +let same_literal_value a b = literal_value_key a = literal_value_key b + (** Information used to recognize a constructor during matching. A literal identifies a constant or an object's tag; a payload shape identifies a value represented directly by its payload. *) diff --git a/compiler/ml/variant_runtime.mli b/compiler/ml/variant_runtime.mli index c49d40a4d4b..c8555da8c61 100644 --- a/compiler/ml/variant_runtime.mli +++ b/compiler/ml/variant_runtime.mli @@ -50,6 +50,13 @@ type literal_tag = | Null | Undefined +val literal_value_key : literal_tag -> string +(** A key that two literals share exactly when they are the same JavaScript + value, as [===] compares them: ints are emitted as 32-bit numbers, so + [1], [1.0] and [4294967297] share one, while [1n] is a bigint. *) + +val same_literal_value : literal_tag -> literal_tag -> bool + (** Information used to recognize a constructor during matching. A literal identifies a constant or an object's tag; a payload shape identifies a value represented directly by its payload. *) diff --git a/tests/build_tests/super_errors/expected/untagged_literal_same_bigint.res.expected b/tests/build_tests/super_errors/expected/untagged_literal_same_bigint.res.expected new file mode 100644 index 00000000000..88dd35a3340 --- /dev/null +++ b/tests/build_tests/super_errors/expected/untagged_literal_same_bigint.res.expected @@ -0,0 +1,8 @@ + + We've found a bug for you! + /.../fixtures/untagged_literal_same_bigint.res:1:31-43 + + 1 │ @unboxed type t = | @as(1n) A | @as(001n) B + 2 │ + + This untagged variant definition is invalid: The literal 001n is the same JavaScript value as 1n. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/untagged_literal_same_int32.res.expected b/tests/build_tests/super_errors/expected/untagged_literal_same_int32.res.expected new file mode 100644 index 00000000000..f2c978779d2 --- /dev/null +++ b/tests/build_tests/super_errors/expected/untagged_literal_same_int32.res.expected @@ -0,0 +1,8 @@ + + We've found a bug for you! + /.../fixtures/untagged_literal_same_int32.res:1:30-48 + + 1 │ @unboxed type t = | @as(1) A | @as(4294967297) B + 2 │ + + This untagged variant definition is invalid: The literal 4294967297 is the same JavaScript value as 1. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/untagged_literal_same_number.res.expected b/tests/build_tests/super_errors/expected/untagged_literal_same_number.res.expected new file mode 100644 index 00000000000..145864914f0 --- /dev/null +++ b/tests/build_tests/super_errors/expected/untagged_literal_same_number.res.expected @@ -0,0 +1,8 @@ + + We've found a bug for you! + /.../fixtures/untagged_literal_same_number.res:1:30-41 + + 1 │ @unboxed type t = | @as(1) A | @as(1.0) B + 2 │ + + This untagged variant definition is invalid: The literal 1.0 is the same JavaScript value as 1. \ No newline at end of file diff --git a/tests/build_tests/super_errors/fixtures/untagged_literal_same_bigint.res b/tests/build_tests/super_errors/fixtures/untagged_literal_same_bigint.res new file mode 100644 index 00000000000..62ce418f6b3 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/untagged_literal_same_bigint.res @@ -0,0 +1 @@ +@unboxed type t = | @as(1n) A | @as(001n) B diff --git a/tests/build_tests/super_errors/fixtures/untagged_literal_same_int32.res b/tests/build_tests/super_errors/fixtures/untagged_literal_same_int32.res new file mode 100644 index 00000000000..2797958cbcc --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/untagged_literal_same_int32.res @@ -0,0 +1 @@ +@unboxed type t = | @as(1) A | @as(4294967297) B diff --git a/tests/build_tests/super_errors/fixtures/untagged_literal_same_number.res b/tests/build_tests/super_errors/fixtures/untagged_literal_same_number.res new file mode 100644 index 00000000000..f481acb49ae --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/untagged_literal_same_number.res @@ -0,0 +1 @@ +@unboxed type t = | @as(1) A | @as(1.0) B diff --git a/tests/tests/src/unboxed_variant_fold_test.mjs b/tests/tests/src/unboxed_variant_fold_test.mjs index 5397459c88f..2e1a13ff3c3 100644 --- a/tests/tests/src/unboxed_variant_fold_test.mjs +++ b/tests/tests/src/unboxed_variant_fold_test.mjs @@ -147,6 +147,18 @@ function wideName(value) { } } +function numberOrBigintName(value) { + if (typeof value !== "string") { + if (value === 1) { + return "number"; + } else { + return "bigint"; + } + } else { + return value; + } +} + function id(x) { return x; } @@ -163,88 +175,94 @@ 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 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"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 174, characters 7-14", colorName(makeColor("primary")), "not Color"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 175, characters 7-14", colorName(makeColor("blue")), "blue"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 176, characters 7-14", recName(makeRecord(1)), "rec"); let calls = 0; calls = calls + 1 | 0; - 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); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 182, characters 7-14", colorName("primary"), "not Color"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 183, characters 7-14", calls, 1); }); Mocha.test("folding agrees with runtime dispatch", () => { - 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({ + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 186, characters 7-14", "not Color", colorName(id("primary"))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 187, characters 7-14", "not Color", colorName(id("secondary"))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 188, characters 7-14", "blue", colorName(id("blue"))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 189, characters 7-14", "not Color", colorName(id("primary"))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 190, characters 7-14", "one", numberName(id(1))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 191, characters 7-14", "number", numberName(id(2))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 192, characters 7-14", "one", numberName(id(1))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 193, characters 7-14", "int", pureName(id(1))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 194, characters 7-14", "string", pureName(id("x"))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 195, characters 7-14", "obj", optName(id({ x: 1 }))); - 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({ + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 196, characters 7-14", "null", optName(id(null))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 197, characters 7-14", "undef", optName(id(undefined))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 198, characters 7-14", "str", flagName(id("true"))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 199, characters 7-14", "yes", flagName(id(true))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 200, characters 7-14", "yes", answerName(id(true))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 201, characters 7-14", "answer false", answerName(id(false))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 202, characters 7-14", "yes", answerName(id(true))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 203, characters 7-14", "toggle true", mixedName(id(true))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 204, characters 7-14", "off", mixedName(id(false))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 205, characters 7-14", "off", mixedName(id(false))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 206, characters 7-14", "unset", mixedName(id(null))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 207, characters 7-14", "false", mixedName(id("false"))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 208, characters 7-14", "one", numName(id(1.0))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 209, characters 7-14", "float", numName(id(2.5))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 210, characters 7-14", "one", numName(id(1))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 211, characters 7-14", "rec", recName(id({ y: 1 }))); - 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({ + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 212, characters 7-14", "empty", recName(id("empty"))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 213, characters 7-14", "lit", pick(id("a"))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 214, characters 7-14", "z", pick(id("z"))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 215, characters 7-14", "lit", pick(id("a"))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 216, characters 7-14", "w", outerName(id("primary"))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 217, characters 7-14", "x", outerName(id("x"))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 218, characters 7-14", "x", outerName(id("x"))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 219, characters 7-14", "primary", boxedName(id({ TAG: "BoxedColor", _0: "primary" }))); - 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([ + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 220, characters 7-14", "not Color", boxedName(id("primary"))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 221, characters 7-14", "tuple", objectName(id([ 1, 2 ]))); - Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 213, characters 7-14", "record", objectName(id({ + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 222, characters 7-14", "record", objectName(id({ x: 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))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 223, characters 7-14", "zero", listName(id(/* [] */0))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 224, characters 7-14", "list", listName(id({hd: 1, tl: /* [] */0}))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 225, characters 7-14", bigintName(10n), bigintName(id(10n))); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 226, 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 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"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 230, characters 7-14", "not Color", "not Color"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 231, characters 7-14", "blue", "blue"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 232, characters 7-14", "one", "one"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 233, characters 7-14", "number", "number"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 234, characters 7-14", "int", "int"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 235, characters 7-14", "yes", "yes"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 236, characters 7-14", "answer false", "answer false"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 237, characters 7-14", "off", "off"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 238, characters 7-14", "toggle true", "toggle true"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 239, characters 7-14", "false", "false"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 240, characters 7-14", "one", "one"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 241, characters 7-14", "lit", "lit"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 242, characters 7-14", "x", "x"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 243, characters 7-14", "primary", "primary"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 244, characters 7-14", "tuple", "tuple"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 245, characters 7-14", "zero", "zero"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 246, characters 7-14", bigintName(10n), "one"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 247, characters 7-14", "one", "one"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 248, characters 7-14", numberOrBigintName(1n), "bigint"); + }); + Mocha.test("a number and a bigint literal with the same digits", () => { + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 252, characters 7-14", numberOrBigintName(id(1)), "number"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 253, characters 7-14", numberOrBigintName(id(1n)), "bigint"); + Test_utils.eq("File \"unboxed_variant_fold_test.res\", line 254, characters 7-14", numberOrBigintName(id("x")), "x"); }); }); @@ -265,6 +283,7 @@ export { listName, bigintName, wideName, + numberOrBigintName, id, makeColor, makeRecord, diff --git a/tests/tests/src/unboxed_variant_fold_test.res b/tests/tests/src/unboxed_variant_fold_test.res index 50801ff9ca3..bcb77655686 100644 --- a/tests/tests/src/unboxed_variant_fold_test.res +++ b/tests/tests/src/unboxed_variant_fold_test.res @@ -154,6 +154,15 @@ let wideName = value => | WideInt(_) => "int" } +// A number and a bigint with the same digits are different values. +@unboxed type numberOrBigint = | @as(1) NumberOne | @as(1n) BigintOne | Other(string) +let numberOrBigintName = value => + switch value { + | NumberOne => "number" + | BigintOne => "bigint" + | Other(s) => s + } + @inline(never) let id = x => x // Nonconstant scalar payloads are passed through; inline records remain objects. @@ -236,5 +245,12 @@ describe(__MODULE__, () => { eq(__LOC__, listName(Values(list{})), "zero") eq(__LOC__, bigintName(Wrapped(DecimalTen)), "one") eq(__LOC__, wideName(WideInt(1)), "one") + eq(__LOC__, numberOrBigintName(BigintOne), "bigint") + }) + + test("a number and a bigint literal with the same digits", () => { + eq(__LOC__, numberOrBigintName(id(NumberOne)), "number") + eq(__LOC__, numberOrBigintName(id(BigintOne)), "bigint") + eq(__LOC__, numberOrBigintName(id(Other("x"))), "x") }) })