Why
A public abbreviation could declare a subclass when its arguments were not all type variables, meant for phantom type hierarchies. The test was syntactic, so @instanceOf("Dog") type dog = animal<int> passed, and every animal<int> was then a dog without a coercion.
A subclass is always a private abbreviation, phantom hierarchies included; upcasts use :>.
addedDiagnostic public subclass of an instantiated class
@instanceOf("Animal") type animal<'a>@instanceOf("Dog") type dog = animal<int>Before
After
noneA subclass must be a private abbreviation: write `= private ...` so that instances of Animal cannot be used as instances of Dog without a coercion.
Why
The payload dispatch of an untagged variant ran its instance checks before the typeof checks, so strings and numbers reached instanceof: a class missing at run time threw for them, and a Symbol.hasInstance could claim them. Literals were tested by a size heuristic that chose between two equivalent expressions, the object case by a guard per instance case, and a subclass by negating it in its superclass's check.
The literals come first, compared with === by one switch (x) (an if per body for at most two), whose default is the payload dispatch: one switch (typeof x) over the primitive kinds, whose default is an if chain over the instance cases (a subclass first), the function case, the tagged constructors and the object case. In a complete match the last arm is not checked. A literal without an arm skips the switch when no payload check would accept it, and a null that only the object check would accept adds x !== null to it. Checks leading to the default before the last checked arm fold into its check, so class checks run only on objects. Dynamic_checks.is_a_literal_case and the per-instance guards are gone.
modifiedJS primitives before classes
@unboxedtype t = Missing(missing) | Text(string) | Count(float)let kind = value => switch value { | Missing(_) => "missing" | Text(text) => text | Count(_) => "count" }Before
After
if (value instanceof UntaggedMissingClass) { return "missing";} switch (typeof value) {switch (typeof value) { case "string" : case "string" : return value; return value; case "number" : case "number" : return "count"; return "count"; default: return "missing";}}
modifiedJS instances without an arm
@unboxedtype t = D(Date.t) | R(RegExp.t) | O({x: int}) | S(string) | I(int) | F(int => int)let kind = value => switch value { | O(_) => "O" | S(_) => "S" | _ => "other" }Before
After
if (value instanceof RegExp || value instanceof Date) { if (typeof value === "string") { return "S";} else if (typeof value === "object" && !(value instanceof RegExp) && !(value instanceof Date)) { return "O";} else { return "other"; return "other";}}switch (typeof value) { case "object" : return "O"; case "string" : return "S"; default: return "other";}
modifiedJS a subclass first
@unboxedtype upload = Blob(Blob.t) | File(File.t)let kind = upload => switch upload { | Blob(_) => "blob" | File(_) => "file" }Before
After
if (upload instanceof Blob && !(upload instanceof File)) { return "blob";} else {if (upload instanceof File) { return "file"; return "file"; } else { return "blob";}}
Review
- Does
break in an arm leave the generated switch instead of the loop? No: every arm placed in a generated switch is compiled in a switch context, where break and continue are labeled; untagged_loop_control_test.res checks arms of every part. - What changes for a value outside every case? It lands in the last arm of the chain: with
PPromise | PFloat the float case is now checked, so a thenable lands in PPromise (rule 10).
A complete payload dispatch with one body runs it unchecked, so a literal arm with the same body needs no case of its own.
modifiedJS literal arm equal to the payload arm
@unboxedtype rec t = | @as(null) Null | @as(undefined) Undefined | WhyNotAnotherOne | Point({x: int, y: int})let plus = (x, y) => switch y { | Null | Undefined => x | WhyNotAnotherOne => WhyNotAnotherOne | Point(_) => WhyNotAnotherOne }Before
After
switch (y) { case null : case undefined : return x; case "WhyNotAnotherOne" : return "WhyNotAnotherOne"; default: return "WhyNotAnotherOne";if (y == null) { return x;} else { return "WhyNotAnotherOne"; }}