Repository navigation
Reading an optional function-typed interface field binds this when present - #614
Merged
ASDAlexander77 merged 3 commits intoOct 19, 2026
Conversation
…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>
…ptional 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>
ASDAlexander77
force-pushed
the
fix-optional-interface-fn-field
branch
from
October 19, 2026 12:37
6ce182c to
8ade6e6
Compare
ASDAlexander77
added a commit
that referenced
this pull request
Oct 19, 2026
* A class method can fill an interface's function-typed field
`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>
* One method cell for a field reached twice through a diamond extends
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>
* A class field holding a function can fill an interface's function-typed 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>
* Keep the receiver alive where the interface tests return a bound field
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>
* Reading an optional function-typed interface field binds this when present (#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>
---------
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.
Stacked on #612 (
fix-class-method-interface-fn-field); retarget tomainonce #612 merges.Reading an optional function-typed interface field
interface M { fn?: () => int }:m.fnloads asoptional<fn>, andInterfaceFieldAccessbound the interface'sthisto that optional directly, which failed verification for every implementer, object literal included:It broke
const g = m.fn,m.fn?.(),m.fn !== undefinedandreturn m.fn.A present field now reads as
optional<bound function>(bound toExtractInterfaceThis), an absent one asundefined. MLIRGen only: the lowering already gives a null address for a-1slot and the cell for anINTERFACE_METHOD_CELL_TAGslot, and an optional load of a null address isundefined. The bind sits in onebindThislambda for both paths.A class's optional function field stays an error
#613's thunk path also took
class C { fn?: () => int }, becauseisAnyFunctionTypestrips the optional. The method cell is shared by every instance, so the field read as present whether set or not, and an unset one was called. The thunk path now skips optional class fields; they stay "field type not matching", as before #613, until the cell can carry a per-instance presence check.Tests
00interface_optional_function_typed_field.ts(AOT, JIT, rc/none corpus): object literal with and without the field,thiskept, a class method filling it (the method-cell path) and a class lacking it, a mixedMaybeFn[]read at one site, the bound value returned as(() => int) | undefined, a field taking a parameter. Failed to compile before the fix.interface-errors/optional_class_function_field.ts: the compile error above.--verify-ownershipclean on the new test.ctest -C Release -j 16 --timeout 300: 4730/4730 (3 usual not-run).Notes
fix-rc-bound-this-moregives one conflict hunk inInterfaceFieldAccess: put itsOWNS_RECEIVER_ATTR_NAMEsetAttrinsidebindThis.MLIRGenImpl.hgenerateSynthMethodToCallField(index < getNumInputs()) comes from A class field holding a function can fill an interface's function-typed field #613, not this PR.🤖 Generated with Claude Code