Skip to content

Cast from any to an interface asks the object what class it is - #712

Merged
ASDAlexander77 merged 1 commit into
mainfrom
fix-any-to-interface-cast
Oct 28, 2026
Merged

ASDAlexander77 merged 1 commit into
mainfrom
fix-any-to-interface-cast

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Problem

<Holder><any>r, where r is a Runner over a Walker (which never becomes a Holder in the module), compiled to a plain unbox. The box's payload was read as Holder's (this, vtable) pair, so calling h.f(...) went through Runner's vtable and crashed the JIT with 0xC0000005 in every memory model. A class object boxed into any and cast to an interface was read the same wrong way.

An any records only the kind of what it holds ("interface", "class", ...), not which interface an interface value was made for. So there is no run-time lookup by interface name.

Fix

castToInterfaceSpecialCases now sends any → interface to castAnyToInterface. That function generates ___unbox_interface<I>, which asks the object behind the box what class it is (instanceof):

  • A class with a vtable for I gives I through that vtable, from a class object or an interface value. That covers a class that declares implements I, and one that some cast in the module turns into I. Those casts are recorded in classInterfaceVTablesCast from the discovery pass on, so a cast later in the file still counts.
  • Any other class of this module throws Can't cast from any: class 'X' does not implement interface 'I'. A class object no class answers for also throws. No vtable is made here, so the cast adds no new way to reach a class's fields (the -mm=own §58 concern).
  • Anything else is read as before: an interface value over an object literal or another module's class, or a number used as a token (29lazyreferences). So a value boxed as the interface it is cast to still round-trips (00any_types).

The repro now throws a catchable error in every model instead of crashing.

Notes

  • Under -mm=own, the result is a borrow of a call result, the same as the existing <C>a unbox. So function f(a: any) { const h = <Holder>a; h.f(x); } is rejected the way const w = <Walker>a; const h: Holder = w; h.f(x) already was. Before this change the plain cast escaped that check.
  • Possible follow-up: build a structural Walker.Holder vtable so that <Holder><any>runner works as TypeScript allows. That vtable would be a new path to the class's fields, so it is left out here.

Tests

  • New 00any_to_interface.ts: another interface's value, a class object, a declared implementer, a vtable made by a cast later in the module, and a round-trip. It runs under gc (compile and JIT), the rc/none corpus, and -mm=own (compile and JIT). The unpatched compiler crashes on it.
  • ctest -C Release: 6055/6055 pass.

🤖 Generated with Claude Code

`<Holder><any>runner` compiled to a plain unbox: the box's payload was read as
Holder's (this, vtable) pair, so a Runner value (Runner's vtable) or a class
object in the box was called through garbage - an access violation in every
memory model. An `any` records only the kind of what it holds, not which
interface an interface value was made for, so there is no run-time lookup by
interface name.

castAnyToInterface generates `___unbox_interface<I>`, which asks the object
behind the box (`instanceof`):
- a class with a vtable for I - it declares `implements I`, or some cast in the
  module makes one (classInterfaceVTablesCast, recorded from the discovery pass
  on, so a cast later in the module counts) - gives I through that vtable, from
  a class object or an interface value;
- any other class of this module throws "class 'X' does not implement
  interface 'I'"; no vtable is made for it here, so no new way to reach a
  class's fields appears;
- anything else (an interface value over an object literal or another module's
  class, a number used as a token) is read as before, so a value boxed as the
  interface it is cast to round-trips.

Test: 00any_to_interface (gc, rc, none, own; compile and JIT).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit 436ee5a into main Oct 28, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the fix-any-to-interface-cast branch October 28, 2026 10:08
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