diff --git a/changes/untagged-variants-spec.internal.md b/changes/untagged-variants-spec.internal.md new file mode 100644 index 0000000000..d7bc0a287f --- /dev/null +++ b/changes/untagged-variants-spec.internal.md @@ -0,0 +1 @@ +- Document the rules of untagged variants in `compiler/ml/UntaggedVariants.md`, with tests pinning them. diff --git a/compiler/ml/README.md b/compiler/ml/README.md index 717e44fcd9..7d7322d71c 100644 --- a/compiler/ml/README.md +++ b/compiler/ml/README.md @@ -31,6 +31,10 @@ For module inclusion and signature compatibility, start with [`includecore.mli`](includecore.mli), [`includemod.mli`](includemod.mli), and [`mtype.mli`](mtype.mli). +[`UntaggedVariants.md`](UntaggedVariants.md) states the rules of untagged +(`@unboxed`) variants: payload kinds, run-time recognition, well-formedness, +and the interaction with `option`. + ## Main data and operations [`types.mli`](types.mli) diff --git a/compiler/ml/UntaggedVariants.md b/compiler/ml/UntaggedVariants.md new file mode 100644 index 0000000000..ba1b48e6e4 --- /dev/null +++ b/compiler/ml/UntaggedVariants.md @@ -0,0 +1,452 @@ +# Untagged variants + +An untagged variant is a variant type declared with `@unboxed` whose cases are +represented by their bare JavaScript values: a nullary case by a literal, a +case with a payload by the payload itself. No field records which case a value +belongs to, so a `switch` recognizes the case by testing the value at run +time, and the type checker limits the payload cases to kinds these tests tell +apart (rule 6). The user manual describes the feature in +[Untagged variants](https://rescript-lang.org/docs/manual/variant/#untagged-variants). + +This document states the rules the compiler implements. Each rule names the +code that implements it and the tests that pin it. Tests named +`spec_test` are in +[`tests/tests/src/untagged_variants_spec_test.res`](../../tests/tests/src/untagged_variants_spec_test.res); +the committed `.mjs` beside it shows the checks emitted for each declaration. +Fixtures are in `tests/build_tests/super_errors/fixtures/` unless another +directory is named. + +## Rules + +### 1. Declaration + +`@unboxed` and `@tag(...)` on a variant type are copied onto every +constructor. `@unboxed` on a single constructor of an ordinary variant makes +that constructor untagged and leaves the others tagged: + +| Declaration | Value | JavaScript | +| --- | --- | --- | +| `type mixed = \| @unboxed MA(int) \| MB({x: int}) \| MC` | `MA(3)` | `3` | +| | `MB({x: 1})` | `{TAG: "MB", x: 1}` | +| | `MC` | `"MC"` | + +The layout of a declaration is computed once, after its recursive group is in +the environment, and stored on the declaration. + +- Implementation: `typedecl.ml` (`copy_tag_attr_from_decl` in + `transl_declaration`; the layout pass in `transl_type_decl`), + `variant_layout.ml:layout_from_type_variant`. +- Tests: `spec_test` rule 1. +- Shaped by #7320 (recursive groups), #8579 (layout stored once). + +### 2. Representation + +A nullary case is its `@as` literal, or the string of its name when it has no +`@as`. A case with a payload is the payload; an inline-record payload is a +plain object holding the fields. In an untagged variant, `@as` on a case with +a payload and `@tag` on the type do not change the representation. + +| Declaration | Value | JavaScript | +| --- | --- | --- | +| `@unboxed type t = Named \| @as("s") Str \| @as(null) Null \| Payload({x: int})` | `Named` | `"Named"` | +| | `Str` | `"s"` | +| | `Null` | `null` | +| | `Payload({x: 1})` | `{x: 1}` | +| `@unboxed @tag("kind") type t = TI({x: int}) \| TS(string)` | `TI({x: 1})` | `{x: 1}` | +| `@unboxed type t = @as("x") AsX(string) \| AsY(int)` | `AsX("v")` | `"v"` | + +- Implementation: `variant_runtime.ml:compute_matching_facts` (a nullary case + without `@as` is its name), `translcore.ml:transl_exp0` (constructor + application). +- Tests: `spec_test` rule 2. + +### 3. `@as` literals + +`@as` takes a string, an int, a float, a bigint, a boolean, `null` or +`undefined`. Any other payload is an error, and so is a second `@as` on one +constructor. An int literal must fit the compiler's native integer; it is +emitted with the 32-bit representation of `int` values, so `@as(4294967297)` +is the number `1`. + +- Implementation: `ast_untagged_variants.ml:runtime_tag_of_parsetree`, + `process_constructor_tag`. +- Tests: fixtures `UntaggedInvalidVariantAsAnnotation.res` + ("A variant case annotation @as(...) must be a string, integer, boolean, + null, or undefined."), `VariantAsIntegerOutOfRange.res`, + `UntaggedDuplicatedBsAs.res`; `spec_test` rule 2; + `tests/tests/src/unboxed_variant_fold_test.res` (`wide`). + +### 4. Payload kinds + +A case of an untagged variant has at most one payload. Its type is classified +into one kind: + +| Payload type | Kind | Recognized by | +| --- | --- | --- | +| `string` | string | `typeof x === "string"` | +| `int`, `float` | number | `typeof x === "number"` | +| `bigint` | bigint | `typeof x === "bigint"` | +| `bool` | boolean | `typeof x === "boolean"` | +| a function type | function | `typeof x === "function"` | +| a record (not `@unboxed`), an inline record, `dict<_>` | object | `typeof x === "object"` | +| `array<_>`, a tuple | instance `Array` | `Array.isArray(x)` | +| a type in the list of rule 5 | instance `C` | `x instanceof C` | +| anything else: an abstract type, a type variable, a variant, `list<_>`, an `@unboxed` record | unknown | no check | + +The payload type is classified after expanding abbreviations +(`type myDate = Date.t` is an instance case), except that the list of rule 5 +is consulted first on the type as written. `@notUndefined` on an abstract type +leaves it unknown. + +- Implementation: `variant_layout.ml:get_block_type_from_typ`, + `get_block_type`; `ast_untagged_variants.ml:type_is_builtin_object`. +- Tests: `spec_test` rules 4 (`kinds`, `tuples`, `aliasDict`, `instances`, + `unknown`); fixtures `UntaggedConstructorMoreThanOneArg.res`, + `UntaggedConstructorAttributeMoreThanOneArg.res`, `UntaggedNonUnary1.res`, + `UntaggedNonUnary2.res` ("Constructor A has more than one argument."), + `UntaggedUnknown.res` (`list` is unknown). +- Shaped by #6121, #6208, #6219, #6279, #6368, #6394, #6550, #6670. + +### 5. Instance types + +The instance cases come from a fixed list of type names in +`ast_untagged_variants.ml:type_to_instanceof_backed_obj`. The predefined +`promise<_>` and `array<_>` are matched by path; the others by the printed +path of the type: + +| Type | Class | +| --- | --- | +| `promise<_>` | `Promise` | +| `array<_>` | `Array` (checked with `Array.isArray`) | +| `Date.t`, `RegExp.t`, `File.t`, `Blob.t`, `ArrayBuffer.t`, `DataView.t` | the class of the same name | +| `Set.t`, `Map.t`, `WeakSet.t`, `WeakMap.t` | the class of the same name | +| `Int8Array.t`, `Uint8Array.t`, `Uint8ClampedArray.t`, `Int16Array.t`, `Uint16Array.t`, `Int32Array.t`, `Uint32Array.t`, `Float32Array.t`, `Float64Array.t`, `BigInt64Array.t`, `BigUint64Array.t` | the class of the same name | + +A type outside this list, such as a binding to a Web API class, is unknown +(rule 4). The typed-array types are abbreviations of one type, +`TypedArray.t<_>`, and are told apart only by the name as written. + +- Implementation: `ast_untagged_variants.ml:type_to_instanceof_backed_obj`, + `variant_runtime.ml:Instance`. +- Tests: `spec_test` rules 4 and 5; fixtures `UntaggedAtMostOneInstance.res`, + `UntaggedTupleAndArray.res`. +- Shaped by #6296, #6383, #6396, #7788, #8009. Related: #7147. + +### 6. Well-formedness + +The type checker rejects an untagged variant, with +"This untagged variant definition is invalid: ..." and the reason below, when: + +| Condition | Reason | Fixture | +| --- | --- | --- | +| two object cases | At most one case can be an object type. | `UntaggedAtMostOneObject.res` | +| two cases of one instance class (arrays and tuples share `Array`) | At most one case can be a C type. | `UntaggedAtMostOneInstance.res`, `UntaggedTupleAndArray.res` | +| two function cases | At most one case can be a function type. | `UntaggedAtMostOneFunction.res` | +| two string cases | At most one case can be a string type. | `UntaggedAtMostOneString.res` | +| two number cases (`int` and `float` count together) | At most one case can be a number type (int or float). | `UntaggedAtMostOneNumber.res` | +| two bigint cases | At most one case can be a bigint type. | `UntaggedAtMostOneBigint.res` | +| two boolean cases | At most one case can be a boolean type. | `UntaggedAtMostOneBoolean.res` | +| 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` | + +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 +or not; in a tagged variant it also compares the `@as` tags of the cases with +a payload. + +A literal may equal a value of a payload case: `@as("a") A | S(string)`, +`@as(1) A | I(int)`, `@as(null) N | O({x: int})` and +`@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`, + `multiple_tag_1.res`, `multiple_tag_2.res` (tagged variants). +- Shaped by #6128, #6136, #7441, #8777. + +### 7. Literal cases are tested first + +A `switch` with literal and payload arms first tests whether the value is one +of the literal cases, compares it with `===` against each literal arm, and +only otherwise tests the payload cases. A value equal to a literal therefore +belongs to the literal's case, even when it was built with a payload +constructor: + +| Declaration | Value | Case | +| --- | --- | --- | +| `@as("a") LitA \| AnyString(string)` | `AnyString("a")` | `LitA` | +| `@as(1) LitOne \| AnyInt(int)` | `AnyInt(1)` | `LitOne` | +| `@as(true) LitTrue \| AnyBool(bool)` | `AnyBool(true)` | `LitTrue` | +| `@as(true) LitTrue \| AnyBool(bool)` | `AnyBool(false)` | `AnyBool` | +| `@as(null) LitNull \| AnyObject({x: int})` | `LitNull` | `LitNull` | + +The "is a literal case" test is the smaller of two equivalent expressions: the +disjunction of `x === literal` over the literal cases, or the conjunction of +the negated payload checks (with a payload check replaced by the literal +disjunction where a literal overlaps it). + +- Implementation: `lam_compile.ml:compile_switch`, + `ast_untagged_variants.ml:Dynamic_checks.is_a_literal_case`. +- Tests: `spec_test` rule 7; `tests/tests/src/untagged_variants_heuristic.res` + (choice of expression). +- Shaped by #6125, #7128, #7135. + +### 8. Payload cases + +The payload arms compile in one of two forms. + +- **Two arms, or one arm and a default**: one `if` on the check of the arm + declared first, with the other branch unchecked (rule 10). The object check is + `typeof x === "object"`, followed by `x !== null` when the variant has an + `@as(null)` case, and by `!(x instanceof C)` (`!Array.isArray(x)` for + arrays) for every instance case of the variant. +- **Three or more arms**: an `if` chain of the instance checks, in the + declaration order of the cases, then one `switch (typeof x)` over the other + arms. + +In both forms a value of an instance case never lands in the object case. +An `int` case is recognized by `typeof x === "number"` and so receives every +number, integral or not. + +- Implementation: `lam_compile.ml:compile_untagged_cases`, + `compile_general_cases`; + `ast_untagged_variants.ml:Dynamic_checks.add_runtime_type_check`. +- Tests: `spec_test` rules 4, 5 and 8 (`instanceName` shows the chain in + declaration order, `objectFirst` both forms); `tests/tests/src/UntaggedVariants.res` + (`AllInstanceofTypes`, `MergeCases`). +- Shaped by #6103, #6383, #6792, #7144, #7149. + +### 9. `null` and arrays are not objects + +`typeof null` and `typeof []` are `"object"`. The object case never receives +`null` when the variant has an `@as(null)` case, and never receives an array +when the variant has an array or tuple case: the two-arm form adds the checks +of rule 8, and the `switch (typeof x)` form is preceded by +`x === null` / `Array.isArray(x)` guards that send those values to the default +arm when their own arm is absent. + +| Declaration | `switch` arms | Value | Arm | +| --- | --- | --- | --- | +| `GArray(array) \| GObject({x: int}) \| @as(null) GNull` | all | `[]` | `GArray` | +| | all | `null` | `GNull` | +| | `GObject(_)`, `_` | `[]`, `null` | `_` | + +- Implementation: `lam_compile.ml:compile_untagged_cases` (`needs_null_guard`, + `needs_array_guard`), `Dynamic_checks.add_runtime_type_check`. +- Tests: `spec_test` rule 9; `UntaggedVariants.res` (`ObjectAndNull`, `Json`). +- Shaped by #6219, #7303, #8253, #8255, #8279. + +### 10. Values outside every case + +Recognition trusts the type. A value that belongs to no case is not rejected: + +- in the two-arm form it lands in the unchecked branch: with + `I(int) | S(string)`, `true` lands in `S`; with + `TDate(Date.t) | TRegExp(RegExp.t)`, `{x: 1}` lands in `TRegExp`; with + `PPromise(promise) | PFloat(float)`, a thenable that is not a + `Promise` lands in `PFloat`; +- in the `switch (typeof x)` form it matches no arm, and the `switch` + completes without running any arm. + +A value from another JavaScript realm fails every `instanceof` check and is +recognized the same way (by the semantics of `instanceof`; not tested). + +- Implementation: `lam_compile.ml:compile_general_cases` (a complete match + emits no check for its last clause). +- Tests: `spec_test` rule 10 (two-arm form); `kindName` in + `untagged_variants_spec_test.mjs` shows the `switch` form. + +### 11. Constant folding + +A `switch` on a constant untagged value folds to the arm that the run-time +dispatch selects for the constant's JavaScript value, so `S("a")` matched +against `@as("a") A | S(string)` folds to `A`. + +- Implementation: `lambda.ml:switch` (`untagged_action`, `literal_denotes_same`). +- Tests: `tests/tests/src/unboxed_variant_fold_test.res`. +- Shaped by #8631. + +### 12. `option` of an untagged variant + +`Some(v)` is emitted as `v` when the type of `v` cannot hold `undefined`, and +as `Primitive_option.some(v)` otherwise. An untagged variant cannot hold +`undefined` when no case is `@as(undefined)` and every payload type is +`dict<_>` or cannot hold `undefined` itself: + +| Payload type | Holds `undefined` | +| --- | --- | +| `string`, `int`, `float`, `bool`, `bigint`, a function, a tuple, a record without `@unboxed` | no | +| an inline record with two or more fields | no | +| an `@unboxed` record, an inline record with one field | as the type of its field | +| an abstract type with `@notUndefined` (rule 13) | no | +| an abstract type without it, a type variable, `option<_>` | yes | +| a type whose declaration is not on the load path | yes | + +An `@as(null)` case does not prevent unboxing; `Some(N)` is `null`. + +| Declaration | `Some(v)` | +| --- | --- | +| `@as(null) WNull \| WString(string)` | `v` | +| `@as(undefined) WUndefined \| WUString(string)` | `Primitive_option.some(v)` | +| `@as("n") ONone \| OPaque(opaque)` | `Primitive_option.some(v)` | +| `BBlob(Blob.t) \| BString(string)` | `Primitive_option.some(v)` | +| `DDate(Date.t) \| DString(string)` | `v` | +| `RRecord(record) \| RString(string)` with `type record = {y: option}` | `v` | +| `INone \| IRecord({x: option})` | `Primitive_option.some(v)` | +| `@as("n") UONone \| UOption(unboxedOption)` with `@unboxed type unboxedOption = {x: option}` | `Primitive_option.some(v)` | +| `@as("n") USNone \| UString(unboxedString)` with `@unboxed type unboxedString = {s: string}` | `v` | + +The stdlib marks `Date.t`, `RegExp.t`, `Map.t`, `Set.t`, `WeakMap.t`, +`WeakSet.t`, `ArrayBuffer.t`, `DataView.t`, the typed arrays and other +abstract types `@notUndefined`; `Blob.t` and `File.t` are not marked. + +- Implementation: `typeopt.ml:type_cannot_contain_undefined`. +- Tests: `spec_test` rule 12; `UntaggedVariants.res` + (`OptionUnboxingHeuristic`); `tests/tests/src/option_wrapping_test.res`; + `tests/build_tests/option_transitive_dependency`. +- Shaped by #6222, #6233, #8719. + +### 13. `@notUndefined` + +`@notUndefined` on an abstract type without a manifest declares that its +values are never `undefined`. It affects only rule 12: `option` of the type, +and of an untagged variant with a payload of the type, is unboxed. It does not +make the type a recognized payload kind (rule 4). On any other type +declaration it is an error, "@notUndefined can only be used on abstract +types". Interface inclusion does not compare it. + +- Implementation: `typedecl.ml:transl_declaration`, `is_not_undefined_attr`; + `typeopt.ml:type_cannot_contain_undefined`. +- Tests: `spec_test` rule 13; fixture `invalid_attribute_not_undefined.res`; + `tests/build_tests/not_undefined_attribute`. +- Shaped by #7458, #7464. + +### 14. `@tag` and interfaces + +In a tagged variant, `@tag("f")` names the field that holds the tag of a case +with a payload (`TAG` by default), and `@as` on such a case sets the tag's +value. An inline-record field whose runtime name equals the tag field is an +error. + +An interface and its implementation must agree on `@unboxed`, on `@tag`, and +on the literal of every nullary case. They are not compared on the `@as` of +cases with a payload (see Known bugs). + +- Implementation: `variant_layout.ml:layout_from_type_variant`, + `ast_untagged_variants.ml:process_tag_name`, `check_tag_field_conflicts`; + `includecore.ml:compare_variants`, `type_declarations`. +- Tests: `spec_test` rule 14; fixtures `UntaggedTagFieldNameConflict.res`, + `variant_tag_overlaps_with_field.res`, `UntaggedInvalidVariantTagAnnotation.res`, + `UntaggedImplIntf.res`; `tests/build_tests/super_errors_multi/fixtures/` + `Iface_unboxed_variant_mismatch`, `Iface_tag_name_mismatch`, + `Iface_variant_representation_mismatch`. +- Shaped by #6498, #6513, #6669, #7875. + +## 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. + +```rescript +@unboxed type u = | @as(undefined) U | V(string) +let x: option = Some(U) // let x = undefined; +@inline(never) let mk = (v: u): option => Some(v) +Console.log2(x == None, mk(U) == None) // true false +``` + +`lambda.ml:const_some_none_depth` returns "not a none" for every +`Const_constructor`, including one whose literal is `undefined`; that this is +the cause is conjectured, not established. + +**Overlapping instance classes follow declaration order** (#8938). + +```rescript +@unboxed type b = B(Blob.t) | F(File.t) +let f = v => switch v { | B(_) => "blob" | F(_) => "file" } +Console.log(f(F(%raw(`new File(["x"], "a.txt")`)))) // blob +``` + +A `File` is a `Blob`, and the `Blob` check runs first. + +**Typed arrays are classified by the name as written.** The typed-array types +are one type, so a value of one can be wrapped in the case of another. + +```rescript +@unboxed type ta = I8(Int8Array.t) | U8(Uint8Array.t) +let f = v => switch v { | I8(_) => "i8" | U8(_) => "u8" } +Console.log(f(I8(Uint8Array.fromArray([1])))) // u8 +``` + +**Payload-case tags are not compared across a signature.** Rule 14 compares +only the literals of nullary cases. + +```rescript +module M: { + type t = A({x: int}) | C({x: int}) + let a: t +} = { + type t = | @as("C") A({x: int}) | @as("A") C({x: int}) + let a = A({x: 1}) +} +Console.log(switch M.a { | M.A(_) => "A" | M.C(_) => "C" }) // C +``` + +## Open questions + +These are the decisions a general mechanism for recognizing payloads by class +or `typeof` kind, declared on types rather than listed in the compiler (rule +5), has to make. + +1. **Unchecked claims.** Is a declared class or kind checked against the type + it is declared on, or trusted? An unknown payload must be the only payload + case (rule 6); a recognized one may sit beside others, so a wrong claim + moves values between cases. Related: #7147. +2. **Subclasses.** How does the compiler learn that one class extends another, + and does a superclass case exclude a subclass case of the same variant + regardless of declaration order (Known bugs, #8938)? What happens to a + value upcast to the superclass and wrapped in its case (rule 7 has the same + effect for literals)? Related: #8806, #8914. +3. **Overlaps across kinds.** Rule 6 counts cases per kind and per class. + Which overlaps are rejected or ordered: a class and the object case + (`Object`), a class and the function case (`Function`), two unrelated + classes, a declared `typeof` kind and a primitive case or literal? + Related: #6123, #6207, #7289, #8251. +4. **The unchecked branch.** Rule 10 sends values outside every case to the + last case or to no arm. Is that the contract, or is the last case checked + too? Related: #6103. +5. **Realms.** `instanceof` is false across realms and overridable through + `Symbol.hasInstance`; arrays use `Array.isArray`. Is realm-local + recognition part of the contract? Related: #6103, #2117. +6. **Interfaces and functors.** Inclusion compares `@unboxed` and tags (rule + 14) but not `@notUndefined`. Does it compare declared classes and kinds? + Related: #6498, #6513, #6669. +7. **Implied `@notUndefined`.** Does a declared class or kind imply that the + type never holds `undefined` (rule 12), and how is that kept consistent + with the declared type's own structure? Related: #6222, #6233, #7458. +8. **Abbreviations.** Rule 5 reads the name as written before expanding; two + equal types classify differently (Known bugs). Does a declaration on an + abbreviation follow the same rule? Related: #6394, #7788. +9. **Globals.** Classes are read as globals. How is a class shadowed by a + local binding, imported from a module, or missing at run time handled? + Related: #8785. diff --git a/tests/build_tests/super_errors/expected/TaggedDuplicateLiteral.res.expected b/tests/build_tests/super_errors/expected/TaggedDuplicateLiteral.res.expected new file mode 100644 index 0000000000..c7143ee309 --- /dev/null +++ b/tests/build_tests/super_errors/expected/TaggedDuplicateLiteral.res.expected @@ -0,0 +1,8 @@ + + We've found a bug for you! + /.../fixtures/TaggedDuplicateLiteral.res:1:23-34 + + 1 │ type t = | @as("x") A | @as("x") B + 2 │ + + This untagged variant definition is invalid: Duplicate literal x. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/UntaggedAtMostOneString.res.expected b/tests/build_tests/super_errors/expected/UntaggedAtMostOneString.res.expected new file mode 100644 index 0000000000..785c12145a --- /dev/null +++ b/tests/build_tests/super_errors/expected/UntaggedAtMostOneString.res.expected @@ -0,0 +1,9 @@ + + We've found a bug for you! + /.../fixtures/UntaggedAtMostOneString.res:2:20-30 + + 1 │ @unboxed + 2 │ type t = A(string) | B(string) + 3 │ + + This untagged variant definition is invalid: At most one case can be a string type. \ No newline at end of file diff --git a/tests/build_tests/super_errors/fixtures/TaggedDuplicateLiteral.res b/tests/build_tests/super_errors/fixtures/TaggedDuplicateLiteral.res new file mode 100644 index 0000000000..12718cbc17 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/TaggedDuplicateLiteral.res @@ -0,0 +1 @@ +type t = | @as("x") A | @as("x") B diff --git a/tests/build_tests/super_errors/fixtures/UntaggedAtMostOneString.res b/tests/build_tests/super_errors/fixtures/UntaggedAtMostOneString.res new file mode 100644 index 0000000000..d35daded29 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/UntaggedAtMostOneString.res @@ -0,0 +1,2 @@ +@unboxed +type t = A(string) | B(string) diff --git a/tests/tests/src/untagged_variants_spec_test.mjs b/tests/tests/src/untagged_variants_spec_test.mjs new file mode 100644 index 0000000000..fe9506bae4 --- /dev/null +++ b/tests/tests/src/untagged_variants_spec_test.mjs @@ -0,0 +1,421 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + +import * as Mocha from "mocha"; +import * as Test_utils from "./test_utils.mjs"; +import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.mjs"; + +function id(x) { + return x; +} + +function mixedName(v) { + if (v === "MC") { + return "MC"; + } else if (typeof v === "number") { + return "MA"; + } else { + return "MB"; + } +} + +function kindName(v) { + switch (typeof v) { + case "string" : + return "string"; + case "number" : + return "number"; + case "bigint" : + return "bigint"; + case "boolean" : + return "bool"; + case "function" : + return "function"; + case "object" : + return "object"; + } +} + +function tupleName(v) { + if (Array.isArray(v)) { + return "array"; + } else { + return "object"; + } +} + +function aliasDictName(v) { + if (typeof v === "object" && !Array.isArray(v)) { + return "object"; + } else { + return "array"; + } +} + +function instanceName(v) { + if (v instanceof Date) { + return "Date"; + } + if (v instanceof RegExp) { + return "RegExp"; + } + if (v instanceof Promise) { + return "Promise"; + } + if (v instanceof Map) { + return "Map"; + } + if (v instanceof Set) { + return "Set"; + } + if (v instanceof WeakMap) { + return "WeakMap"; + } + if (v instanceof WeakSet) { + return "WeakSet"; + } + if (v instanceof ArrayBuffer) { + return "ArrayBuffer"; + } + if (v instanceof DataView) { + return "DataView"; + } + if (v instanceof Float64Array) { + return "Float64Array"; + } + switch (typeof v) { + case "string" : + return "string"; + } +} + +function unknownName(v) { + if (v === null || v === "none") { + if (v === null) { + return "null"; + } else { + return "none"; + } + } else { + return "list"; + } +} + +function overlapStringName(v) { + if (v === "a") { + return "LitA"; + } else { + return "AnyString"; + } +} + +function overlapNumberName(v) { + if (v === 1) { + return "LitOne"; + } else { + return "AnyInt"; + } +} + +function overlapBoolName(v) { + if (v === true) { + return "LitTrue"; + } else { + return "AnyBool"; + } +} + +function overlapNullName(v) { + if (v === null) { + return "LitNull"; + } else { + return "AnyObject"; + } +} + +function objectFirstName(v) { + if (typeof v === "object" && !(v instanceof Date)) { + return "object"; + } else { + return "Date"; + } +} + +function intOrStringName(v) { + if (typeof v === "number") { + return "int"; + } else { + return "string"; + } +} + +function guardedName(v) { + if (v === null) { + return "null"; + } else if (Array.isArray(v)) { + return "array"; + } else { + return "object"; + } +} + +function guardedObjectOnly(v) { + if (v === null || Array.isArray(v)) { + return "other"; + } else { + return "object"; + } +} + +function twoInstancesName(v) { + if (v instanceof Date) { + return "Date"; + } else { + return "RegExp"; + } +} + +function promiseOrFloatName(v) { + if (v instanceof Promise) { + return "promise"; + } else { + return "float"; + } +} + +function someWithNull(v) { + return v; +} + +function someWithUndefined(v) { + return Primitive_option.some(v); +} + +function someWithOpaque(v) { + return Primitive_option.some(v); +} + +function someWithVariable(v) { + return Primitive_option.some(v); +} + +function someWithBlob(v) { + return Primitive_option.some(v); +} + +function someWithDate(v) { + return v; +} + +function someWithRecord(v) { + return v; +} + +function someWithInlineRecord(v) { + return Primitive_option.some(v); +} + +function someWithUnboxedOption(v) { + return Primitive_option.some(v); +} + +function someWithUnboxedString(v) { + return v; +} + +function someWithDefined(v) { + return v; +} + +Mocha.describe("Untagged_variants_spec_test", () => { + Mocha.test("rule 1: @unboxed on one constructor of a tagged variant", () => { + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 224, characters 7-14", id(3), 3); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 225, characters 7-14", id({ + TAG: "MB", + x: 1 + }), { + TAG: "MB", + x: 1 + }); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 226, characters 7-14", id("MC"), "MC"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 227, characters 7-14", mixedName(id(3)), "MA"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 228, characters 7-14", mixedName(id({ + TAG: "MB", + x: 1 + })), "MB"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 229, characters 7-14", mixedName(id("MC")), "MC"); + }); + Mocha.test("rule 2: representation of each case", () => { + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 233, characters 7-14", id("Named"), "Named"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 234, characters 7-14", id("s"), "s"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 235, characters 7-14", id(1), 1); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 236, characters 7-14", id(2.5), 2.5); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 237, characters 7-14", id(3n), 3n); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 238, characters 7-14", id(true), true); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 239, characters 7-14", id(null), null); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 240, characters 7-14", id(undefined), undefined); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 241, characters 7-14", id({ + x: 1 + }), { + x: 1 + }); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 242, characters 7-14", id({ + x: 1 + }), { + x: 1 + }); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 243, characters 7-14", id("v"), "v"); + }); + Mocha.test("rule 4: typeof kinds", () => { + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 247, characters 7-14", kindName(id("a")), "string"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 248, characters 7-14", kindName(id(1.5)), "number"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 249, characters 7-14", kindName(id(1n)), "bigint"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 250, characters 7-14", kindName(id(false)), "bool"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 251, characters 7-14", kindName(id(x => x)), "function"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 252, characters 7-14", kindName(id({ + x: 1 + })), "object"); + }); + Mocha.test("rule 4: tuples are array cases, dicts are object cases", () => { + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 256, characters 7-14", tupleName(id([ + 1, + "a" + ])), "array"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 257, characters 7-14", tupleName(id({ + a: 1 + })), "object"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 258, characters 7-14", aliasDictName(id({ + a: 1 + })), "object"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 259, characters 7-14", aliasDictName(id([1])), "array"); + }); + Mocha.test("rules 4 and 5: instance cases", () => { + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 263, characters 7-14", instanceName(id(new Date())), "Date"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 264, characters 7-14", instanceName(id(/a/)), "RegExp"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 265, characters 7-14", instanceName(id(Promise.resolve(1))), "Promise"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 266, characters 7-14", instanceName(id(new Map())), "Map"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 267, characters 7-14", instanceName(id(new Set())), "Set"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 268, characters 7-14", instanceName(id(new WeakMap())), "WeakMap"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 269, characters 7-14", instanceName(id(new WeakSet())), "WeakSet"); + let buffer = new ArrayBuffer(8); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 271, characters 7-14", instanceName(id(buffer)), "ArrayBuffer"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 272, characters 7-14", instanceName(id(new DataView(buffer))), "DataView"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 273, characters 7-14", instanceName(id(new Float64Array([1.0]))), "Float64Array"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 274, characters 7-14", instanceName(id("a")), "string"); + }); + Mocha.test("rule 4: the unknown case receives every non-literal value", () => { + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 278, characters 7-14", unknownName(id(null)), "null"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 279, characters 7-14", unknownName(id("none")), "none"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 280, characters 7-14", unknownName(id({hd: 1, tl: /* [] */0})), "list"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 281, characters 7-14", unknownName(id(/* [] */0)), "list"); + }); + Mocha.test("rule 7: a payload value equal to a literal is that literal's case", () => { + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 285, characters 7-14", overlapStringName(id("a")), "LitA"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 286, characters 7-14", overlapStringName(id("b")), "AnyString"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 287, characters 7-14", overlapNumberName(id(1)), "LitOne"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 288, characters 7-14", overlapNumberName(id(2)), "AnyInt"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 289, characters 7-14", overlapBoolName(id(true)), "LitTrue"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 290, characters 7-14", overlapBoolName(id(false)), "AnyBool"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 291, characters 7-14", overlapNullName(id(null)), "LitNull"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 292, characters 7-14", overlapNullName(id({ + x: 1 + })), "AnyObject"); + }); + Mocha.test("rule 8: instance checks precede typeof checks", () => { + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 296, characters 7-14", objectFirstName(id(new Date())), "Date"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 297, characters 7-14", objectFirstName(id({ + x: 1 + })), "object"); + }); + Mocha.test("rule 8: an int case receives any number", () => { + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 301, characters 7-14", intOrStringName(id(1)), "int"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 302, characters 7-14", intOrStringName(id(1.5)), "int"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 303, characters 7-14", intOrStringName(id("1")), "string"); + }); + Mocha.test("rule 9: null and arrays are not objects", () => { + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 307, characters 7-14", guardedName(id([])), "array"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 308, characters 7-14", guardedName(id(null)), "null"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 309, characters 7-14", guardedName(id({ + x: 1 + })), "object"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 310, characters 7-14", guardedObjectOnly(id([])), "other"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 311, characters 7-14", guardedObjectOnly(id(null)), "other"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 312, characters 7-14", guardedObjectOnly(id({ + x: 1 + })), "object"); + }); + Mocha.test("rule 10: a value outside every case lands in the unchecked branch", () => { + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 316, characters 7-14", intOrStringName(true), "string"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 317, characters 7-14", twoInstancesName({ + x: 1 + }), "RegExp"); + let thenable = ({then: (resolve) => resolve(1)}); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 319, characters 7-14", promiseOrFloatName(thenable), "float"); + }); + Mocha.test("rule 12: option of an untagged variant", () => { + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 323, characters 7-14", someWithNull(null), null); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 324, characters 7-14", someWithNull(null) === undefined, false); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 325, characters 7-14", someWithUndefined(id(undefined)) === undefined, false); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 326, characters 7-14", someWithUndefined("a"), "a"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 329, characters 7-14", someWithOpaque(undefined) === undefined, false); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 330, characters 7-14", someWithVariable(undefined) === undefined, false); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 331, characters 7-14", someWithBlob(undefined) === undefined, false); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 332, characters 7-14", someWithDate(undefined) === undefined, true); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 333, characters 7-14", someWithRecord(undefined) === undefined, true); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 334, characters 7-14", someWithInlineRecord(undefined) === undefined, false); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 335, characters 7-14", someWithUnboxedOption(undefined) === undefined, false); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 336, characters 7-14", someWithUnboxedString(undefined) === undefined, true); + }); + Mocha.test("rule 13: @notUndefined unboxes option of a variant holding the type", () => { + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 340, characters 7-14", someWithDefined(undefined) === undefined, true); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 341, characters 7-14", someWithDefined(5), 5); + }); + Mocha.test("rule 14: @tag and @as on a tagged variant", () => { + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 345, characters 7-14", id({ + kind: "TA", + x: 1 + }), { + kind: "TA", + x: 1 + }); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 346, characters 7-14", id({ + kind: "bee", + y: 1 + }), { + kind: "bee", + y: 1 + }); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 347, characters 7-14", id("TC"), "TC"); + }); +}); + +export { + id, + mixedName, + kindName, + tupleName, + aliasDictName, + instanceName, + unknownName, + overlapStringName, + overlapNumberName, + overlapBoolName, + overlapNullName, + objectFirstName, + intOrStringName, + guardedName, + guardedObjectOnly, + twoInstancesName, + promiseOrFloatName, + someWithNull, + someWithUndefined, + someWithOpaque, + someWithVariable, + someWithBlob, + someWithDate, + someWithRecord, + someWithInlineRecord, + someWithUnboxedOption, + someWithUnboxedString, + someWithDefined, +} +/* Not a pure module */ diff --git a/tests/tests/src/untagged_variants_spec_test.res b/tests/tests/src/untagged_variants_spec_test.res new file mode 100644 index 0000000000..9976b193c9 --- /dev/null +++ b/tests/tests/src/untagged_variants_spec_test.res @@ -0,0 +1,349 @@ +open Mocha +open Test_utils + +/*** +Pins the rules stated in compiler/ml/UNTAGGED_VARIANTS.md. Each block names +the rule it pins. Values are passed through [id], whose [@inline(never)] keeps +them away from constant folding, so the assertions observe the generated +dispatch. [raw] injects a JavaScript value a case's type does not describe; +those assertions show how recognition treats values outside every case. +*/ + +@inline(never) let id = x => x +external raw: 'a => 'b = "%identity" + +// Rule 1: `@unboxed` and `@tag` on the type apply to every constructor; +// `@unboxed` on one constructor of a tagged variant applies to that one. +type mixed = | @unboxed MA(int) | MB({x: int}) | MC +let mixedName = v => + switch v { + | MA(_) => "MA" + | MB(_) => "MB" + | MC => "MC" + } + +// Rule 2: a nullary case is its literal; a payload case is its payload. +@unboxed +type literals = + | Named + | @as("s") Str + | @as(1) One + | @as(2.5) Half + | @as(3n) Three + | @as(true) True + | @as(null) Null + | @as(undefined) Undefined + | Payload({x: int}) +@unboxed @tag("kind") type tagIgnored = TI({x: int}) | TS(string) +@unboxed type asIgnored = | @as("x") AsX(string) | AsY(int) + +// Rule 4: payload kinds, one case of each kind recognized by `typeof`. +@unboxed +type kinds = + | KString(string) + | KNumber(float) + | KBigint(bigint) + | KBool(bool) + | KFunction(int => int) + | KObject({x: int}) +let kindName = v => + switch v { + | KString(_) => "string" + | KNumber(_) => "number" + | KBigint(_) => "bigint" + | KBool(_) => "bool" + | KFunction(_) => "function" + | KObject(_) => "object" + } + +// Rule 4: arrays, tuples and dicts. +@unboxed type tuples = Tuple((int, string)) | TDict(dict) +let tupleName = v => + switch v { + | Tuple(_) => "array" + | TDict(_) => "object" + } +type myDict = Dict.t +@unboxed type aliasDict = ADict(myDict) | AArray(array) +let aliasDictName = v => + switch v { + | ADict(_) => "object" + | AArray(_) => "array" + } + +// Rules 4 and 5: instance cases, written directly and through an alias. +type myDate = Date.t +@unboxed +type instances = + | IDate(myDate) + | IRegExp(RegExp.t) + | IPromise(promise) + | IMap(Map.t) + | ISet(Set.t) + | IWeakMap(WeakMap.t<{"a": int}, int>) + | IWeakSet(WeakSet.t<{"a": int}>) + | IArrayBuffer(ArrayBuffer.t) + | IDataView(DataView.t) + | IFloat64(Float64Array.t) + | IString(string) +let instanceName = v => + switch v { + | IDate(_) => "Date" + | IRegExp(_) => "RegExp" + | IPromise(_) => "Promise" + | IMap(_) => "Map" + | ISet(_) => "Set" + | IWeakMap(_) => "WeakMap" + | IWeakSet(_) => "WeakSet" + | IArrayBuffer(_) => "ArrayBuffer" + | IDataView(_) => "DataView" + | IFloat64(_) => "Float64Array" + | IString(_) => "string" + } + +// Rule 4: a payload of no recognized kind is the only payload case. +@unboxed type unknown = | @as(null) UNull | @as("none") UNone | UList(list) +let unknownName = v => + switch v { + | UNull => "null" + | UNone => "none" + | UList(_) => "list" + } + +// Rule 7: literal cases are tested first. +@unboxed type overlapString = | @as("a") LitA | AnyString(string) +let overlapStringName = v => + switch v { + | LitA => "LitA" + | AnyString(_) => "AnyString" + } +@unboxed type overlapNumber = | @as(1) LitOne | AnyInt(int) +let overlapNumberName = v => + switch v { + | LitOne => "LitOne" + | AnyInt(_) => "AnyInt" + } +@unboxed type overlapBool = | @as(true) LitTrue | AnyBool(bool) +let overlapBoolName = v => + switch v { + | LitTrue => "LitTrue" + | AnyBool(_) => "AnyBool" + } +@unboxed type overlapNull = | @as(null) LitNull | AnyObject({x: int}) +let overlapNullName = v => + switch v { + | LitNull => "LitNull" + | AnyObject(_) => "AnyObject" + } + +// Rule 8: instance checks precede the `typeof` checks. +@unboxed type objectFirst = OObject({x: int}) | ODate(Date.t) +let objectFirstName = v => + switch v { + | OObject(_) => "object" + | ODate(_) => "Date" + } + +// Rule 8: an int case is recognized as any number. +@unboxed type intOrString = I(int) | S(string) +let intOrStringName = v => + switch v { + | I(_) => "int" + | S(_) => "string" + } + +// Rule 9: `null` and arrays do not reach the object case. +@unboxed type guarded = GArray(array) | GObject({x: int}) | @as(null) GNull +let guardedName = v => + switch v { + | GArray(_) => "array" + | GObject(_) => "object" + | GNull => "null" + } +let guardedObjectOnly = v => + switch v { + | GObject(_) => "object" + | _ => "other" + } + +// Rule 10: in a two-way test the second case has no check. +@unboxed type twoInstances = TDate(Date.t) | TRegExp(RegExp.t) +let twoInstancesName = v => + switch v { + | TDate(_) => "Date" + | TRegExp(_) => "RegExp" + } +@unboxed type promiseOrFloat = PPromise(promise) | PFloat(float) +let promiseOrFloatName = v => + switch v { + | PPromise(_) => "promise" + | PFloat(_) => "float" + } + +// Rule 12: `Some(v)` is `v` unless the variant can hold `undefined`. +@unboxed type withNull = | @as(null) WNull | WString(string) +@inline(never) let someWithNull = (v: withNull): option => Some(v) +@unboxed type withUndefined = | @as(undefined) WUndefined | WUString(string) +@inline(never) let someWithUndefined = (v: withUndefined): option => Some(v) +type opaque +@unboxed type withOpaque = | @as("n") ONone | OPaque(opaque) +@inline(never) let someWithOpaque = (v: withOpaque): option => Some(v) +@unboxed type withVariable<'a> = | @as("n") VNone | Var('a) +@inline(never) let someWithVariable = (v: withVariable): option> => Some(v) +@unboxed type withBlob = BBlob(Blob.t) | BString(string) +@inline(never) let someWithBlob = (v: withBlob): option => Some(v) +@unboxed type withDate = DDate(Date.t) | DString(string) +@inline(never) let someWithDate = (v: withDate): option => Some(v) +// A record payload holds `undefined` only through the field of an `@unboxed` +// record or of a one-field inline record. +type record = {y: option} +@unboxed type withRecord = RRecord(record) | RString(string) +@inline(never) let someWithRecord = (v: withRecord): option => Some(v) +@unboxed type withInlineRecord = INone | IRecord({x: option}) +@inline(never) +let someWithInlineRecord = (v: withInlineRecord): option => Some(v) +@unboxed type unboxedOption = {x: option} +@unboxed type withUnboxedOption = | @as("n") UONone | UOption(unboxedOption) +@inline(never) +let someWithUnboxedOption = (v: withUnboxedOption): option => Some(v) +@unboxed type unboxedString = {s: string} +@unboxed type withUnboxedString = | @as("n") USNone | UString(unboxedString) +@inline(never) +let someWithUnboxedString = (v: withUnboxedString): option => Some(v) + +// Rule 13: `@notUndefined` on an abstract type. +@notUndefined type defined +@unboxed type withDefined = | @as("n") DNone | Defined(defined) +@inline(never) let someWithDefined = (v: withDefined): option => Some(v) + +// Rule 14: `@tag` names the tag field of a tagged variant. +@tag("kind") type tagged = TA({x: int}) | @as("bee") TB({y: int}) | TC + +describe(__MODULE__, () => { + test("rule 1: @unboxed on one constructor of a tagged variant", () => { + eq(__LOC__, raw(id(MA(3))), 3) + eq(__LOC__, raw(id(MB({x: 1}))), {"TAG": "MB", "x": 1}) + eq(__LOC__, raw(id(MC)), "MC") + eq(__LOC__, mixedName(id(MA(3))), "MA") + eq(__LOC__, mixedName(id(MB({x: 1}))), "MB") + eq(__LOC__, mixedName(id(MC)), "MC") + }) + + test("rule 2: representation of each case", () => { + eq(__LOC__, raw(id(Named)), "Named") + eq(__LOC__, raw(id(Str)), "s") + eq(__LOC__, raw(id(One)), 1) + eq(__LOC__, raw(id(Half)), 2.5) + eq(__LOC__, raw(id(Three)), 3n) + eq(__LOC__, raw(id(True)), true) + eq(__LOC__, raw(id(Null)), Nullable.null) + eq(__LOC__, raw(id(Undefined)), Nullable.undefined) + eq(__LOC__, raw(id(Payload({x: 1}))), {"x": 1}) + eq(__LOC__, raw(id(TI({x: 1}))), {"x": 1}) + eq(__LOC__, raw(id(AsX("v"))), "v") + }) + + test("rule 4: typeof kinds", () => { + eq(__LOC__, kindName(id(KString("a"))), "string") + eq(__LOC__, kindName(id(KNumber(1.5))), "number") + eq(__LOC__, kindName(id(KBigint(1n))), "bigint") + eq(__LOC__, kindName(id(KBool(false))), "bool") + eq(__LOC__, kindName(id(KFunction(x => x))), "function") + eq(__LOC__, kindName(id(KObject({x: 1}))), "object") + }) + + test("rule 4: tuples are array cases, dicts are object cases", () => { + eq(__LOC__, tupleName(id(Tuple((1, "a")))), "array") + eq(__LOC__, tupleName(id(TDict(dict{"a": 1}))), "object") + eq(__LOC__, aliasDictName(id(ADict(dict{"a": 1}))), "object") + eq(__LOC__, aliasDictName(id(AArray([1]))), "array") + }) + + test("rules 4 and 5: instance cases", () => { + eq(__LOC__, instanceName(id(IDate(Date.make()))), "Date") + eq(__LOC__, instanceName(id(IRegExp(/a/))), "RegExp") + eq(__LOC__, instanceName(id(IPromise(Promise.resolve(1)))), "Promise") + eq(__LOC__, instanceName(id(IMap(Map.make()))), "Map") + eq(__LOC__, instanceName(id(ISet(Set.make()))), "Set") + eq(__LOC__, instanceName(id(IWeakMap(WeakMap.make()))), "WeakMap") + eq(__LOC__, instanceName(id(IWeakSet(WeakSet.make()))), "WeakSet") + let buffer = ArrayBuffer.make(8) + eq(__LOC__, instanceName(id(IArrayBuffer(buffer))), "ArrayBuffer") + eq(__LOC__, instanceName(id(IDataView(DataView.fromBuffer(buffer)))), "DataView") + eq(__LOC__, instanceName(id(IFloat64(Float64Array.fromArray([1.0])))), "Float64Array") + eq(__LOC__, instanceName(id(IString("a"))), "string") + }) + + test("rule 4: the unknown case receives every non-literal value", () => { + eq(__LOC__, unknownName(id(UNull)), "null") + eq(__LOC__, unknownName(id(UNone)), "none") + eq(__LOC__, unknownName(id(UList(list{1}))), "list") + eq(__LOC__, unknownName(id(UList(list{}))), "list") + }) + + test("rule 7: a payload value equal to a literal is that literal's case", () => { + eq(__LOC__, overlapStringName(id(AnyString("a"))), "LitA") + eq(__LOC__, overlapStringName(id(AnyString("b"))), "AnyString") + eq(__LOC__, overlapNumberName(id(AnyInt(1))), "LitOne") + eq(__LOC__, overlapNumberName(id(AnyInt(2))), "AnyInt") + eq(__LOC__, overlapBoolName(id(AnyBool(true))), "LitTrue") + eq(__LOC__, overlapBoolName(id(AnyBool(false))), "AnyBool") + eq(__LOC__, overlapNullName(id(LitNull)), "LitNull") + eq(__LOC__, overlapNullName(id(AnyObject({x: 1}))), "AnyObject") + }) + + test("rule 8: instance checks precede typeof checks", () => { + eq(__LOC__, objectFirstName(id(ODate(Date.make()))), "Date") + eq(__LOC__, objectFirstName(id(OObject({x: 1}))), "object") + }) + + test("rule 8: an int case receives any number", () => { + eq(__LOC__, intOrStringName(id(I(1))), "int") + eq(__LOC__, intOrStringName(raw(id(1.5))), "int") + eq(__LOC__, intOrStringName(id(S("1"))), "string") + }) + + test("rule 9: null and arrays are not objects", () => { + eq(__LOC__, guardedName(id(GArray([]))), "array") + eq(__LOC__, guardedName(id(GNull)), "null") + eq(__LOC__, guardedName(id(GObject({x: 1}))), "object") + eq(__LOC__, guardedObjectOnly(id(GArray([]))), "other") + eq(__LOC__, guardedObjectOnly(id(GNull)), "other") + eq(__LOC__, guardedObjectOnly(id(GObject({x: 1}))), "object") + }) + + test("rule 10: a value outside every case lands in the unchecked branch", () => { + eq(__LOC__, intOrStringName(raw(true)), "string") + eq(__LOC__, twoInstancesName(raw({"x": 1})), "RegExp") + let thenable = %raw(`{then: (resolve) => resolve(1)}`) + eq(__LOC__, promiseOrFloatName(raw(thenable)), "float") + }) + + test("rule 12: option of an untagged variant", () => { + eq(__LOC__, raw(someWithNull(WNull)), Nullable.null) + eq(__LOC__, someWithNull(WNull) == None, false) + eq(__LOC__, someWithUndefined(id(WUndefined)) == None, false) + eq(__LOC__, raw(someWithUndefined(WUString("a"))), "a") + // An undefined payload shows whether `Some` boxes: a boxed `Some` stays + // distinct from `None`, an unboxed one does not. + eq(__LOC__, someWithOpaque(OPaque(raw(Nullable.undefined))) == None, false) + eq(__LOC__, someWithVariable(Var(raw(Nullable.undefined))) == None, false) + eq(__LOC__, someWithBlob(BBlob(raw(Nullable.undefined))) == None, false) + eq(__LOC__, someWithDate(DDate(raw(Nullable.undefined))) == None, true) + eq(__LOC__, someWithRecord(RRecord(raw(Nullable.undefined))) == None, true) + eq(__LOC__, someWithInlineRecord(raw(Nullable.undefined)) == None, false) + eq(__LOC__, someWithUnboxedOption(UOption(raw(Nullable.undefined))) == None, false) + eq(__LOC__, someWithUnboxedString(UString(raw(Nullable.undefined))) == None, true) + }) + + test("rule 13: @notUndefined unboxes option of a variant holding the type", () => { + eq(__LOC__, someWithDefined(Defined(raw(Nullable.undefined))) == None, true) + eq(__LOC__, raw(someWithDefined(Defined(raw(5)))), 5) + }) + + test("rule 14: @tag and @as on a tagged variant", () => { + eq(__LOC__, raw(id(TA({x: 1}))), {"kind": "TA", "x": 1}) + eq(__LOC__, raw(id(TB({y: 1}))), {"kind": "bee", "y": 1}) + eq(__LOC__, raw(id(TC)), "TC") + }) +})