diff --git a/tslang/lib/TypeScript/MLIRGenCast.cpp b/tslang/lib/TypeScript/MLIRGenCast.cpp index ff0dfd522..e2e732ef9 100644 --- a/tslang/lib/TypeScript/MLIRGenCast.cpp +++ b/tslang/lib/TypeScript/MLIRGenCast.cpp @@ -1086,6 +1086,20 @@ namespace mlirgen return castInterfaceToInterface(location, value, srcInterfaceType, interfaceType, genContext); } + // `J | null` or `C | null` (a `J | undefined` given to `I | undefined` gets here too) is stored + // as its one member, null a null pointer in it: this is the cast of that member. A plain cast + // kept J's vtable for I, and lowering could not make a class pointer an interface. + if (auto srcUnionType = dyn_cast(valueType)) + { + mlir::Type srcBaseType; + if (!mth.isUnionTypeNeedsTag(location, srcUnionType, srcBaseType) + && isa_and_nonnull(srcBaseType)) + { + auto member = builder.create(location, srcBaseType, value); + return cast(location, interfaceType, member, genContext); + } + } + // A number, string, boolean or array has no fields an interface's members could be read // from: passed for an `{ kind: number }` it compiled, without a word, to a cast that // lowering could not make, and the callee read garbage. An interface without members @@ -1692,11 +1706,13 @@ namespace mlirgen // `C | null` given to `I | null`: both are stored as their one member (null is a null pointer // in it), so this is the class-to-interface cast of `C` to `I`. A plain cast read the class - // pointer as an interface. + // pointer as an interface. `J | null` given to `I | null` is likewise the cast of `J` to `I`, + // which gives an `I` that `J` does not extend a vtable of its own (castInterfaceToInterface). if (auto srcUnionType = dyn_cast(valueType); srcUnionType && isa(baseType)) { mlir::Type srcBaseType; - if (mth.isUnionTypeNeedsTag(location, srcUnionType, srcBaseType) || !isa_and_nonnull(srcBaseType)) + if (mth.isUnionTypeNeedsTag(location, srcUnionType, srcBaseType) + || !isa_and_nonnull(srcBaseType)) { return std::nullopt; } diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index ec453ee48..e74df8071 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -401,6 +401,7 @@ tslang_add_test(NAME test-compile-00-string-view-copy COMMAND test-runner "${PRO tslang_add_test(NAME test-compile-00-global-copy-counted COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_copy_counted.ts") tslang_add_test(NAME test-compile-00-global-interface-initializer COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_interface_initializer.ts") tslang_add_test(NAME test-compile-00-interface-cast-unrelated COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_cast_unrelated.ts") +tslang_add_test(NAME test-compile-00-interface-union-cast-unrelated COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_union_cast_unrelated.ts") tslang_add_test(NAME test-compile-00-c-string-copy COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00c_string_copy.ts") tslang_add_test(NAME test-compile-00-const-field-read COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_field_read.ts") tslang_add_test(NAME test-compile-00-const-element-read COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_element_read.ts") @@ -996,6 +997,7 @@ tslang_add_test(NAME test-jit-00-string-view-copy COMMAND test-runner -jit "${PR tslang_add_test(NAME test-jit-00-global-copy-counted COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_copy_counted.ts") tslang_add_test(NAME test-jit-00-global-interface-initializer COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00global_interface_initializer.ts") tslang_add_test(NAME test-jit-00-interface-cast-unrelated COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_cast_unrelated.ts") +tslang_add_test(NAME test-jit-00-interface-union-cast-unrelated COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_union_cast_unrelated.ts") tslang_add_test(NAME test-jit-00-c-string-copy COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00c_string_copy.ts") tslang_add_test(NAME test-jit-00-const-field-read COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_field_read.ts") tslang_add_test(NAME test-jit-00-const-element-read COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_element_read.ts") @@ -1981,6 +1983,7 @@ set(TSLANG_CORPUS 00global_copy_counted.ts 00global_interface_initializer.ts 00interface_cast_unrelated.ts + 00interface_union_cast_unrelated.ts 00c_string_copy.ts 00const_field_read.ts 00const_element_read.ts diff --git a/tslang/test/tester/tests/00interface_union_cast_unrelated.ts b/tslang/test/tester/tests/00interface_union_cast_unrelated.ts new file mode 100644 index 000000000..3c9085881 --- /dev/null +++ b/tslang/test/tester/tests/00interface_union_cast_unrelated.ts @@ -0,0 +1,97 @@ +// `J | null` given to `I | null`, where J does not extend I, was a plain cast of the union: the value kept +// J's vtable and I read it by its own slot numbers (00interface_cast_unrelated.ts has the cast of J to I +// alone). `C | null` given to an interface (through `I | undefined`) was a class pointer read as an +// interface. Both are the cast of the union's one member now; null stays null. +interface Hit { + thing: number; + ray: number; + dist: number; +} + +interface HasDist { + dist: number; +} + +interface A { + f(): number; + g(): number; +} + +interface B { + g(): number; +} + +class AC implements A { + base: number = 100; + f(): number { + return this.base + 1; + } + g(): number { + return this.base + 2; + } +} + +class HitC implements Hit { + thing: number = 10; + ray: number = 20; + dist: number = 30; +} + +function pick(flag: boolean): Hit | null { + if (flag) { + const h: Hit = { thing: 1, ray: 2, dist: 3 }; + return h; + } + + return null; +} + +function pickC(flag: boolean): HitC | null { + if (flag) { + return new HitC(); + } + + return null; +} + +function distOrMinus(d: HasDist | null) { + return d ? d.dist : -1; +} + +function main() { + // `Hit | null` to `HasDist | null` + const h = pick(true); + const d: HasDist | null = h; + assert(d !== null && d.dist == 3, "a field through J | null"); + assert(distOrMinus(h) == 3, "a field through J | null as an argument"); + + const n = pick(false); + const dn: HasDist | null = n; + assert(dn === null, "null stays null"); + assert(distOrMinus(n) == -1, "null as an argument"); + + // methods, `A | null` to `B | null` + const a: A | null = new AC(); + const b: B | null = a; + assert(b !== null && b.g() == 102, "a method through J | null"); + + // to `I | undefined`, from an interface and from a class + const u: HasDist | undefined = h; + assert(u !== undefined && u.dist == 3, "an interface through I | undefined"); + const c: HasDist | undefined = pickC(true); + assert(c !== undefined && c.dist == 30, "a class through I | undefined"); + + // `` of `J | null` + const cast = h; + assert(cast.dist == 3, " of J | null"); + + let sum: number = 0; + for (let i = 0; i < 100; i++) { + const each: HasDist | null = pick(i % 2 == 0); + sum += distOrMinus(each); + } + + assert(sum == 50 * 3 - 50, "casts in a loop"); + + print("done."); +}