From 55b7297f808aae2136fa39dd937251817fa288f1 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Thu, 22 Oct 2026 19:14:26 +0000 Subject: [PATCH 1/3] Test primitive untagged cases before class checks Why: A `switch` on an untagged variant ran its class checks before the `typeof` switch, so strings and numbers reached `v instanceof C` too. A class missing where the code runs, such as a user class or a newer `Intl` class, then threw for every value, even for the primitive cases. When a variant has class cases, the arms of primitive kinds (string, number, bigint, boolean, symbol) are now tested first, in an outer `switch (typeof v)` whose default holds the class checks and the remaining `object`/`function` arms. A class instance is never a primitive, so no value changes arm. Primitive and other arms no longer share a body through fall-through, since they sit in different switches. Variants with only an array case keep the old form, since `Array.isArray` cannot throw. JS modified (a class case beside primitive cases): Example: @instanceOf("Nope") type nope @unboxed type t = N(nope) | S(string) | F(float) | A(array) Before (js): if (v instanceof Nope) { return "N"; } if (Array.isArray(v)) { return "A"; } switch (typeof v) { case "string" : return v; case "number" : return "F"; } After (js): switch (typeof v) { case "string" : return v; case "number" : return "F"; default: if (v instanceof Nope) { return "N"; } if (Array.isArray(v)) { return "A"; } } Review: What does it cost? A second `switch (typeof v)` when there are object or function arms too, and a body shared by a primitive and an object arm is emitted twice (`awaitUser` in `UntaggedVariants.mjs`). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Fc7LEuyEfynjaKCVEBPmTF Signed-off-by: Christoph Knittel --- changes/untagged-typeof-first.polish.md | 1 + compiler/core/lam_compile.ml | 44 ++- compiler/ml/UntaggedVariants.md | 24 +- tests/tests/src/UntaggedVariants.mjs | 298 ++++++++++-------- ...tagged_runtime_annotations_shadow_test.mjs | 13 +- .../src/untagged_runtime_annotations_test.mjs | 129 +++++--- .../src/untagged_runtime_annotations_test.res | 39 +++ .../tests/src/untagged_variants_spec_test.mjs | 61 ++-- 8 files changed, 388 insertions(+), 221 deletions(-) create mode 100644 changes/untagged-typeof-first.polish.md diff --git a/changes/untagged-typeof-first.polish.md b/changes/untagged-typeof-first.polish.md new file mode 100644 index 0000000000..4aaa306ae3 --- /dev/null +++ b/changes/untagged-typeof-first.polish.md @@ -0,0 +1 @@ +- Test the primitive cases of an untagged variant before its class checks, so that strings and numbers skip `instanceof` and a class missing at run time no longer throws for them. diff --git a/compiler/core/lam_compile.ml b/compiler/core/lam_compile.ml index 337df8fcb6..45ef67e1be 100644 --- a/compiler/core/lam_compile.ml +++ b/compiler/core/lam_compile.ml @@ -878,10 +878,34 @@ let compile output_prefix = | _ -> false in let clause_is_not_typeof (tag, _) = tag_is_not_typeof tag in + let tag_is_primitive = function + | Variant_runtime.Payload_shape + (NumberType | StringType | BigintType | BooleanType | SymbolType) -> + true + | _ -> false + in + (* Only class checks can throw (when the class is missing), so arrays, + recognized by [Array.isArray], keep the shorter form *) + let has_class_case = + List.exists + (function + | Variant_runtime.InstanceType (Class _) -> true + | _ -> false) + block_cases + in let switch ?default ?declaration e clauses = let not_typeof_clauses, typeof_clauses = List.partition clause_is_not_typeof clauses in + (* A class instance is never a primitive, so the primitive kinds can be + tested first: their values then skip the instance checks, which + throw when a class is missing at run time *) + let primitive_clauses, typeof_clauses = + if (not has_class_case) || not_typeof_clauses = [] then + ([], typeof_clauses) + else + List.partition (fun (tag, _) -> tag_is_primitive tag) typeof_clauses + in let has_object_typeof = List.exists (function @@ -927,7 +951,12 @@ let compile output_prefix = ~else_:[build_if_chain rest] | _ -> ( let typeof_switch () = - S.string_switch ?default ?declaration (E.typeof e) typeof_clauses + match (primitive_clauses, typeof_clauses) with + | _ :: _, [] -> S.block (Option.value default ~default:[]) + | _ :: _, _ :: _ -> + S.string_switch ?default (E.typeof e) typeof_clauses + | [], _ -> + S.string_switch ?default ?declaration (E.typeof e) typeof_clauses in let guards = (if needs_null_guard then [E.is_null e] else []) @@ -946,11 +975,18 @@ let compile output_prefix = | Some guard, None -> S.if_ (E.not guard) [typeof_switch ()] | None, _ -> typeof_switch ()) in - build_if_chain not_typeof_clauses + match primitive_clauses with + | [] -> build_if_chain not_typeof_clauses + | _ -> + S.string_switch ?declaration (E.typeof e) primitive_clauses + ~default:[build_if_chain not_typeof_clauses] in let merge_cases tag1 tag2 = - (* only merge typeof cases, as instanceof cases are pulled out into if-then-else *) - not (tag_is_not_typeof tag1 || tag_is_not_typeof tag2) + (* only merge typeof cases, as instanceof cases are pulled out into + if-then-else. With instance cases, the primitive kinds are tested in + a switch of their own, so they share bodies only among themselves *) + (not (tag_is_not_typeof tag1 || tag_is_not_typeof tag2)) + && ((not has_class_case) || tag_is_primitive tag1 = tag_is_primitive tag2) in cases |> compile_general_cases ~make_exp:E.tag_type ~eq_exp:mk_eq ~cxt ~switch diff --git a/compiler/ml/UntaggedVariants.md b/compiler/ml/UntaggedVariants.md index 01929fe970..162eb029a5 100644 --- a/compiler/ml/UntaggedVariants.md +++ b/compiler/ml/UntaggedVariants.md @@ -298,7 +298,19 @@ The payload arms compile in one of two forms. followed by `!(x instanceof C)` for every class case. - **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. + arms. When a variant has class cases (not only arrays), the arms of + primitive kinds (string, number, bigint, boolean, symbol) come first, in an + outer `switch (typeof x)` whose default holds the rest; a class instance is + never a primitive, so this changes no outcome: + + ```js + switch (typeof x) { + case "string": ... + default: + if (x instanceof Date) { ... } + else switch (typeof x) { case "object": ... } + } + ``` In both forms a value of an instance case never lands in the object or function case. A class check is followed by `!(x instanceof D)` for every @@ -367,8 +379,9 @@ Recognition trusts the type. A value that belongs to no case is not rejected: A value from another JavaScript realm fails every `instanceof` check and is recognized the same way (by the semantics of `instanceof`; not tested): recognition is local to a realm. A class that does not exist where the -`switch` runs makes its check throw, for every value: the instance checks -precede the `typeof` checks, so primitives reach them too. +`switch` runs makes its check throw for every value that reaches it: in the +`switch` form, objects, functions and primitives outside every case; in the +two-arm form, every value when the class arm is declared first. - Implementation: `lam_compile.ml:compile_general_cases` (a complete match emits no check for its last clause). @@ -545,6 +558,5 @@ had to answer, and the answers of rule 5: 9. **Globals.** A shadowed class is read through `globalThis`. A missing class throws when the check runs (rule 10). -Still open: a class imported from a module cannot be named; the `typeof` -checks could precede the instance checks, so that a missing class affects -only objects; and the typed arrays share one type (#8955). +Still open: a class imported from a module cannot be named, and the typed +arrays share one type (#8955). diff --git a/tests/tests/src/UntaggedVariants.mjs b/tests/tests/src/UntaggedVariants.mjs index df8f7b89be..e48b34a26e 100644 --- a/tests/tests/src/UntaggedVariants.mjs +++ b/tests/tests/src/UntaggedVariants.mjs @@ -437,25 +437,32 @@ let ComplexPattern = { }; async function getUserName(u) { - if (u instanceof Promise) { - return (await u).name; - } switch (typeof u) { - case "object" : - return u.name; case "string" : return u; + default: + if (u instanceof Promise) { + return (await u).name; + } + switch (typeof u) { + case "object" : + return u.name; + } } } async function awaitUser(u) { - if (u instanceof Promise) { - return (await u).name; - } switch (typeof u) { - case "object" : case "string" : return "dummy"; + default: + if (u instanceof Promise) { + return (await u).name; + } + switch (typeof u) { + case "object" : + return "dummy"; + } } } @@ -473,21 +480,24 @@ async function classify$10(a) { console.log(12); return; } else { - if (Array.isArray(a)) { - console.log(a.join("-")); - return; - } - if (a instanceof Promise) { - console.log(await a); - return; - } switch (typeof a) { case "string" : console.log(a); return; - case "object" : - console.log(a.userName); - return; + default: + if (Array.isArray(a)) { + console.log(a.join("-")); + return; + } + if (a instanceof Promise) { + console.log(await a); + return; + } + switch (typeof a) { + case "object" : + console.log(a.userName); + return; + } } } } @@ -497,105 +507,108 @@ let Arr = { }; async function classifyAll(t) { - if (Array.isArray(t)) { - console.log(t.join("-")); - return; - } - if (t instanceof Promise) { - console.log(await t); - return; - } - if (t instanceof Date) { - console.log(t.toString()); - return; - } - if (t instanceof RegExp) { - console.log(t.test("test")); - return; - } - if (t instanceof File) { - console.log(t.name); - return; - } - if (t instanceof Blob && !(t instanceof File)) { - console.log(t.size); - return; - } - if (t instanceof ArrayBuffer) { - console.log("ArrayBuffer"); - return; - } - if (t instanceof Int8Array) { - console.log("Int8Array"); - return; - } - if (t instanceof Int16Array) { - console.log("Int16Array"); - return; - } - if (t instanceof Int32Array) { - console.log("Int32Array"); - return; - } - if (t instanceof Uint8Array) { - console.log("Uint8Array"); - return; - } - if (t instanceof Uint8ClampedArray) { - console.log("Uint8ClampedArray"); - return; - } - if (t instanceof Uint16Array) { - console.log("Uint16Array"); - return; - } - if (t instanceof Uint32Array) { - console.log("Uint32Array"); - return; - } - if (t instanceof Float32Array) { - console.log("Float32Array"); - return; - } - if (t instanceof Float64Array) { - console.log("Float64Array"); - return; - } - if (t instanceof BigInt64Array) { - console.log("BigInt64Array"); - return; - } - if (t instanceof BigUint64Array) { - console.log("BigUint64Array"); - return; - } - if (t instanceof DataView) { - console.log("DataView"); - return; - } - if (t instanceof Set) { - console.log("Set"); - return; - } - if (t instanceof Map) { - console.log("Map"); - return; - } - if (t instanceof WeakSet) { - console.log("WeakSet"); - return; - } - if (t instanceof WeakMap) { - console.log("WeakMap"); - return; - } switch (typeof t) { case "string" : console.log(t); return; - case "object" : - console.log(t.userName); - return; + default: + if (Array.isArray(t)) { + console.log(t.join("-")); + return; + } + if (t instanceof Promise) { + console.log(await t); + return; + } + if (t instanceof Date) { + console.log(t.toString()); + return; + } + if (t instanceof RegExp) { + console.log(t.test("test")); + return; + } + if (t instanceof File) { + console.log(t.name); + return; + } + if (t instanceof Blob && !(t instanceof File)) { + console.log(t.size); + return; + } + if (t instanceof ArrayBuffer) { + console.log("ArrayBuffer"); + return; + } + if (t instanceof Int8Array) { + console.log("Int8Array"); + return; + } + if (t instanceof Int16Array) { + console.log("Int16Array"); + return; + } + if (t instanceof Int32Array) { + console.log("Int32Array"); + return; + } + if (t instanceof Uint8Array) { + console.log("Uint8Array"); + return; + } + if (t instanceof Uint8ClampedArray) { + console.log("Uint8ClampedArray"); + return; + } + if (t instanceof Uint16Array) { + console.log("Uint16Array"); + return; + } + if (t instanceof Uint32Array) { + console.log("Uint32Array"); + return; + } + if (t instanceof Float32Array) { + console.log("Float32Array"); + return; + } + if (t instanceof Float64Array) { + console.log("Float64Array"); + return; + } + if (t instanceof BigInt64Array) { + console.log("BigInt64Array"); + return; + } + if (t instanceof BigUint64Array) { + console.log("BigUint64Array"); + return; + } + if (t instanceof DataView) { + console.log("DataView"); + return; + } + if (t instanceof Set) { + console.log("Set"); + return; + } + if (t instanceof Map) { + console.log("Map"); + return; + } + if (t instanceof WeakSet) { + console.log("WeakSet"); + return; + } + if (t instanceof WeakMap) { + console.log("WeakMap"); + return; + } + switch (typeof t) { + case "object" : + console.log(t.userName); + return; + } } } @@ -631,16 +644,20 @@ function shareEquivalentStringActions(x) { } function can_merge(x) { - if (Array.isArray(x)) { - return "do not merge"; - } - if (x instanceof Date) { - return "do not merge"; - } switch (typeof x) { case "boolean" : - case "object" : return "merge"; + default: + if (Array.isArray(x)) { + return "do not merge"; + } + if (x instanceof Date) { + return "do not merge"; + } + switch (typeof x) { + case "object" : + return "merge"; + } } } @@ -665,26 +682,29 @@ function shareAlphaEquivalentStringActions(value) { } function preserveDiscriminatorGroups(x) { - if (Array.isArray(x)) { - while (keepGoing()) { - sideEffect(); - }; - return; - } - if (x instanceof Date) { - while (keepGoing()) { - sideEffect(); - }; - return; - } switch (typeof x) { case "boolean" : return; - case "object" : - while (keepGoing()) { - sideEffect(); - }; - return; + default: + if (Array.isArray(x)) { + while (keepGoing()) { + sideEffect(); + }; + return; + } + if (x instanceof Date) { + while (keepGoing()) { + sideEffect(); + }; + return; + } + switch (typeof x) { + case "object" : + while (keepGoing()) { + sideEffect(); + }; + return; + } } } diff --git a/tests/tests/src/untagged_runtime_annotations_shadow_test.mjs b/tests/tests/src/untagged_runtime_annotations_shadow_test.mjs index ce74261c11..0a2cbcd38e 100644 --- a/tests/tests/src/untagged_runtime_annotations_shadow_test.mjs +++ b/tests/tests/src/untagged_runtime_annotations_shadow_test.mjs @@ -20,15 +20,16 @@ let UntaggedNs = { }; function kind(value) { - if (value instanceof globalThis.UntaggedShadowed) { - return "shadowed"; - } - if (value instanceof globalThis.UntaggedNs.Klass) { - return "klass"; - } switch (typeof value) { case "string" : return value; + default: + if (value instanceof globalThis.UntaggedShadowed) { + return "shadowed"; + } + if (value instanceof globalThis.UntaggedNs.Klass) { + return "klass"; + } } } diff --git a/tests/tests/src/untagged_runtime_annotations_test.mjs b/tests/tests/src/untagged_runtime_annotations_test.mjs index bb67992134..48ed93c354 100644 --- a/tests/tests/src/untagged_runtime_annotations_test.mjs +++ b/tests/tests/src/untagged_runtime_annotations_test.mjs @@ -11,28 +11,30 @@ globalThis.UntaggedCallable = class Callable extends Function {} ; function describePet(pet) { - if (pet instanceof UntaggedAnimal && !(pet instanceof UntaggedDog)) { - return "animal"; - } - if (pet instanceof UntaggedDog) { - return "dog"; - } switch (typeof pet) { case "string" : return pet; + default: + if (pet instanceof UntaggedAnimal && !(pet instanceof UntaggedDog)) { + return "animal"; + } + if (pet instanceof UntaggedDog) { + return "dog"; + } } } function onlyAnimal(pet) { - if (pet instanceof UntaggedAnimal && !(pet instanceof UntaggedDog)) { - return "animal"; - } - if (pet instanceof UntaggedDog) { - return "other"; - } switch (typeof pet) { case "string" : return "other"; + default: + if (pet instanceof UntaggedAnimal && !(pet instanceof UntaggedDog)) { + return "animal"; + } + if (pet instanceof UntaggedDog) { + return "other"; + } } } @@ -102,56 +104,108 @@ function objectOrNameKind(value) { } } +function id(x) { + return x; +} + +function missingKind(value) { + switch (typeof value) { + case "string" : + return value; + case "number" : + return "count"; + default: + if (value instanceof UntaggedMissingClass) { + return "missing"; + } + } +} + +function sharedKind(value) { + let kind; + switch (typeof value) { + case "string" : + kind = "text"; + break; + case "number" : + kind = "other"; + break; + default: + if (value instanceof Date) { + kind = "date"; + } else { + switch (typeof value) { + case "object" : + kind = "other"; + break; + } + } + } + return kind + "!"; +} + Mocha.describe("Untagged_runtime_annotations_test", () => { Mocha.test("a subclass case is not taken for its superclass", () => { - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 148, characters 7-14", "animal", describePet(new UntaggedAnimal())); - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 149, characters 7-14", "dog", describePet(new UntaggedDog())); - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 150, characters 7-14", "rex", "rex"); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 175, characters 7-14", "animal", describePet(new UntaggedAnimal())); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 176, characters 7-14", "dog", describePet(new UntaggedDog())); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 177, characters 7-14", "rex", "rex"); }); Mocha.test("a superclass case alone excludes the subclass", () => { - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 154, characters 7-14", "animal", onlyAnimal(new UntaggedAnimal())); - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 155, characters 7-14", "other", onlyAnimal(new UntaggedDog())); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 181, characters 7-14", "animal", onlyAnimal(new UntaggedAnimal())); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 182, characters 7-14", "other", onlyAnimal(new UntaggedDog())); }); Mocha.test("declaration order does not matter", () => { - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 159, characters 7-14", "dog", describeReversed(new UntaggedDog())); - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 160, characters 7-14", "animal", describeReversed(new UntaggedAnimal())); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 186, characters 7-14", "dog", describeReversed(new UntaggedDog())); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 187, characters 7-14", "animal", describeReversed(new UntaggedAnimal())); }); Mocha.test("a subclass upcasts to its superclass", () => { let animal = new UntaggedDog(); - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 165, characters 7-14", "dog", describePet(animal)); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 192, characters 7-14", "dog", describePet(animal)); }); Mocha.test("classes reached through a module alias", () => { - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 169, characters 7-14", "animal", describeAliased(new UntaggedAnimal())); - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 170, characters 7-14", "dog", describeAliased(new UntaggedDog())); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 196, characters 7-14", "animal", describeAliased(new UntaggedAnimal())); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 197, characters 7-14", "dog", describeAliased(new UntaggedDog())); }); Mocha.test("typeOf symbol", () => { - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 174, characters 7-14", "symbol", keyKind(Symbol.for("key"))); - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 175, characters 7-14", "text", "text"); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 201, characters 7-14", "symbol", keyKind(Symbol.for("key"))); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 202, characters 7-14", "text", "text"); }); Mocha.test("typeOf string", () => { - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 179, characters 7-14", "id", "id"); - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 180, characters 7-14", "index", "index"); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 206, characters 7-14", "id", "id"); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 207, characters 7-14", "index", "index"); }); Mocha.test("a class at a dotted path", () => { - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 184, characters 7-14", "locale", localeKind(new Intl.Locale("en"))); - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 185, characters 7-14", "de", "de"); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 211, characters 7-14", "locale", localeKind(new Intl.Locale("en"))); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 212, characters 7-14", "de", "de"); }); Mocha.test("an annotated type is not wrapped in an option", () => { let animal = new UntaggedAnimal(); - Test_utils.ok("File \"untagged_runtime_annotations_test.res\", line 190, characters 7-14", animal === animal); - Test_utils.ok("File \"untagged_runtime_annotations_test.res\", line 191, characters 7-14", animal === animal); + Test_utils.ok("File \"untagged_runtime_annotations_test.res\", line 217, characters 7-14", animal === animal); + Test_utils.ok("File \"untagged_runtime_annotations_test.res\", line 218, characters 7-14", animal === animal); }); Mocha.test("a phantom type hierarchy", () => { - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 195, characters 7-14", "animal", describePhantom(new UntaggedAnimal())); - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 196, characters 7-14", "dog", describePhantom(new UntaggedDog())); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 222, characters 7-14", "animal", describePhantom(new UntaggedAnimal())); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 223, characters 7-14", "dog", describePhantom(new UntaggedDog())); }); Mocha.test("a function case excludes class instances", () => { - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 200, characters 7-14", "callable", handlerKind(new UntaggedCallable())); - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 201, characters 7-14", "function", handlerKind(x => x)); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 227, characters 7-14", "callable", handlerKind(new UntaggedCallable())); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 228, characters 7-14", "function", handlerKind(x => x)); }); Mocha.test("a class declared over an object type", () => { - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 205, characters 7-14", "dog", objectOrNameKind(new UntaggedDog())); - Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 206, characters 7-14", "rex", "rex"); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 232, characters 7-14", "dog", objectOrNameKind(new UntaggedDog())); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 233, characters 7-14", "rex", "rex"); + }); + Mocha.test("primitive cases are tested before class checks", () => { + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 237, characters 7-14", "a", missingKind(id("a"))); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 238, characters 7-14", "count", missingKind(id(1))); + }); + Mocha.test("a default shared by a primitive and an object case", () => { + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 242, characters 7-14", "date!", sharedKind(id(new Date()))); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 243, characters 7-14", "other!", sharedKind(id({ + x: 1 + }))); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 244, characters 7-14", "text!", sharedKind(id("a"))); + Test_utils.eq("File \"untagged_runtime_annotations_test.res\", line 245, characters 7-14", "other!", sharedKind(id(1))); }); }); @@ -170,5 +224,8 @@ export { describePhantom, handlerKind, objectOrNameKind, + id, + missingKind, + sharedKind, } /* Not a pure module */ diff --git a/tests/tests/src/untagged_runtime_annotations_test.res b/tests/tests/src/untagged_runtime_annotations_test.res index d7e53b83cf..bef8f2b55a 100644 --- a/tests/tests/src/untagged_runtime_annotations_test.res +++ b/tests/tests/src/untagged_runtime_annotations_test.res @@ -143,6 +143,33 @@ let objectOrNameKind = value => | ObjectName(name) => name } +@inline(never) let id = x => x + +// No class of this name exists at run time +@instanceOf("UntaggedMissingClass") type missing + +@unboxed +type withMissing = Missing(missing) | Text(string) | Count(float) + +let missingKind = value => + switch value { + | Missing(_) => "missing" + | Text(text) => text + | Count(_) => "count" + } + +@unboxed +type shared = SharedDate(Date.t) | SharedObject({x: int}) | SharedText(string) | SharedCount(float) + +let sharedKind = value => { + let kind = switch value { + | SharedDate(_) => "date" + | SharedText(_) => "text" + | _ => "other" + } + kind ++ "!" +} + describe(__MODULE__, () => { test("a subclass case is not taken for its superclass", () => { eq(__LOC__, "animal", describePet(Animal(makeAnimal()))) @@ -205,4 +232,16 @@ describe(__MODULE__, () => { eq(__LOC__, "dog", objectOrNameKind(ObjectDog(makeObjectDog()))) eq(__LOC__, "rex", objectOrNameKind(ObjectName("rex"))) }) + + test("primitive cases are tested before class checks", () => { + eq(__LOC__, "a", missingKind(id(Text("a")))) + eq(__LOC__, "count", missingKind(id(Count(1.)))) + }) + + test("a default shared by a primitive and an object case", () => { + eq(__LOC__, "date!", sharedKind(id(SharedDate(Date.make())))) + eq(__LOC__, "other!", sharedKind(id(SharedObject({x: 1})))) + eq(__LOC__, "text!", sharedKind(id(SharedText("a")))) + eq(__LOC__, "other!", sharedKind(id(SharedCount(1.)))) + }) }) diff --git a/tests/tests/src/untagged_variants_spec_test.mjs b/tests/tests/src/untagged_variants_spec_test.mjs index 135972012d..e21c9312ae 100644 --- a/tests/tests/src/untagged_variants_spec_test.mjs +++ b/tests/tests/src/untagged_variants_spec_test.mjs @@ -52,39 +52,40 @@ function aliasDictName(v) { } 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"; + default: + 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"; + } } } From 8891e84e74df89fdd1e543d87df9f56b190d3a77 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Fri, 23 Oct 2026 05:18:45 +0000 Subject: [PATCH 2/3] Trust Symbol.hasInstance only for objects and functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why: With the primitive cases tested first, a class whose `Symbol.hasInstance` accepts a primitive no longer wins over a case of that primitive kind, while the spec still said `Symbol.hasInstance` is trusted without qualification. Rule 8 now says that `Symbol.hasInstance` is trusted for objects and functions, and that a primitive whose kind is a case lands in that case first. The values of an `@instanceOf` type are instances, so a primitive of that type already contradicts its annotation. Representation modified (a primitive accepted by Symbol.hasInstance): Example: %%raw(`globalThis.Even = class { static [Symbol.hasInstance](x) { return x % 2 === 0 } }`) @instanceOf("Even") type even @unboxed type t = E(even) | N(float) | S(string) Before (representation): 4 ↦ E After (representation): 4 ↦ N Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Fc7LEuyEfynjaKCVEBPmTF Signed-off-by: Christoph Knittel --- compiler/ml/UntaggedVariants.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/compiler/ml/UntaggedVariants.md b/compiler/ml/UntaggedVariants.md index 162eb029a5..2b4bd8af46 100644 --- a/compiler/ml/UntaggedVariants.md +++ b/compiler/ml/UntaggedVariants.md @@ -324,7 +324,10 @@ if (v instanceof Animal && !(v instanceof Dog)) { ... } A value upcast to the superclass and wrapped in its case lands in the subclass case, as a payload equal to a literal lands in the literal's case (rule 7). Classes not declared to be related are tested in declaration order. -`instanceof` consults `Symbol.hasInstance`, which is trusted. +`instanceof` consults `Symbol.hasInstance`, which is trusted for objects and +functions. A primitive whose kind is a case of the variant lands in that case +before any class check runs, even if a class's `Symbol.hasInstance` would +accept it: the values of a class type are instances, never primitives. An `int` case is recognized by `typeof x === "number"` and so receives every number, integral or not. From 3604ed8a99fd6b31f0ee964307825e275e3aab3d Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Fri, 23 Oct 2026 11:40:56 +0000 Subject: [PATCH 3/3] Keep class checks away from primitives and default-arm cases Why: Two forms still ran class checks on primitives: the two-arm `if`, with the class arm declared first, and the literal test, in its form of negated payload checks. And the guard that sends instance cases without an arm to the default (#9010) knew only object instances, while a declared class can have callable instances, and its class checks ran before the primitive arms. Of a class arm and an arm of another kind, the two-arm form checks the other one. A variant with class cases tests its literals by comparison. The guard for instance cases without an arm also protects a function arm from class instances, and when the guard has class checks, the primitive arms are tested first. The spec's rule 10 example of an unchecked branch changes accordingly: with `PPromise(promise) | PFloat(float)`, the float case is checked, so a thenable that is not a `Promise` lands in `PPromise`. JS modified (an instance handled by the default): Example: @unboxed type t = D(Date.t) | R(RegExp.t) | O({x: int}) | S(string) | I(int) | F(int => int) let f = v => switch v { | O(_) => "O" | S(_) => "S" | _ => "other" } Before (js): if (v instanceof RegExp || v instanceof Date) { return "other"; } switch (typeof v) { case "object" : return "O"; case "string" : return "S"; After (js): switch (typeof v) { case "string" : return "S"; default: if (v instanceof RegExp || v instanceof Date) { return "other"; } switch (typeof v) { case "object" : return "O"; JS modified (two arms): Example: @unboxed type t = W(weird) | S(string) Before (js): if (v instanceof Weird) { return "W"; } else { return "S"; } After (js): if (typeof v === "string") { return "S"; } else { return "W"; } Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CMkrYaktctdKc4M41WF36t Signed-off-by: Christoph Knittel --- compiler/core/lam_compile.ml | 88 +++++++++++++------ compiler/ml/UntaggedVariants.md | 48 +++++----- compiler/ml/ast_untagged_variants.ml | 14 ++- tests/tests/src/untagged_default_arm_test.mjs | 75 ++++++++++++---- tests/tests/src/untagged_default_arm_test.res | 49 +++++++++++ .../src/untagged_runtime_annotations_test.mjs | 12 +-- .../tests/src/untagged_variants_spec_test.mjs | 8 +- .../tests/src/untagged_variants_spec_test.res | 2 +- 8 files changed, 220 insertions(+), 76 deletions(-) diff --git a/compiler/core/lam_compile.ml b/compiler/core/lam_compile.ml index 45ef67e1be..96eee3aa00 100644 --- a/compiler/core/lam_compile.ml +++ b/compiler/core/lam_compile.ml @@ -897,15 +897,6 @@ let compile output_prefix = let not_typeof_clauses, typeof_clauses = List.partition clause_is_not_typeof clauses in - (* A class instance is never a primitive, so the primitive kinds can be - tested first: their values then skip the instance checks, which - throw when a class is missing at run time *) - let primitive_clauses, typeof_clauses = - if (not has_class_case) || not_typeof_clauses = [] then - ([], typeof_clauses) - else - List.partition (fun (tag, _) -> tag_is_primitive tag) typeof_clauses - in let has_object_typeof = List.exists (function @@ -913,25 +904,52 @@ let compile output_prefix = | _ -> false) typeof_clauses in - (* When there's an ObjectType typeof case, null, arrays and class - instances can incorrectly match it (typeof null === typeof [] === - "object"). Guard against them when they should fall through to - default: null, and the instance cases without an arm of their own. *) + let has_function_typeof = + List.exists + (function + | Variant_runtime.Payload_shape FunctionType, _ -> true + | _ -> false) + typeof_clauses + in + (* When there's an ObjectType typeof case, null and arrays can + incorrectly match it (typeof null === typeof [] === "object"), and so + can class instances, which can be functions too. Guard against the + instance cases without an arm of their own, which belong to the + default. *) let needs_null_guard = has_object_typeof && has_null_case in let unmatched_instances = - if not has_object_typeof then [] + Ext_list.filter_map block_cases (function + | Variant_runtime.InstanceType instance + when not + (List.exists + (function + | Variant_runtime.Payload_shape (InstanceType i), _ -> + i = instance + | _ -> false) + not_typeof_clauses) -> ( + match instance with + | Array -> if has_object_typeof then Some instance else None + | Class _ -> + if has_object_typeof || has_function_typeof then Some instance + else None) + | _ -> None) + in + (* A class instance is never a primitive, so the primitive kinds can be + tested first: their values then skip the instance checks and guards, + which throw when a class is missing at run time *) + let primitive_clauses, typeof_clauses = + if + (not has_class_case) + || not_typeof_clauses = [] + && not + (List.exists + (function + | Variant_runtime.Instance.Class _ -> true + | Array -> false) + unmatched_instances) + then ([], typeof_clauses) else - Ext_list.filter_map block_cases (function - | Variant_runtime.InstanceType instance - when not - (List.exists - (function - | Variant_runtime.Payload_shape (InstanceType i), _ -> - i = instance - | _ -> false) - not_typeof_clauses) -> - Some instance - | _ -> None) + List.partition (fun (tag, _) -> tag_is_primitive tag) typeof_clauses in let instances = Ext_list.filter_map block_cases (function @@ -961,7 +979,10 @@ let compile output_prefix = let guards = (if needs_null_guard then [E.is_null e] else []) @ List.map - (fun instance -> E.emit_check (IsInstanceOf (instance, Expr e))) + (fun instance -> + E.emit_check + (Ast_untagged_variants.Dynamic_checks.is_instance instance + (Expr e))) unmatched_instances in let guard = @@ -988,6 +1009,21 @@ let compile output_prefix = (not (tag_is_not_typeof tag1 || tag_is_not_typeof tag2)) && ((not has_class_case) || tag_is_primitive tag1 = tag_is_primitive tag2) in + (* Two arms are compiled to one check of the first. When it is a class + check and the other arm's check is not, check the other arm instead, + so that primitives never reach [instanceof] *) + let cases = + match (cases, default) with + | ( [ + ((Variant_runtime.Payload_shape (InstanceType (Class _)), _) as + class_case); + ((tag, _) as other_case); + ], + Complete ) + when not (tag_is_not_typeof tag) -> + [other_case; class_case] + | _ -> cases + in cases |> compile_general_cases ~make_exp:E.tag_type ~eq_exp:mk_eq ~cxt ~switch ~switch_exp ~default ~merge_cases diff --git a/compiler/ml/UntaggedVariants.md b/compiler/ml/UntaggedVariants.md index 2b4bd8af46..34120cc0f6 100644 --- a/compiler/ml/UntaggedVariants.md +++ b/compiler/ml/UntaggedVariants.md @@ -275,10 +275,11 @@ constructor: 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). A `switch` whose arms name no -literal case still tests the literals first when one of them would pass the -check of a payload arm, such as `@as("lit")` beside a `string` arm, so that -it reaches the default arm. +disjunction where a literal overlaps it). A variant with class cases always +uses the first, so that literals never reach a class check. A `switch` whose +arms name no literal case still tests the literals first when one of them +would pass the check of a payload arm, such as `@as("lit")` beside a +`string` arm, so that it reaches the default arm. - Implementation: `lam_compile.ml:compile_switch`, `ast_untagged_variants.ml:Dynamic_checks.is_a_literal_case`. @@ -291,7 +292,9 @@ it reaches the default arm. 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 + declared first, with the other branch unchecked (rule 10). Of a class arm + and an arm of another kind, the other one is checked, so that primitives + never reach a class check. 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. The function check is @@ -325,9 +328,11 @@ A value upcast to the superclass and wrapped in its case lands in the subclass case, as a payload equal to a literal lands in the literal's case (rule 7). Classes not declared to be related are tested in declaration order. `instanceof` consults `Symbol.hasInstance`, which is trusted for objects and -functions. A primitive whose kind is a case of the variant lands in that case -before any class check runs, even if a class's `Symbol.hasInstance` would -accept it: the values of a class type are instances, never primitives. +functions. A literal, and a primitive whose kind has an arm of the `switch`, +land in their case before any class check runs, even if a class's +`Symbol.hasInstance` would accept them: the values of a class type are +instances, never primitives. With one class arm and a default, every value +reaches the class check. An `int` case is recognized by `typeof x === "number"` and so receives every number, integral or not. @@ -339,19 +344,18 @@ number, integral or not. declaration order, `objectFirst` both forms); `tests/tests/src/UntaggedVariants.res` (`AllInstanceofTypes`, `MergeCases`); `tests/tests/src/untagged_runtime_annotations_test.res` (subclasses, the - function case). + function case), `untagged_default_arm_test.res` (`Symbol.hasInstance`). - Shaped by #6103, #6383, #6792, #7144, #7149. ### 9. `null` and arrays are not objects -`typeof null` and `typeof []` are `"object"`, and so is the `typeof` of a -class instance. The object case never receives `null` when the variant has an -`@as(null)` case, and never receives a value of another instance case (array, -tuple or class): the two-arm form adds the checks of rule 8, and the -`switch (typeof x)` form is preceded by `x === null`, `Array.isArray(x)` and -`x instanceof C` guards that send those values to the default arm when their -own arm is absent. -A case declared with `@typeOf("object")` +`typeof null` and `typeof []` are `"object"`, and a class instance is an +object or a function. The object case never receives `null` when the variant +has an `@as(null)` case, and neither the object nor the function case +receives an instance of another 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)` and `x instanceof C` guards that send those values to the +default arm when their own case has no arm. A case declared with `@typeOf("object")` (rule 5) is an object case: without an `@as(null)` case, it receives `null`, as a record case does. @@ -374,17 +378,17 @@ 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`; + `PPromise(promise) | PFloat(float)`, the float case is checked (rule + 8), so a thenable that is not a `Promise` lands in `PPromise`; - 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): recognition is local to a realm. A class that does not exist where the -`switch` runs makes its check throw for every value that reaches it: in the -`switch` form, objects, functions and primitives outside every case; in the -two-arm form, every value when the class arm is declared first. +`switch` runs makes its check throw for every value that reaches it: +objects and functions, primitives whose case has no arm of the `switch`, and, +with one class arm and a default, every value. - Implementation: `lam_compile.ml:compile_general_cases` (a complete match emits no check for its last clause). diff --git a/compiler/ml/ast_untagged_variants.ml b/compiler/ml/ast_untagged_variants.ml index f1fecbbd14..fd6ed3c7b4 100644 --- a/compiler/ml/ast_untagged_variants.ml +++ b/compiler/ml/ast_untagged_variants.ml @@ -673,8 +673,18 @@ module Dynamic_checks = struct let without_literal_cases = is_a_literal_case ~literal_cases ~block_cases ~list_literal_cases:false e in - if size with_literal_cases <= size without_literal_cases then - with_literal_cases + (* Class checks would run on the literals, which are primitives: a class + missing at run time would throw, and a [Symbol.hasInstance] could + claim them *) + let has_class_case = + List.exists + (function + | InstanceType (Class _) -> true + | _ -> false) + block_cases + in + if has_class_case || size with_literal_cases <= size without_literal_cases + then with_literal_cases else without_literal_cases let is_int_tag ?(has_null_undefined_other = (false, false, false)) (e : _ t) : diff --git a/tests/tests/src/untagged_default_arm_test.mjs b/tests/tests/src/untagged_default_arm_test.mjs index 49ddffd59e..455cdaa697 100644 --- a/tests/tests/src/untagged_default_arm_test.mjs +++ b/tests/tests/src/untagged_default_arm_test.mjs @@ -3,21 +3,29 @@ import * as Mocha from "mocha"; import * as Test_utils from "./test_utils.mjs"; +globalThis.UntaggedAnyValue = class AnyValue { + static [Symbol.hasInstance]() { return true } +} +; + function id(x) { return x; } function objectOrString(value) { - if (value instanceof RegExp || value instanceof Date) { - return "other"; - } switch (typeof value) { - case "object" : - return "O"; case "string" : return "S"; default: - return "other"; + if (value instanceof RegExp || value instanceof Date) { + return "other"; + } + switch (typeof value) { + case "object" : + return "O"; + default: + return "other"; + } } } @@ -35,23 +43,58 @@ function textOrRecord(value) { } } +function twoArmsKind(value) { + if (typeof value === "string") { + return "name"; + } else { + return "any"; + } +} + +function withLiteralsKind(value) { + if (value !== 1 && value !== 5 && value !== 2 && value !== 4 && value !== 3) { + if (typeof value === "string") { + return "name"; + } else { + return "any"; + } + } + switch (value) { + case 1 : + return "one"; + case 2 : + return "two"; + case 3 : + return "three"; + case 4 : + return "four"; + case 5 : + return "five"; + } +} + Mocha.describe("Untagged_default_arm_test", () => { Mocha.test("an instance handled by the default is not an object case", () => { - Test_utils.eq("File \"untagged_default_arm_test.res\", line 31, characters 7-14", "other", objectOrString(id(new Date()))); - Test_utils.eq("File \"untagged_default_arm_test.res\", line 32, characters 7-14", "other", objectOrString(id(/a/))); - Test_utils.eq("File \"untagged_default_arm_test.res\", line 33, characters 7-14", "O", objectOrString(id({ + Test_utils.eq("File \"untagged_default_arm_test.res\", line 71, characters 7-14", "other", objectOrString(id(new Date()))); + Test_utils.eq("File \"untagged_default_arm_test.res\", line 72, characters 7-14", "other", objectOrString(id(/a/))); + Test_utils.eq("File \"untagged_default_arm_test.res\", line 73, characters 7-14", "O", objectOrString(id({ x: 1 }))); - Test_utils.eq("File \"untagged_default_arm_test.res\", line 34, characters 7-14", "S", objectOrString(id("s"))); - Test_utils.eq("File \"untagged_default_arm_test.res\", line 35, characters 7-14", "other", objectOrString(id(1))); + Test_utils.eq("File \"untagged_default_arm_test.res\", line 74, characters 7-14", "S", objectOrString(id("s"))); + Test_utils.eq("File \"untagged_default_arm_test.res\", line 75, characters 7-14", "other", objectOrString(id(1))); }); Mocha.test("a literal handled by the default is not a string case", () => { - Test_utils.eq("File \"untagged_default_arm_test.res\", line 39, characters 7-14", "other", textOrRecord(id("lit"))); - Test_utils.eq("File \"untagged_default_arm_test.res\", line 40, characters 7-14", "text", textOrRecord(id("lit2"))); - Test_utils.eq("File \"untagged_default_arm_test.res\", line 41, characters 7-14", "record", textOrRecord(id({ + Test_utils.eq("File \"untagged_default_arm_test.res\", line 79, characters 7-14", "other", textOrRecord(id("lit"))); + Test_utils.eq("File \"untagged_default_arm_test.res\", line 80, characters 7-14", "text", textOrRecord(id("lit2"))); + Test_utils.eq("File \"untagged_default_arm_test.res\", line 81, characters 7-14", "record", textOrRecord(id({ x: 1 }))); - Test_utils.eq("File \"untagged_default_arm_test.res\", line 42, characters 7-14", "other", textOrRecord(id(1))); + Test_utils.eq("File \"untagged_default_arm_test.res\", line 82, characters 7-14", "other", textOrRecord(id(1))); + }); + Mocha.test("primitives skip class checks in the two-arm form", () => Test_utils.eq("File \"untagged_default_arm_test.res\", line 86, characters 7-14", "name", twoArmsKind(id("a")))); + Mocha.test("literals skip class checks", () => { + Test_utils.eq("File \"untagged_default_arm_test.res\", line 90, characters 7-14", "one", withLiteralsKind(id(1))); + Test_utils.eq("File \"untagged_default_arm_test.res\", line 91, characters 7-14", "name", withLiteralsKind(id("a"))); }); }); @@ -59,5 +102,7 @@ export { id, objectOrString, textOrRecord, + twoArmsKind, + withLiteralsKind, } /* Not a pure module */ diff --git a/tests/tests/src/untagged_default_arm_test.res b/tests/tests/src/untagged_default_arm_test.res index dce3172e6a..9f7648d87b 100644 --- a/tests/tests/src/untagged_default_arm_test.res +++ b/tests/tests/src/untagged_default_arm_test.res @@ -1,6 +1,12 @@ open Mocha open Test_utils +%%raw(` +globalThis.UntaggedAnyValue = class AnyValue { + static [Symbol.hasInstance]() { return true } +} +`) + @inline(never) let id = x => x // A switch with a default arm keeps the cases it does not name out of the @@ -26,6 +32,40 @@ let textOrRecord = value => | _ => "other" } +// A class whose Symbol.hasInstance accepts every value does not take +// primitives in the two-arm form or in the literal test +@instanceOf("UntaggedAnyValue") type anyValue + +@unboxed +type twoArms = Any(anyValue) | Name(string) + +let twoArmsKind = value => + switch value { + | Any(_) => "any" + | Name(_) => "name" + } + +@unboxed +type withLiterals = + | @as(1) One + | @as(2) Two + | @as(3) Three + | @as(4) Four + | @as(5) Five + | AnyLit(anyValue) + | NameLit(string) + +let withLiteralsKind = value => + switch value { + | One => "one" + | Two => "two" + | Three => "three" + | Four => "four" + | Five => "five" + | AnyLit(_) => "any" + | NameLit(_) => "name" + } + describe(__MODULE__, () => { test("an instance handled by the default is not an object case", () => { eq(__LOC__, "other", objectOrString(id(D(Date.make())))) @@ -41,4 +81,13 @@ describe(__MODULE__, () => { eq(__LOC__, "record", textOrRecord(id(Rec({x: 1})))) eq(__LOC__, "other", textOrRecord(id(Count(1)))) }) + + test("primitives skip class checks in the two-arm form", () => { + eq(__LOC__, "name", twoArmsKind(id(Name("a")))) + }) + + test("literals skip class checks", () => { + eq(__LOC__, "one", withLiteralsKind(id(One))) + eq(__LOC__, "name", withLiteralsKind(id(NameLit("a")))) + }) }) diff --git a/tests/tests/src/untagged_runtime_annotations_test.mjs b/tests/tests/src/untagged_runtime_annotations_test.mjs index 48ed93c354..9225cc8d84 100644 --- a/tests/tests/src/untagged_runtime_annotations_test.mjs +++ b/tests/tests/src/untagged_runtime_annotations_test.mjs @@ -73,10 +73,10 @@ function referenceKind(reference) { } function localeKind(locale) { - if (locale instanceof Intl.Locale) { - return "locale"; - } else { + if (typeof locale === "string") { return locale; + } else { + return "locale"; } } @@ -97,10 +97,10 @@ function handlerKind(handler) { } function objectOrNameKind(value) { - if (value instanceof UntaggedDog) { - return "dog"; - } else { + if (typeof value === "string") { return value; + } else { + return "dog"; } } diff --git a/tests/tests/src/untagged_variants_spec_test.mjs b/tests/tests/src/untagged_variants_spec_test.mjs index e21c9312ae..d3b95aa2e6 100644 --- a/tests/tests/src/untagged_variants_spec_test.mjs +++ b/tests/tests/src/untagged_variants_spec_test.mjs @@ -176,10 +176,10 @@ function twoInstancesName(v) { } function promiseOrFloatName(v) { - if (v instanceof Promise) { - return "promise"; - } else { + if (typeof v === "number") { return "float"; + } else { + return "promise"; } } @@ -352,7 +352,7 @@ Mocha.describe("Untagged_variants_spec_test", () => { x: 1 }), "RegExp"); let thenable = ({then: (resolve) => resolve(1)}); - Test_utils.eq("File \"untagged_variants_spec_test.res\", line 320, characters 7-14", promiseOrFloatName(thenable), "float"); + Test_utils.eq("File \"untagged_variants_spec_test.res\", line 320, characters 7-14", promiseOrFloatName(thenable), "promise"); }); Mocha.test("rule 12: option of an untagged variant", () => { Test_utils.eq("File \"untagged_variants_spec_test.res\", line 324, characters 7-14", someWithNull(null), null); diff --git a/tests/tests/src/untagged_variants_spec_test.res b/tests/tests/src/untagged_variants_spec_test.res index e0068d02f5..b9cf3d08f2 100644 --- a/tests/tests/src/untagged_variants_spec_test.res +++ b/tests/tests/src/untagged_variants_spec_test.res @@ -317,7 +317,7 @@ describe(__MODULE__, () => { 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") + eq(__LOC__, promiseOrFloatName(raw(thenable)), "promise") }) test("rule 12: option of an untagged variant", () => {