diff --git a/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md b/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md index 7ae60fa58..cee30af12 100644 --- a/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md +++ b/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md @@ -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`), 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. diff --git a/tslang/lib/TypeScript/MLIRGenExpressions.cpp b/tslang/lib/TypeScript/MLIRGenExpressions.cpp index 13fc862ec..405cbcadd 100644 --- a/tslang/lib/TypeScript/MLIRGenExpressions.cpp +++ b/tslang/lib/TypeScript/MLIRGenExpressions.cpp @@ -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)) diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 25f30d97c..027703f30 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -11605,8 +11605,53 @@ class MLIRGenImpl SmallVector methodInfos; SmallVector> methodInfosWithCaptures; SmallVector> fieldsToSet; + // by property: an arrow function made as a closure in a field, not a method (isClosureMember) + SmallVector 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(MLIRTypeHelper::stripValueType(receiverType)); + for (auto &item : oli.objectLiteral->properties) + { + auto closure = false; + if (item == SyntaxKind::PropertyAssignment && objectTypeReceiver) + { + auto propertyAssignment = item.as(); + Node initializer = propertyAssignment->initializer; + if (initializer == SyntaxKind::ArrowFunction) + { + auto saysThis = false; + FilterVisitorAST thisVisitor(SyntaxKind::ThisKeyword, [&](auto) { saysThis = true; }); + FilterVisitorAST superVisitor(SyntaxKind::SuperKeyword, [&](auto) { saysThis = true; }); + thisVisitor.visit(initializer); + superVisitor.visit(initializer); + + auto arrow = initializer.as(); + 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) { @@ -11837,16 +11882,18 @@ class MLIRGenImpl std::optional 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(); - if (propertyAssignment->initializer == SyntaxKind::FunctionExpression || - propertyAssignment->initializer == SyntaxKind::ArrowFunction) + if ((propertyAssignment->initializer == SyntaxKind::FunctionExpression || + propertyAssignment->initializer == SyntaxKind::ArrowFunction) && !closureMember) { continue; } @@ -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); @@ -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(); - if (propertyAssignment->initializer != SyntaxKind::FunctionExpression && - propertyAssignment->initializer != SyntaxKind::ArrowFunction) + if ((propertyAssignment->initializer != SyntaxKind::FunctionExpression && + propertyAssignment->initializer != SyntaxKind::ArrowFunction) || closureMember) { continue; } @@ -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(); - if (propertyAssignment->initializer != SyntaxKind::FunctionExpression && - propertyAssignment->initializer != SyntaxKind::ArrowFunction) + if ((propertyAssignment->initializer != SyntaxKind::FunctionExpression && + propertyAssignment->initializer != SyntaxKind::ArrowFunction) || closureMember) { continue; } diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index b71f551cc..1a6f92c45 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -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") @@ -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") diff --git a/tslang/test/tester/own/own_literal_closures.ts b/tslang/test/tester/own/own_literal_closures.ts new file mode 100644 index 000000000..19755aded --- /dev/null +++ b/tslang/test/tester/own/own_literal_closures.ts @@ -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(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."); +}