Skip to content

instanceof: slot 0 of every class and interface vtable is .instanceOf - #407

Merged
ASDAlexander77 merged 3 commits into
mainfrom
fix-instanceof-opaque-vtable
Sep 29, 2026
Merged

ASDAlexander77 merged 3 commits into
mainfrom
fix-instanceof-opaque-vtable

Conversation

@ASDAlexander77

@ASDAlexander77 ASDAlexander77 commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

Asking what an object is (instanceof, casting down to a class, unboxing an any) reads .instanceOf from slot 0 of the vtable. That slot was not always there.

1. A class that implements an interface

A root class put its interface vtable pointers first, so slot 0 held an interface pointer and not .instanceOf. Unboxing such an object from any (<B>anyValue) called through the wrong pointer and crashed under every memory model. A root class now reserves slot 0 for .instanceOf before its interfaces.

2. An interface value

Every interface vtable now starts with a header slot: slot 0 is the implementer's .instanceOf. An object literal has no class to ask, so it gets a generated .instanceOf.none that returns false.

  • Members keep their logical indexes. Only the vtable builders and InterfaceSymbolRef lowering add INTERFACE_VTABLE_HEADER_SLOTS (1), so the index arithmetic for extends is unchanged.

  • i instanceof B on an interface was decided at compile time and was always false. It now asks the vtable.

  • <B>i, i as B, and narrowing by instanceof crashed the compiler with an index assert in castFieldsToClass. They now return the very object when it is a B. When it is not a B, the cast still builds a new B from the interface's fields, but only if every field of B is in the interface. Otherwise it throws Can't cast from interface.

  • <B>a, where a is an any holding an interface, threw Can't cast from any type. ___unbox now has an interface branch. An any holding an object literal throws a catchable cast error.

  • own: borrowedParam sees through the interface cast and ExtractInterfaceThis, and isInstanceOfSlot accepts the interface vtable form. Interface values themselves stay rejected under -mm=own, as before.

  • rc: OwnedReturnConsumptionPass reads an interface call's possible implementations from the vtable globals, and now skips the header slot too. Reading at the member's own index looked at the member before it: i.make() (member 0) asked .instanceOf and leaked every result (4.6 → 10.8 MB). New test: own/rc_interface_call_owned_result.ts (JIT, AOT, and an IR check that every interface call returning a class carries __owned_result).

ABI

Both vtable layouts change. A prebuilt DefaultLib must be rebuilt with this compiler; its StringIterator implements ClassIterator.

Tests

  • New: 00any_unbox_interface_class.ts and 00interface_instanceof.ts, each compile + JIT.
  • Windows Release: 3236/3236.
  • Linux (WSL): 3220/3220.
  • DefaultLib rebuilt with this compiler: 157/157 (release, compile + JIT).
  • -mm=own corpus: unchanged at 303 compiling files.

🤖 Generated with Claude Code

ASDAlexander77 and others added 3 commits September 29, 2026 20:08
… interface

An object reached with no static type - an `any` being unboxed, `instanceof`
on an opaque value - is asked what it is through slot 0 of its vtable
(mlirGenInstanceOfOpaque), which MLIRGen already said "must be first element in
VTable for Cast Any". getVirtualTable put a root class's interface vtables
first, so for `class B implements I` slot 0 held I's vtable and `<B>anyValue`
called it as a function: a segfault under every memory model.

A root class now reserves slot 0 for .instanceOf ahead of its interfaces, and a
derived class overrides it in place. This changes the vtable layout of every
root class that implements an interface, so a prebuilt default library (its
StringIterator implements ClassIterator) has to be rebuilt; DefaultLib rebuilt
with this compiler passes 157/157 release compile and jit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…header

OwnedReturnConsumptionPass decides whether a call through an interface
hands back a +1 by reading slot `index` of every vtable global for that
interface. The op's index is the member's; the global now has the
.instanceOf header in front, so the lookup read the member before it.
Member 0 asked .instanceOf, which returns no heap value, and a call to
it kept its reference: a loop of `i.make()` grew from 4.6 MB to 10.8 MB
under rc. Other members were right only by coincidence of their
neighbour's classification.

The lookup adds INTERFACE_VTABLE_HEADER_SLOTS. A new rc test checks that
no interface call returning a class is left without __owned_result; a
generator in it keeps the whole-module shortcut from hiding the lookup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit a563884 into main Sep 29, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the fix-instanceof-opaque-vtable branch September 29, 2026 21:09
ASDAlexander77 added a commit that referenced this pull request Sep 29, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ASDAlexander77 added a commit that referenced this pull request Sep 29, 2026
* rc: a getter read is settled like a call

A getter retains what it returns, as every function does (§9.24), but
OwnedReturnConsumptionPass only looked at ts.CallIndirect. A getter read
is one of the accessor ops (ts.Accessor, ThisAccessor, ThisIndirect*,
BoundIndirect*) until the affine lowering turns it into a call, so its
+1 was never taken over or given back: `h.cc.x` leaked the C, and
`let b = h.cc` retained a second time. own_getter_borrow read 12.6 MB
under rc; it now reads 4.8 MB, as gc's 6.5 and own's 4.8 do.

The accessor ops are classified as calls are - the getter named outright,
every method of that member name for a virtual slot, the vtables for an
interface slot, or the whole-module answer - and then marked, consumed
by their receiver, or released at the end of the block.

A getter read with no live use is left alone: an assignment through an
accessor builds a read before rebuilding the op as a setter, and a
release would keep that read, a call the program never made.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Spec 15.3: rc's getter-temporary leak is fixed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Spec 15.7: the getter-temporary leak is no longer a known limit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Spec 15.7: the <B>anyValue segfault was fixed by #407

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
ASDAlexander77 added a commit that referenced this pull request Sep 29, 2026
…helper

#407 included MLIRRTTIHelperVC.h after MLIRGenImpl.h, and the helper
undefines DEBUG_TYPE on the way out, so every LLVM_DEBUG in the file was
an error in a Debug build (Release compiles LLVM_DEBUG away, so CI never
saw it). MLIRGenStatements.cpp does the same after the same include.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ASDAlexander77 added a commit that referenced this pull request Sep 30, 2026
…411)

* A type alias that contains itself is an error, not a stack overflow

`type Node = [value: s32, next: Reference<Node>]`, `{ value: number,
next: Node }`, mutual aliases, a generic `List<T>` inside itself: every
mention of an alias expanded its declaration again, and one that names
itself did so until the stack ran out - exit 0xC00000FD, no message.

An alias being expanded is now remembered (expandTypeAlias, used by the
plain and both generic resolutions), and one that meets itself is
reported: "type alias 'Node' circularly references itself; use an
interface or a class for a recursive type". A type here has a fixed shape
- a tuple holds its fields in place - so none can contain itself, not
even behind Reference<T>, whose tuple type would have to name itself; an
interface or a class is the recursive type.

The errors that only follow from it are left out: those raised while the
expansion unwinds (a scoped handler swallows them until the outermost
expansion of that alias ends), and "can't find type" / "can't resolve
name" / "generic type ... can't be found" at later mentions of it. An
exported alias is registered before it is resolved, so it is found - and
found circular - the same way.

Tests: nine shapes in type-alias-circular/ (tuple through Reference,
object, optional field, declared function parameter, mutual, exported,
generic, union through an array, in a namespace), each passing only on
the message and failing on the follow-on ones; and positive.ts, aliases
naming other aliases and themselves one after another, JIT and AOT.

Fixes #370.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Debug build: MLIRGenCast.cpp defines DEBUG_TYPE again after the RTTI helper

#407 included MLIRRTTIHelperVC.h after MLIRGenImpl.h, and the helper
undefines DEBUG_TYPE on the way out, so every LLVM_DEBUG in the file was
an error in a Debug build (Release compiles LLVM_DEBUG away, so CI never
saw it). MLIRGenStatements.cpp does the same after the same include.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
ASDAlexander77 added a commit that referenced this pull request Sep 30, 2026
…ed once; Debug DEBUG_TYPE fix (#412)

* Debug build: MLIRGenCast.cpp defines DEBUG_TYPE again after the RTTI helper

#407 included MLIRRTTIHelperVC.h after MLIRGenImpl.h, and the helper
undefines DEBUG_TYPE on the way out, so every LLVM_DEBUG in the file was
an error in a Debug build (Release compiles LLVM_DEBUG away, so CI never
saw it). MLIRGenStatements.cpp does the same after the same include.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* A function's attributes name each attribute once

`@dllname("strlen") @Linkname("strlen")` both become DLL_NAME, and
processFunctionAttributes pushed it once per decorator. Two entries of one
name are an assert in a Debug build ("DictionaryAttr element names must be
unique", test-{compile,jit}-declare-linkname-shared-symbol) and print twice
in Release. checkLinkNameDecorators has already made sure they agree, so the
first stands; the same holds for any decorator written twice.

New test: --emit=mlir of declare_linkname_shared_symbol.ts fails on a
repeated dllname.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Implement process-heap allocator for Windows modules and integrate with MLIR passes

---------

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