Repository navigation
An interface cast to one it does not extend gets a vtable laid out for it - #666
Merged
Merged
Conversation
…r it An interface value keeps the vtable it was made with, laid out for its own interface. Cast to another interface that it does not extend - one with only some of its members, in other places - the value was a plain ts.Cast, so the other interface read that vtable by its own slot numbers: `HasDist.dist` read `Hit.thing`, its first slot, and `B.g()` called `A.f()`. Every model, gc too. The cast now carries, for each slot of the target's vtable, the slot of the source's that holds the same member (INTERFACE_VTABLE_MAP_ATTR_NAME), and lowering builds the target's vtable from the value's own one. A vtable with the same content is built once and kept in a list for as long as the program runs (ts_remap_vtable, generated into the module), so a cast in a loop allocates nothing after the first and nothing has to free one. The cast stays a ts.Cast, so the ownership passes see it as before. A cast to an interface the value extends, or to one laid out the same, is unchanged. A slot is copied as it is, so a member that is a method of one and a field of the other, or a field of another type, is an error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…error tslang has always let `tube = ball` through (a Ball having no `length`), and structural-typing.ts and typeGuardOfFormThisMember.ts rely on it. The remapped vtable gives such a member -1, as an absent optional one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 23, 2026
ASDAlexander77
added a commit
that referenced
this pull request
Oct 23, 2026
…slot (#668) 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
added a commit
that referenced
this pull request
Oct 23, 2026
…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>
ASDAlexander77
added a commit
that referenced
this pull request
Oct 23, 2026
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
added a commit
that referenced
this pull request
Oct 23, 2026
…ace-to-interface cast (#670) `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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up 6 of
.superpowers/own-raytrace-rulings.md: an object literal re-viewed as another interface read wrong offsets. It was wider than filed: every cast from one interface to another that it does not extend kept the value's own vtable, and the target interface read it by its own slot numbers. Every model, gc included:A cast to an interface the value extends worked, because the base's members come first.
Fix
castInterfaceToInterface): for each slot of the target's vtable, find the slot of the source's that holds the same member. If that is every slot in place (anextendsbase, or the same layout), the cast is unchanged. Otherwise thets.Castcarries the map (__vtable_map). Lowering builds the target's vtable from the value's own one. A slot is copied as it is, so the cast is rejected when a member is a method of one interface and a field of the other, or when a field has another type. A member the source lacks reads as absent (-1). tslang has always compiledtube = ball, where Ball has nolength, andstructural-typing.tsrelies on that.ts_remap_vtable(vtable, map, count)is generated into the module with internal linkage, so it needs no runtime-library symbol under any model, JIT or AOT. A vtable with the same content is built once and kept in a list for as long as the program runs. A cast in a loop therefore allocates nothing after the first time, and nothing has to free a vtable. The list is pushed with a compare-exchange. A null interface stays null.ts.Cast, so the rc and own passes see it exactly as before.Tests
tests/00interface_cast_unrelated.tscovers:<T>, and of an element;extends;It runs under gc and in the rc/none corpus.
Tooth: with the new path turned off, gc prints
1(thing) where3(dist) is right, and1 | 1where2 | 20is right for the methods.Two compile errors, in
interface-cast/method_field.tsandinterface-cast/field_type.ts.Not covered
S | null→T | null). A null class object, null or undefined given to an interface is a null interface whose methods throw #665 handles a class inside a union.Verification
MALLOC_PERTURB_=165 MALLOC_CHECK_=3.lib.ts,lib.win32.ts) under gc, rc, none and own.🤖 Generated with Claude Code