Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5162,4 +5162,41 @@ both classes).

Not here: a function-typed field of an object literal typed by its receiver (`{ w: x => x + 1 }` given
for `{ w: (x: number) => number }`) is a method bound to the literal object, which nothing owns; own
rejects the call that receives it.
rejects the call that receives it. §60.

method is called on its class.

## 60. An object literal's arrow functions as closures, 2026-10-28

`apply({ w: x => x + 1 })` for `apply(o: { w: (x: number) => number })`, and
`const o: { w: (x: number) => number } = { w: x => x * 2 }`, were `'this value' borrows a field and
cannot be stored, returned or captured`, in every call that took such a literal. Every function member of
an object literal was a method taking the literal object as its `this`; read into the receiver's
function-typed field, it became a `hybrid_func` bound to that object, made by `ts.New` and owned by
nothing (no model released it, and several members shared it).

- **An arrow function member that never says `this` or `super`** (anywhere in it, nested functions
included: an arrow's `this` is its surroundings', and `00object_func3` relies on an arrow member's
nested function reaching the literal through `this`) is a value in a field like any other
(`markClosureMembers`, MLIRGen): made in the literal's first pass as a closure, its captures in its own
box, not in the literal's `.captured`. It is not a method: the passes that make methods skip it.
- **Its untyped parameters** take their types from the receiver's member, as a method's do
(`getObjectLiteralMemberReceiverFuncType`, set as the closure's `receiverFuncType`); an arrow with an
untyped parameter and no such member stays a method.
- **Only a literal made for an object type** (its receiver, or a generic call's parameter, is a tuple
type): a literal made for an interface, or with no receiver (`return { area: () => box.w }` for a
`Shape` result), may become an interface value, whose vtable is built from the literal's methods, so
its members stay methods.
- **Generic inference** (`tryInferInterfaceFromObject`) already tells a closure's type from a method's by
its arity.

So a literal of closures read into a function-typed field hands out closures that own their boxes,
which own handles as it does any closure (§5); none borrows the literal.

`own_literal_closures` gives such literals for a typed local, a parameter, a generic call's parameter
(`f1<T, U>`), with and without captures, several members in one literal, under own, rc, gc and none; own
emits no counting.

Not here: a function expression member, an arrow that says `this`, and any member of a literal with no
object-type receiver (`const o = { w: (x: number) => x }; apply(o)`) is a method bound to the literal as
before, and such a literal read into a function-typed field is still rejected under own.
1 change: 1 addition & 0 deletions tslang/lib/TypeScript/MLIRGenExpressions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2404,6 +2404,7 @@ namespace mlirgen
auto objectNameSymbol = mlir::FlatSymbolRefAttr::get(builder.getContext(), name);
auto objectStorageType = getObjectStorageType(objectNameSymbol);
oli.objThis = getObjectType(objectStorageType);
markClosureMembers(oli, genContext);

// add all fields
if (auto earlyResult = mlirGenObjectLiteralFields(location, oli, genContext))
Expand Down
69 changes: 63 additions & 6 deletions tslang/lib/TypeScript/MLIRGenImpl.h
Original file line number Diff line number Diff line change
Expand Up @@ -11605,8 +11605,53 @@ class MLIRGenImpl
SmallVector<size_t> methodInfos;
SmallVector<std::pair<std::string, size_t>> methodInfosWithCaptures;
SmallVector<std::pair<mlir::Attribute, mlir::Value>> fieldsToSet;
// by property: an arrow function made as a closure in a field, not a method (isClosureMember)
SmallVector<bool> closureMembers;
};

bool isClosureMember(const ObjectLiteralInfo &oli, size_t index)
{
return index < oli.closureMembers.size() && oli.closureMembers[index];
}

// An arrow function member that never says `this` or `super` (an arrow's `this` is its surroundings',
// not the literal's) is a value in a field like any other, made as a closure, not a method bound to the
// literal object: a method read out of the literal into a function-typed field (`{ w: x => x + 1 }`
// given for `{ w: (x: number) => number }`) hands out the literal, which nothing owns then. Only a literal
// made for an object type is one: a literal with no receiver may become an interface, whose vtable is
// built from the literal's methods, and so may one made for an interface. An arrow whose untyped
// parameters have no function type to take theirs from stays a method too.
void markClosureMembers(ObjectLiteralInfo &oli, const GenContext &genContext)
{
oli.closureMembers.clear();
auto receiverType = oli.receiverType ? oli.receiverType : oli.memberReceiverType;
auto objectTypeReceiver = receiverType && isa<mlir_ts::TupleType>(MLIRTypeHelper::stripValueType(receiverType));
for (auto &item : oli.objectLiteral->properties)
{
auto closure = false;
if (item == SyntaxKind::PropertyAssignment && objectTypeReceiver)
{
auto propertyAssignment = item.as<PropertyAssignment>();
Node initializer = propertyAssignment->initializer;
if (initializer == SyntaxKind::ArrowFunction)
{
auto saysThis = false;
FilterVisitorAST<Node> thisVisitor(SyntaxKind::ThisKeyword, [&](auto) { saysThis = true; });
FilterVisitorAST<Node> superVisitor(SyntaxKind::SuperKeyword, [&](auto) { saysThis = true; });
thisVisitor.visit(initializer);
superVisitor.visit(initializer);

auto arrow = initializer.as<ArrowFunction>();
auto typed = llvm::all_of(arrow->parameters, [](ParameterDeclaration parameter) { return !!parameter->type; })
|| getObjectLiteralMemberReceiverFuncType(oli, TupleFieldName(propertyAssignment->name, genContext));
closure = !saysThis && typed;
}
}

oli.closureMembers.push_back(closure);
}
}

mlir::LogicalResult addObjectFuncFieldInfo(ObjectLiteralInfo &oli, mlir::Attribute fieldId, const std::string &funcName,
mlir_ts::FunctionType funcType)
{
Expand Down Expand Up @@ -11837,16 +11882,18 @@ class MLIRGenImpl
std::optional<ValueOrLogicalResult> mlirGenObjectLiteralFields(mlir::Location location, ObjectLiteralInfo &oli,
const GenContext &genContext)
{
auto propertyIndex = 0;
for (auto &item : oli.objectLiteral->properties)
{
auto closureMember = isClosureMember(oli, propertyIndex++);
mlir::Value itemValue;
mlir::Attribute fieldId;
mlir::Type receiverElementType;
if (item == SyntaxKind::PropertyAssignment)
{
auto propertyAssignment = item.as<PropertyAssignment>();
if (propertyAssignment->initializer == SyntaxKind::FunctionExpression ||
propertyAssignment->initializer == SyntaxKind::ArrowFunction)
if ((propertyAssignment->initializer == SyntaxKind::FunctionExpression ||
propertyAssignment->initializer == SyntaxKind::ArrowFunction) && !closureMember)
{
continue;
}
Expand All @@ -11866,6 +11913,12 @@ class MLIRGenImpl
receiverTypeGenContext.receiverType = receiverElementType;
}

if (closureMember)
{
// its untyped parameters take their types from the receiver's member, as a method's do
receiverTypeGenContext.receiverFuncType = getObjectLiteralMemberReceiverFuncType(oli, fieldId);
}

auto result = mlirGen(propertyAssignment->initializer, receiverTypeGenContext);
EXIT_IF_FAILED_OR_NO_VALUE(result)
itemValue = V(result);
Expand Down Expand Up @@ -12093,14 +12146,16 @@ class MLIRGenImpl
// pass 2: register method prototypes as fields
mlir::LogicalResult mlirGenObjectLiteralMethodPrototypes(ObjectLiteralInfo &oli, const GenContext &genContext)
{
auto propertyIndex = 0;
for (auto &item : oli.objectLiteral->properties)
{
auto closureMember = isClosureMember(oli, propertyIndex++);
mlir::Attribute fieldId;
if (item == SyntaxKind::PropertyAssignment)
{
auto propertyAssignment = item.as<PropertyAssignment>();
if (propertyAssignment->initializer != SyntaxKind::FunctionExpression &&
propertyAssignment->initializer != SyntaxKind::ArrowFunction)
if ((propertyAssignment->initializer != SyntaxKind::FunctionExpression &&
propertyAssignment->initializer != SyntaxKind::ArrowFunction) || closureMember)
{
continue;
}
Expand Down Expand Up @@ -12219,13 +12274,15 @@ class MLIRGenImpl
// pass 3: generate method bodies
mlir::LogicalResult mlirGenObjectLiteralMethodBodies(ObjectLiteralInfo &oli, const GenContext &genContext)
{
auto propertyIndex = 0;
for (auto &item : oli.objectLiteral->properties)
{
auto closureMember = isClosureMember(oli, propertyIndex++);
if (item == SyntaxKind::PropertyAssignment)
{
auto propertyAssignment = item.as<PropertyAssignment>();
if (propertyAssignment->initializer != SyntaxKind::FunctionExpression &&
propertyAssignment->initializer != SyntaxKind::ArrowFunction)
if ((propertyAssignment->initializer != SyntaxKind::FunctionExpression &&
propertyAssignment->initializer != SyntaxKind::ArrowFunction) || closureMember)
{
continue;
}
Expand Down
4 changes: 2 additions & 2 deletions tslang/test/tester/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3972,7 +3972,7 @@ foreach(own_test own_fresh_string own_fresh_array own_return_new own_try_local o
own_generator_this_param own_copy_within_returned_param own_swap_elements
own_callback_forwarded own_optional_record_read own_param_default_kept own_literal_into_array
own_generator_take own_rest_array_borrows own_string_copies
own_captured_string_callback own_callback_and_call own_callback_captured
own_captured_string_callback own_callback_and_call own_callback_captured own_literal_closures
own_callback_shapes own_callback_extended own_callback_interface
own_generic_specializations own_callback_cast own_callback_interface_slot)
tslang_add_test(NAME test-jit-own-${own_test} COMMAND test-runner -jit -mm=own "${PROJECT_SOURCE_DIR}/test/tester/own/${own_test}.ts")
Expand Down Expand Up @@ -4010,7 +4010,7 @@ foreach(keeps_mm rc gc none)
own_copy_within_returned_param own_swap_elements own_callback_forwarded
own_optional_record_read own_param_default_kept own_literal_into_array
own_generator_take own_rest_array_borrows own_string_copies
own_captured_string_callback own_callback_and_call own_callback_captured
own_captured_string_callback own_callback_and_call own_callback_captured own_literal_closures
own_callback_shapes own_callback_extended own_callback_interface
own_generic_specializations own_callback_cast own_callback_interface_slot)
tslang_add_test(NAME test-jit-${keeps_mm}-${keeps_test} COMMAND test-runner -jit -mm=${keeps_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/${keeps_test}.ts")
Expand Down
54 changes: 54 additions & 0 deletions tslang/test/tester/own/own_literal_closures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// An arrow function member of an object literal that never says `this` is a closure in a field, not a
// method bound to the literal (spec 60): read into a function-typed field of the literal's receiver, it
// hands out no literal object that nothing owns. Typed by a variable, by a parameter, by a generic call's
// parameter, capturing a local or not, several in one literal. Each step churns, so a closure or capture
// freed too early reads garbage; every model runs it.
class Item {
constructor(public n: number) {}
}

function churn() {
const junk: Item[] = [];
for (let i = 0; i < 64; i++) junk.push(new Item(999));
}

function apply(o: { w: (x: number) => number }) {
return o.w(3);
}

function both(o: { w: (x: number) => number; r: () => number }) {
const r = o.r();
return o.w(r);
}

function f1<T, U>(a: { w: (x: T) => U; r: () => T }, b: T): U {
return a.w(b);
}

function test(k: number) {
let s: number = 0;
const o: { w: (x: number) => number } = { w: x => x * 2 };
churn();
s += o.w(4);
s += apply({ w: x => x + 1 });
churn();
s += apply({ w: x => x + k });
s += both({ w: x => x * 10, r: () => 2 });
churn();
s += f1({ w: x => x + 1, r: () => 0 }, 5);
const item = new Item(7);
s += apply({ w: x => x + item.n });
churn();
return s;
}

function main() {
let ok = 0;
for (let i = 0; i < 300; i++) {
if (test(2) == 8 + 4 + 5 + 20 + 6 + 10) ok++;
churn();
}

assert(ok == 300, "ok");
print("done.");
}
Loading