Repository navigation
A class method can fill an interface's function-typed field - #612
Merged
Merged
Conversation
`interface HasFn { fn: () => int }` with `class C { fn(): int }` failed at the
cast (`const o: HasFn = new C(42)`): mlirGenClassVirtualTableDefinitionForInterface
looked the interface's field up among the class's fields only, reported
"field type not matching for "fn"", and the caller then saw the cascade
"not a function to call". TS accepts a method for a function-typed property.
The class has no field holding the method, so the slot can't be the usual
this-relative offset. It now holds the address of a private global cell
(`<C>.<I>..vtbl.<field>.method`) holding the method, plus
INTERFACE_METHOD_CELL_TAG (bit 0, free in every function-typed slot: field
offsets and the cell are pointer-aligned). InterfaceSymbolRefOpLowering reads
the cell when the bit is set, ahead of the optional-storage tag. The read in
InterfaceFieldAccess is unchanged (Load + CreateBoundFunction over
ExtractInterfaceThis), so the rc receiver tag work applies to it as is.
A write through such a field changes the shared cell, i.e. the method for
every instance seen through that interface.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An interface inheriting the same function-typed field from two parents gives the field two vtable slots; registering a cell per slot failed with "redefinition of symbol". The test now also covers `implements`, a base class that implements the interface, and the diamond. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed field (#613) * A class field holding a function can fill an interface's function-typed field `class W { fn: () => int = () => 7 }` cast to `interface HasFn { fn: () => int }` failed with "field type not matching": the class stores the field as a hybrid function ({this-or-null, fn}), while the interface slot is read as a function taking the object as its `this`. Neither a null-this hybrid (no `this` parameter) nor a bound one (its own receiver) can be read that way. The field callback of mlirGenClassVirtualTableDefinitionForInterface now accepts a class field of any function kind whose signature matches, and synthesizes a private thunk `<Class>.<Iface>..vtbl.<field>.thunk(this, ...)` that calls `this.<field>(...)`. The thunk fills the slot through the method cell added for class methods (INTERFACE_METHOD_CELL_TAG), so the field is read at every call and a later assignment to it is seen through the interface. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Cover an unannotated function-typed class field filling an interface field `fn = () => 6` is stored as a plain function rather than a hybrid one; it reaches the interface through the same thunk. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Under -mm=rc a bound method does not hold its receiver yet: `return o.fn` from a function that made `new C(42)` handed back a value pointing into a freed instance. Linux CI's JIT rc run read the freed block (assert in 00interface_function_typed_field_class.ts); with glibc's tcache off the class-field test crashed calling through the freed field. The caller now owns the instance, as the rest of these tests already do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner
Author
|
Linux CI failure ( |
…esent (#614) * Reading an optional function-typed interface field binds `this` when present `interface M { fn?: () => int }`: `m.fn` loads as optional<fn>, and InterfaceFieldAccess bound the interface's `this` to that optional directly, which failed verification ('ts.CreateBoundFunction' op operand #1 must be ...) for every implementer, object literal included - `const g = m.fn`, `m.fn?.()`, `m.fn !== undefined` and `return m.fn` all hit it. A present field now reads as optional<bound function> (bound to ExtractInterfaceThis), an absent one as undefined. The lowering needed no change: a -1 slot already gives a null address and a class's method cell (INTERFACE_METHOD_CELL_TAG) its cell, and an optional load of a null address is undefined. The test covers object literals with and without the field, a class whose method fills it and a class lacking it, a mixed array at one access site, the bound value returned as `(() => int) | undefined`, and a field taking a parameter (AOT, JIT, rc/none corpus). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * A class's optional function field stays an error for an interface's optional one Since class fields holding a function can fill an interface's function-typed field (a thunk in the shared method cell), `class C { fn?: () => int }` cast to `interface M { fn?: () => int }` took that path too: isAnyFunctionType strips the optional. The cell is the same for every instance, so the field read as present whether it was set or not, and an unset one was called. The thunk path now skips an optional class field, which stays "field type not matching" as before, until the cell can carry a per-instance presence check. A compile-error test pins it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keep the receiver alive where the optional-field test returns a bound field Under -mm=rc a bound method does not hold its receiver yet, so `getFn(new WithFn(13))` handed back a value pointing into a freed instance (as #612's tests did, fixed there in f12b53f). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 19, 2026
ASDAlexander77
added a commit
that referenced
this pull request
Oct 19, 2026
Two class-field bugs with no interface involved: - `fn = () => 0` was stored as a plain function, which has no room for a closure's context: assigning a capturing closure to it later warned "losing this reference" and the call crashed (0x80000003). A field whose function type is inferred is now stored as a hybrid function, as an annotated `fn: () => int` field and a `let` (adjustLocalVariableType) are. - `fn = this.m` had no type in the discovery pass: the fields are laid out before the methods are declared, so the field became `any` there and `w.fn()` failed with "not a function to call" before the real pass ran. Such a field now holds its place and is typed again once the members are declared (mlirGenClassRetypeFields); the code made against the placeholder is thrown away with the pass. The real pass already typed it as the bound method. The field type computation moved into mlirGenClassDataFieldType so both places share it. 00class_function_typed_field_inferred.ts covers both forms, reassignment, a closure over the bound-method field and the interface path of #612, under gc AOT/JIT and the rc/none corpus. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
ASDAlexander77
added a commit
that referenced
this pull request
Oct 19, 2026
`interface HasFn { fn: () => int }` with `const o = { fn: cl }`, `cl` a
capturing closure: `const h: HasFn = o; h.fn()` printed garbage. The
literal stores the closure as a bound function (its `this` is the capture
tuple), the interface slot is read as a function taking the object as its
`this`. The cast cloned the object with the interface's field type, and
bound_func -> func<opaque> dropped the captures, so the call bound the
object where the captures belonged. A hybrid field (a type-literal
annotated one) crashed the same way once it captured, an interface method
(`fn(): int`) did too, and a plain function (`{ add: plusOne }`) received
the object as its first argument.
Such a field - bound, hybrid, or a plain function without a `this`
parameter (MLIRTypeHelper::isObjectFieldFunctionWithoutThis) - now matches
the interface's function-typed member as it is, and keeps its own type in
a clone and in a literal built for the interface. The object's vtable
calls it through a thunk, `<vtable>.<field>.thunk(this, ...)`, that reads
the field and calls it (generateSynthMethodToCallField, now taking the
`this` type, shared with classes): a method's slot holds the thunk, a
function-typed field's slot a tagged method cell holding it, as for a
class since #612. As there, a write through the interface changes the
shared cell.
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.
interface HasFn { fn: () => int }withclass C { fn(): int { ... } }failed at the cast (const o: HasFn = new C(42)) withfield type not matching for "fn", and callers then saw the cascade "not a function to call". TypeScript accepts a method for a function-typed property.Change
mlirGenClassVirtualTableDefinitionForInterface(MLIRGenClasses.cpp): when the interface field has no class field of that name, a non-static method with a matching signature fills it (the method'sthisis skipped in the match).<C>.<I>..vtbl.<field>.method(which holds the method), plusINTERFACE_METHOD_CELL_TAG(Defines.h, bit 0). Bit 0 is free in every function-typed slot: field offsets and the cell are pointer-aligned.InterfaceSymbolRefOpLowering(LowerToLLVM.cpp) reads the cell when the bit is set. It tests the bit beforeINTERFACE_OPTIONAL_STORAGE_TAG, which a 32-bit cell address can carry; -1 (absent) is still tested first.extendshas two slots and one cell.InterfaceFieldAccessis unchanged (Load +CreateBoundFunctionoverExtractInterfaceThis).Known limitation: a write through such a field (
o.fn = g) changes the shared cell, i.e. the method for every instance of the class seen through that interface.Tests
00interface_function_typed_field_class.ts(gc AOT/JIT + rc/none corpus): structural cast, the returned bound value, parameters, an inherited method,implements, a base class that implements the interface, the diamond, and one call site shared by a class and an object literal. Full Release ctest passed on each commit.A stacked follow-up (a class field holding a function) builds on this cell.
🤖 Generated with Claude Code