x instanceof C is false whenever x is typed as a union or an optional that holds a class, even when it holds a C, and even after narrowing. Every memory model (gc, rc, none), JIT and AOT, with and without --opt.
Repro
class C { constructor(public x: number) {} }
class D { y = 1; }
function main() {
let a = new C(1);
print("C:", a instanceof C);
let b: C | D = new C(2);
print("C | D:", b instanceof C);
let e: C | null = new C(3);
print("C | null:", e instanceof C);
if (e) print("C | null, narrowed:", e instanceof C);
let f: C | number = new C(4);
print("C | number:", f instanceof C);
}
tslang --emit=jit --no-default-lib -mm=gc --shared-libs=TypeScriptRuntime.dll io.ts prints:
C: true
C | D: false
C | null: false
C | null, narrowed: false
C | number: false
Expected true on every line.
Cause
mlirGenInstanceOfLogic (tslang/lib/TypeScript/MLIRGenImpl.h) dispatches on the left operand's type: a class calls .instanceOf with the RTTI, any checks typeof and goes through the vtable, and opaque and interface values have paths of their own. A union or an optional matches none of them and falls through to the "default logic", which emits a compile-time constant leftType == rightType. That is false for union<C, null> against C. The MLIR shows "ts.Constant"() <{value = false}> where the test should be. Narrowing with if (e) doesn't change the type the expression is generated with, so the narrowed case folds the same way.
A fix would handle a union or optional on the left at run time: for an optional, test HasValue and run the class path on the value; for a union, run it on each class member's payload, selected by the union's type tag. Non-class members (null, number) give false.
Found while writing -mm=own phase 6 tests (#449); own_declared_constant.ts tests its union value with typeof instead.
x instanceof Cisfalsewheneverxis typed as a union or an optional that holds a class, even when it holds aC, and even after narrowing. Every memory model (gc, rc, none), JIT and AOT, with and without--opt.Repro
tslang --emit=jit --no-default-lib -mm=gc --shared-libs=TypeScriptRuntime.dll io.tsprints:Expected
trueon every line.Cause
mlirGenInstanceOfLogic(tslang/lib/TypeScript/MLIRGenImpl.h) dispatches on the left operand's type: a class calls.instanceOfwith the RTTI,anycheckstypeofand goes through the vtable, and opaque and interface values have paths of their own. A union or an optional matches none of them and falls through to the "default logic", which emits a compile-time constantleftType == rightType. That isfalseforunion<C, null>againstC. The MLIR shows"ts.Constant"() <{value = false}>where the test should be. Narrowing withif (e)doesn't change the type the expression is generated with, so the narrowed case folds the same way.A fix would handle a union or optional on the left at run time: for an optional, test
HasValueand run the class path on the value; for a union, run it on each class member's payload, selected by the union's type tag. Non-class members (null,number) givefalse.Found while writing
-mm=ownphase 6 tests (#449);own_declared_constant.tstests its union value withtypeofinstead.