Skip to content

An interface is { vtable, this }: the type tag is the vtable's first slot - #668

Merged
ASDAlexander77 merged 1 commit into
mainfrom
interface-type-tag-in-vtable
Oct 23, 2026
Merged

ASDAlexander77 merged 1 commit into
mainfrom
interface-type-tag-in-vtable

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

An interface value was { vtable, this, type }: the third word was the runtime type tag of what this points at, which -mm=rc and -mm=own release and retain the object through. Every interface vtable is made for one concrete type (a class's, with a subclass getting its own, or an object literal's), so the tag now lives in the vtable. The value is two pointers again: 16 bytes, not 24.

Layout

  • Interface vtable header is { type tag, .instanceOf }, and members follow it as before:
    • INTERFACE_VTABLE_TYPE_SLOT = 0
    • INTERFACE_VTABLE_INSTANCEOF_SLOT = 1
    • INTERFACE_VTABLE_HEADER_SLOTS = 2
  • Class vtable: a class's own vtable still starts with its .instanceOf (CLASS_VTABLE_INSTANCEOF_SLOT). instanceof now names the slot of the vtable it reads.
  • Tag value: the class, object-literal and null vtable builders write it (mlirGenInterfaceVTableTypeTag). It is the type's descriptor, or null when the type owns no heap memory. It is always null in an interface's null vtable (A null class object, null or undefined given to an interface is a null interface whose methods throw #665).

Example (rc):

@Square.Shape..vtbl = { td_<hash>_class+32, @Square..instanceOf, @Rect.area, <offset of id> }
@Shape.null..vtbl   = { null, @.instanceOf.none, @Shape.null.area, null }

Changes

  • Lowering:
    • ts.NewInterface no longer makes a tag.
    • releaseViaInterfaceTag reads the tag through the vtable. A zeroed slot has no vtable and a null interface has a null tag; both release nothing.
    • A method bound through an interface takes its tag from the same vtable.
  • Interface-to-interface cast (An interface cast to one it does not extend gets a vtable laid out for it #666): the remap keeps the header, because the tag belongs to the value whatever it is seen through.
  • Own passes: the signature, closed-world and resolve passes read interface vtables past the two-slot header.
  • -mm=own borrowing records (spec 32):
    • A record box that borrows its fields is released by a routine of its own, so the tag must name that routine.
    • Its vtable is the object literal's, which every record of that shape shares.
    • The signature pass therefore gives such a box a copy of that vtable, <vtable>.r<i>...vtbl, whose descriptor names the borrowing routine (giveBorrowingRecordsTheirVTables).
  • Docs: the rc design doc (§9.31) records the move.

DefaultLib

No source change is needed: none of its C++ reads an interface's layout. I rebuilt it locally with this compiler for gc, rc, none and own (release), and ran its tests.ps1 (compile + JIT):

Model Result
gc 167/167
rc 166/167 (weakref_basic is gc-only, skipped)
none 166/167 (weakref_basic is gc-only, skipped)
own 61/167

All 105 own failures are compile-time refusals ("-mm=own cannot prove..."). The pre-#665 compiler in the main checkout gives the same first error on every one, so the change didn't cause them. There are no run errors.

Tests

  • New test: 00interface_type_tag_in_vtable.ts, registered as compile + JIT, plus the rc/none corpus. It covers a class, a subclass, an object literal, a cast to an interface the value's type doesn't extend, a copy whose original is given null, and dropping a null interface. Each object is reached only through interfaces, and churn() reuses freed blocks before the field reads.
  • Tooth: with the retain through the tag skipped, the test fails under rc at its first field check (assertion failed: a class object's field).
  • Windows ctest -C Release: 5408/5408 passed.
  • WSL (Release, ninja -j 4, ctest -j 8): 5396/5396 passed.

Not in this PR

let n: J | null = i where i: I | null and I does not extend J is a plain copy: I's vtable is used as J's with no remap, so n.id reads a method slot as a field offset and crashes. It fails the same way before this change. It is the I | null sibling of #665's C | null → I | null and needs its own fix.

🤖 Generated with Claude Code

…slot

An interface value was { vtable, this, type }: the third word was the
runtime type tag of what `this` points at, which rc and own release
and retain the object through. Every interface vtable is made for one
concrete type - a class's (a subclass has its own), an object
literal's - so the tag now lives in the vtable instead, and the value
is two pointers again (16 bytes, not 24).

The interface vtable header is { type tag, `.instanceOf` }
(INTERFACE_VTABLE_TYPE_SLOT 0, INTERFACE_VTABLE_INSTANCEOF_SLOT 1,
INTERFACE_VTABLE_HEADER_SLOTS 2); members follow it as before. A
class's own vtable still starts with its `.instanceOf`
(CLASS_VTABLE_INSTANCEOF_SLOT), so `instanceof` names the slot of the
vtable it reads.

- The class, object-literal and null vtable builders put the tag in
  (mlirGenInterfaceVTableTypeTag): the type's descriptor, or null when
  the type owns no heap memory, and in a null vtable.
- NewInterface no longer makes a tag. releaseViaInterfaceTag reads it
  through the vtable (a zeroed slot has no vtable, a null interface a
  null tag: both release nothing); a method bound through an interface
  takes the tag from the same vtable.
- A cast to an interface the value's type does not extend (#666) keeps
  the header: the tag is the value's, whatever it is seen through.
- The own signature pass reads interface vtables past the two-slot
  header.
- -mm=own: a record box that borrows its fields (spec 32) is released
  by a routine of its own, which the tag must name. Its vtable is the
  object literal's, shared by every record of its shape, so the
  signature pass gives such a box a copy of that vtable
  (`<vtable>.r<i>...vtbl`) whose descriptor names the borrowing
  routine (giveBorrowingRecordsTheirVTables).

DefaultLib needs no source change; it is rebuilt by the new compiler.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit 253ccff into main Oct 23, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the interface-type-tag-in-vtable branch October 23, 2026 14:49
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