Repository navigation
An object's closure field keeps its captures through an interface - #617
Merged
Merged
Conversation
`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>
#615 made a method cell {method, flag offset}: an object's thunk cell now has that layout too, with -1 (always present), so a read of an optional interface field finds its offset where it looks for it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CMakeLists.txt: both sides added a test at the same place; both kept. 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.
Problem
The literal stores
clas a bound function (itsthisis the capture tuple); the interface slot is read as a function taking the object as itsthis(func<opaque, si32>). The cast cloned the object with the interface's field type, andbound_func -> func<opaque>dropped the captures, so the call bound the object where the captures belonged.Probing turned up related cases with the same cause:
const o: { add: (a: int) => int } = { add: (a) => a + k }) holds a hybrid function: it worked only while it captured nothing, and crashed once it didfn(): int) filled by the closure field: garbage{ add: plusOne }): got the object as its first argumentreturn { fn: cl }from a function returningHasFn) cast the closure while building the literalFix
A field holding a function the interface can't call with the object as
this(bound, hybrid, or a plain function without athisparameter:MLIRTypeHelper::isObjectFieldFunctionWithoutThis):getInterfaceVirtualTableForObject), and keeps its own type in a clone (getInterfaceCloneFields) and in a literal built for the interface (addObjectFieldInfo)<vtable>.<field>.thunk(this, ...) { return this.<field>(...) }(generateSynthMethodToCallField, which now takes thethistype and is shared with the class path from A class method can fill an interface's function-typed field #612). A method's slot holds the thunk, and a function-typed field's slot holds a method cell tagged withINTERFACE_METHOD_CELL_TAG, as A class method can fill an interface's function-typed field #612 does for classes. The per-cast method patch skips these slots.Object-literal methods (
fn() {...}, or arrows written inside the literal) take the object, so they keep the direct path.The object's cell uses the
{method, flag offset}layout from #615, with -1 (always present). Writing through the interface stays an error ("saving to constant object"), as #615 made it.Tests
00interface_function_typed_field_object_closure.ts(AOT and JIT, plus rc/none corpus) covers: a capturing closure in a field and a method, a live capture (k = 10is seen), a closure with a parameter, a hybrid annotated field, plain functions, a clone forced by another field, a literal typed by the interface, an escaping closure returned from a function and used in an array, an optional field, and an object-literal method. It failed before the fix (garbage, then an assert).Full
ctest -C Release -j 16 --timeout 300: 4736/4736 passed on the first push (2 skipped, 1 disabled, as on main), and 4742/4742 after merging main (#615).🤖 Generated with Claude Code