Skip to content

Test: an interface-or-null given to another interface-or-null (field read first) - #670

Merged
ASDAlexander77 merged 2 commits into
mainfrom
fix-nullable-interface-to-interface-cast
Oct 23, 2026
Merged

ASDAlexander77 merged 2 commits into
mainfrom
fix-nullable-interface-to-interface-cast

Conversation

@ASDAlexander77

@ASDAlexander77 ASDAlexander77 commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

Update: #669 merged the same fix into main (and also covers J | null given to I). Merging main took its MLIRGenCast.cpp as it is, so this PR now only adds the test 00nullable_interface_to_interface.ts. With main merged: Windows ctest -C Release 5426/5426 passed, the new test included. The description below is the original one.

Problem

Giving an I | null value to a J | null variable, where I does not extend J, compiled to a plain cast. The value kept I's vtable, and J read it by its own slot numbers. A field read through J took one of I's method slots as the field's offset, so the program crashed (0xC0000005) under gc, rc and none:

interface Shape { id: number; area(): number; }
interface HasId { id: number; }
const b: Shape | null = mkSquare(7, 3);
let n: HasId | null = mkNull();
n = b;
assert(n.id == 7);   // 0xC0000005

Fix

castToNullableUnionType (MLIRGenCast.cpp): both unions are stored as their one member, so I | null → J | null is now the interface-to-interface cast of I to J (castInterfaceToInterface, with the #666 vtable remap). This is how #665 already handles C | null → I | null. A null value stays null. Its vtable, I's null vtable, is remapped like any other, so its methods still throw. 00null_interface_to_interface.ts (#665) settled the same rule for I → J. I | null → I | null (the same interface) is left as it was.

Tests

  • New tests/00nullable_interface_to_interface.ts, registered as compile + jit and in TSLANG_CORPUS (rc, none). It reads a field before any method call or allocation, so a regression fails at a few MB instead of running into random code. It covers assignment, a declaration, a value returned from a call, an interface whose members come in another order, and null (stays null, its method throws).
  • Not under -mm=own: own refuses this program's interface-returning calls ("'this value' takes a second reference") with or without the fix.
  • Tooth: with the MLIRGenCast.cpp change reverted, the test crashes with 0xC0000005 under gc, rc and none (JIT, run one process at a time under a memory watchdog; peak 9–114 MB, no runaway).
  • Windows ctest -C Release: 5420/5420 passed (2 disabled/skipped as on main).
  • WSL (Linux, Release): 5402/5402 passed (disabled/skipped as on main: defaultlib-collector tests need DEFAULT_LIB_PATH)

Based on main after #668 (type tag in the vtable): the only overlap was adjacent lines in tslang/test/tester/CMakeLists.txt.

🤖 Generated with Claude Code

ASDAlexander77 and others added 2 commits October 23, 2026 15:51
…ace-to-interface cast

`J | null` given to `I | null`, where `J` does not extend `I`, was a plain cast:
the value kept `J`'s vtable, and a field read through `I` took a method slot of
it for the field's offset (0xC0000005 under every model). Both are stored as
their one member, so this is now the interface-to-interface cast of `J` to `I`
(#666), as `C | null` to `I | null` is the class-to-interface cast (#665). A
null value stays null: its vtable, the null vtable of `J`, is remapped as any
other, and its methods throw.

Test 00nullable_interface_to_interface.ts (compile + jit, rc/none corpus) reads
a field before any method call; not under -mm=own, which refuses the program's
interface-returning calls with or without the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#669 made `J | null` given to `I | null` (and to `I`) the cast of J to I, the
change this branch made to castToNullableUnionType and more. Main's
MLIRGenCast.cpp is taken as it is; this branch now adds only the test
00nullable_interface_to_interface.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77 ASDAlexander77 changed the title An interface-or-null given to another interface-or-null is the interface-to-interface cast Test: an interface-or-null given to another interface-or-null (field read first) Oct 23, 2026
@ASDAlexander77
ASDAlexander77 merged commit 3342bcf into main Oct 23, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the fix-nullable-interface-to-interface-cast branch October 23, 2026 15:26
ASDAlexander77 added a commit that referenced this pull request Oct 23, 2026
…s the block it shows (#671)

own_facts::isView took a cast between class, union and optional types as the
same block, but not one from or to an interface. So `const b: B = a` (A to B),
`const d: HasDist = mkHit()`, `return h` into `Hit | null`, and even a cast
to a base interface (B extends A, A of B) were each a second reference own
could not prove a move or a borrow of. Every interface-to-interface cast
(#666, #669, #670) was rejected under own.

The cast keeps `this`; the vtable laid out for the other interface is
interned, nobody's block. So it is a view like the others, and own moves and
borrows through it as it does the object: a read after `[a]` moved it is
still an error.

00interface_cast_unrelated, 00interface_union_cast_unrelated,
00nullable_interface_to_interface and 00null_interface_to_interface now run
under own too. 00interface_cast_unrelated's element and loop cases no longer
move or merge `h` (own's rules, not this cast's).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant