Skip to content

J | null given to I | null, or to I, is the cast of J to I - #669

Merged
ASDAlexander77 merged 1 commit into
mainfrom
iface-union-cast-remap
Oct 23, 2026
Merged

ASDAlexander77 merged 1 commit into
mainfrom
iface-union-cast-remap

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Extends #666 to unions. #666 gives an interface J cast to an interface I it does not extend a vtable laid out for I. J | null given to I | null didn't get that remap, because it was a plain cast of the union. The value kept J's vtable, and I read it by its own slot numbers. This happened under every model, gc included:

const h: Hit | null = pick(true);       // Hit { thing; ray; dist }
const d: HasDist | null = h;            // d.dist read h.thing
const a: A | null = new AC();           // A { f(); g() }
const b: B | null = a;                  // B { g() }: b.g() called f

The same union given to I alone, as I | undefined is through its optional, was a plain cast as well. With a class member (C | null → I | undefined), lowering couldn't turn the class pointer into an interface. It warned invalid cast from '!llvm.ptr' to '!llvm.struct<...>'.

Fix

Both unions are stored as their one member, with null as a null pointer in it. So each is the cast of that member (MLIRGenCast.cpp):

Tests

  • tests/00interface_union_cast_unrelated.ts covers:

    • Hit | null → HasDist | null, for a value and for null, as a local and as an argument;
    • A | null → B | null, a method;
    • I | undefined from an interface and from a class;
    • <I> of J | null;
    • 100 casts in a loop.

    It runs as a compile test and a JIT test under gc, and in the rc/none corpus.

  • Tooth: with the compiler change reverted, gc and rc fail with assertion failed: a field through J | null, along with the invalid-cast warning.

Not covered

Verification

  • Windows: 5414/5414.
  • Linux: 5396/5396, and the 19 interface-cast tests pass with MALLOC_PERTURB_=165 MALLOC_CHECK_=3.

🤖 Generated with Claude Code

An interface J that does not extend I gets a vtable laid out for I when it
is cast to I (#666), but `J | null` given to `I | null` was a plain cast of
the union: the value kept J's vtable, and I read it by its own slot numbers
(`d.dist` read `thing`, `b.g()` called `f`). The same union given to `I`
(as `I | undefined` is, through its optional) was a plain cast too, and for
`C | null`, a class, lowering could not make the class pointer an interface.

Both unions are stored as their one member, null a null pointer in it, so
these are the cast of that member: castToNullableUnionType takes an
interface member as it takes a class one (#665), and
castToInterfaceSpecialCases casts the member of such a union. Null stays
null.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit c42fea3 into main Oct 23, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the iface-union-cast-remap branch October 23, 2026 15:04
ASDAlexander77 added a commit that referenced this pull request Oct 23, 2026
#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 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