diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index e9e799b6e..70a2e5d53 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -418,6 +418,7 @@ tslang_add_test(NAME test-compile-00-nullable-class-to-interface COMMAND test-ru tslang_add_test(NAME test-compile-00-class-to-interface-null COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_to_interface_null.ts") tslang_add_test(NAME test-compile-00-null-interface-to-interface COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00null_interface_to_interface.ts") tslang_add_test(NAME test-compile-00-interface-type-tag-in-vtable COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_type_tag_in_vtable.ts") +tslang_add_test(NAME test-compile-00-nullable-interface-to-interface COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00nullable_interface_to_interface.ts") tslang_add_test(NAME test-compile-00-argument-place-aliased COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00argument_place_aliased.ts") tslang_add_test(NAME test-compile-00-temporary-read-in-finally COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00temporary_read_in_finally.ts") tslang_add_test(NAME test-compile-00-unowned-local-store COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00unowned_local_store.ts") @@ -1015,6 +1016,7 @@ tslang_add_test(NAME test-jit-00-nullable-class-to-interface COMMAND test-runner tslang_add_test(NAME test-jit-00-class-to-interface-null COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_to_interface_null.ts") tslang_add_test(NAME test-jit-00-null-interface-to-interface COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00null_interface_to_interface.ts") tslang_add_test(NAME test-jit-00-interface-type-tag-in-vtable COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_type_tag_in_vtable.ts") +tslang_add_test(NAME test-jit-00-nullable-interface-to-interface COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00nullable_interface_to_interface.ts") tslang_add_test(NAME test-jit-00-argument-place-aliased COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00argument_place_aliased.ts") tslang_add_test(NAME test-jit-00-temporary-read-in-finally COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00temporary_read_in_finally.ts") tslang_add_test(NAME test-jit-00-unowned-local-store COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00unowned_local_store.ts") @@ -2002,6 +2004,7 @@ set(TSLANG_CORPUS 00class_to_interface_null.ts 00null_interface_to_interface.ts 00interface_type_tag_in_vtable.ts + 00nullable_interface_to_interface.ts 00argument_place_aliased.ts 00temporary_read_in_finally.ts 00unowned_local_store.ts diff --git a/tslang/test/tester/tests/00nullable_interface_to_interface.ts b/tslang/test/tester/tests/00nullable_interface_to_interface.ts new file mode 100644 index 000000000..92ae0b04b --- /dev/null +++ b/tslang/test/tester/tests/00nullable_interface_to_interface.ts @@ -0,0 +1,84 @@ +// An interface-or-null given to another interface-or-null whose interface it does not extend is the +// interface-to-interface cast (#666), which builds the target's vtable out of the value's own one. A +// plain copy kept the source's vtable, and a field read through the target read a method slot as the +// field's offset. A null value is null still, and its methods throw. Not under -mm=own, which does not +// take this program's interface values returned from calls yet. +interface Shape { + id: number; + area(): number; +} + +interface HasId { + id: number; +} + +interface Sized { + area(): number; + id: number; +} + +class Box { + constructor(public w: number, public h: number) {} +} + +class Rect implements Shape { + constructor(public id: number, public box: Box) {} + + area(): number { + return this.box.w * this.box.h; + } +} + +class Square extends Rect { + constructor(id: number, side: number, public label: Box) { + super(id, new Box(side, side)); + } +} + +@noinline function mkSquare(id: number, side: number): Shape { + return new Square(id, side, new Box(id, id)); +} + +@noinline function mkShape(flag: boolean): Shape | null { + return flag ? mkSquare(5, 2) : null; +} + +@noinline function mkNull(): HasId | null { + return null; +} + +// what a method of the interface does: -2 when it throws +function call(s: Sized | null): number { + try { + return s.area(); + } catch (e) { + return -2; + } +} + +function main() { + // a field first, and no method: a method called through a wrong vtable runs whatever code it lands on + const b: Shape | null = mkSquare(7, 3); + let n: HasId | null = mkNull(); + n = b; + assert(n.id == 7, "a field read through the other interface"); + + // members in another order: the field and the method each moved + const s: Sized | null = b; + assert(s.id == 7, "a field that moved"); + assert(call(s) == 9, "a method that moved"); + + // a declaration, and a value that came back from a call + const m: HasId | null = mkShape(true); + assert(m.id == 5, "a returned value seen through the other interface"); + + // null stays null, and its methods throw + const nullShape: Shape | null = mkShape(false); + const nullId: HasId | null = nullShape; + assert(nullId === null, "null seen through the other interface"); + const nullSized: Sized | null = nullShape; + assert(nullSized === null, "null seen through an interface with members in another order"); + assert(call(nullSized) == -2, "its method throws"); + + print("done."); +}