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 @@ -5217,3 +5217,63 @@ the object out of it (`ts.GetValueFromUnion`), casts it to an opaque pointer, an
`own_union_narrowed_calls` calls the narrowed name twice with a churn between, under own, rc, gc and
none; own emits no counting. Negative: `own_err_union_narrowed_moved`, which stores the narrowed value
into a global and then calls through it.

## 62. An object literal given to a variable with no type, 2026-10-28

`const o = { w: (x: number) => x * 2 }; apply(o); o.w(1)` was `'this value' borrows a field and cannot
be stored, returned or captured` at `apply(o)`, and the read after it a use after a move: §60 left the
members of a literal with no receiver methods bound to the literal.

- **A literal a variable declared with no type is given** (MLIRGen marks it
`InternalFlags::UntypedVariableInitializer`) is one more whose this-free arrows are closures
(`markClosureMembers`), as one made for an object type is. A cast of it to an interface copies the
closure into the interface's object field by field (`const a: Adder = o`), as it copies any field.
- **Not an arrow that names a local around it** (any identifier the enclosing scopes' symbol table
holds, its own parameters included when they shadow one): copied out of the literal by such a cast,
a closure's captures would still be the literal's, freed with it (`const o = { area: () => box.w };
return o;` for a `Shape` result). It stays a method, as before.
- **Not a literal returned or given with no receiver** (`return { id, area: () => box.w }`): MLIRGen
builds it for no type and casts it to the interface, which takes the literal's methods for its
vtable; a closure field of a capture's type does not fit that slot.

`own_literal_variable_closures` gives such literals to a function-typed field and calls them after, casts
one to an interface after reading its number field, and gives one holding a class object to a
function-typed field and reads the object after, churning between steps, under own, rc, gc and none.
`00interface_type_tag_in_vtable` keeps its directly returned capturing literal.

Not here: a literal whose arrows capture a local (`const o = { w: (x: number) => x * k }; apply(o)`)
is still rejected under own. Found and fixed (every model): `<Adder>x` of an `any` that holds an object
literal read its block as the interface's pair and crashed; it now throws, since the box does not say which
fields the literal has (`00any_literal_to_interface`). A boxed null cast so was not null under rc, none and
own; it is the null interface now.

## 63. A field taken out of a temporary in a branch, 2026-10-28

`let result = initial ?? next(xs, 0).value` (DefaultLib's `reduce` shape, `value: any`) was `'this
value' borrows a field and cannot be stored, returned or captured`. The `??` builds a `ts.If`: the
branch that calls `next` reads the field, rc retains the read, the temporary is released at the end of
the branch, and the merge goes to the local.

- **The take** (`temporaryFieldTakeOf`, checkPlaceRead). A read of a field of a call's own result
(`callResultIsOwn`), with rc's retain of it right after the read, the result released once after
that retain in the same block, and no other read of that field, is a move out of the field: the walk
is made again with the retain and its taker (a local, a store) taken for uses, and nothing may use the
borrow after the taker. rc's retain goes, and right before the release a `ts.Default` of the field's
type is stored into the field, so the release frees the object and nothing of what moved. A string is
moved the same way.
- **rc, every branch retains** (`consumeConditionalResults`, OwnedReturnConsumptionPass). A branch that
yields a field read out of a temporary made in it gave the temporary back before the `ts.If` ended,
and with it what the field held; the receiver's retain after the merge came too late. rc then read
freed memory for a class or a string field (`const item = given ?? pair(n).item` read 0, and with
`let` the program died): only an `any` field, whose other branch boxed a fresh value, was retained in
the branch. Such a branch now retains what it yields, as a branch beside one that carries a fresh
value always did, and the `ts.If` carries the reference.

`own_temporary_field_take` reduces strings through `initial ?? next(xs, 0).value`, picks with a `?:`,
and gives class objects read out of temporaries in both branches of a `?:` to a `const` and a `let`,
churning between steps, under own, rc, gc and none. Teeth: without the store of the empty value, the
own build fails its count (exit 3).

Not here: the field of a named local of a class type (`const r = next(xs, 0); let v = initial ??
r.value`) is still a borrow that cannot be stored, and a parameter on the other branch (`given ??
pair(n).item`) is kept, which callers cannot give up.
10 changes: 10 additions & 0 deletions tslang/lib/TypeScript/MLIRGenCast.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2592,6 +2592,9 @@ namespace mlirgen
// - any other class of this module can't: no vtable for the interface was made for it, and none
// is made here (one made here would be a way to the class's fields that nothing else in the
// module shows), so the cast throws. A class object no class answers for throws too;
// - an object literal boxed as it is (`typeof` 'object' or 'tuple') throws: the box does not say
// which fields it has, so nothing can lay out a vtable for it;
// - null and undefined give the null interface;
// - anything else - an interface value over an object literal or a class of another module, a
// number used as a token - is read as the interface's pair, as before: an `any` holding the
// interface it is cast to round-trips.
Expand Down Expand Up @@ -2680,6 +2683,13 @@ namespace mlirgen
}

ss << S("}\n");

// an object literal, boxed as it is: the box says only that it is one, not what fields it has, so
// no vtable can be found for it here, and its block read as the interface's pair called garbage
ss << S("if (typeof a == 'object' || typeof a == 'tuple') throw \"Can't cast from any: an object literal has no vtable for interface '")
<< stows(interfaceInfo->fullName.str()) << S("'\";\n");
// null and undefined have a box of their own under the counting models, not the null pair
ss << S("if (typeof a == 'null' || typeof a == 'undefined') {\nconst n: TYPE_ANY_INTERFACE_ALIAS | null = null;\nreturn <T>n;\n}\n");
ss << S("return <T><TYPE_ANY_INTERFACE_ALIAS>a;\n}\n");

auto src = ss.str();
Expand Down
30 changes: 22 additions & 8 deletions tslang/lib/TypeScript/MLIRGenImpl.h
Original file line number Diff line number Diff line change
Expand Up @@ -11618,14 +11618,18 @@ class MLIRGenImpl
// 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.
// made for an object type is one, or the value of a variable declared with no type (`const o = { w: ... }`,
// given later to `apply(o)`), which a cast to an interface copies field by field. A literal returned or
// given with no receiver may be built for an interface, whose vtable takes 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));
auto untypedVariable = !receiverType && !!(oli.objectLiteral->internalFlags & InternalFlags::UntypedVariableInitializer);
auto objectTypeReceiver = receiverType ? isa<mlir_ts::TupleType>(MLIRTypeHelper::stripValueType(receiverType))
: untypedVariable;
for (auto &item : oli.objectLiteral->properties)
{
auto closure = false;
Expand All @@ -11635,16 +11639,26 @@ class MLIRGenImpl
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; });
auto bound = false;
FilterVisitorAST<Node> thisVisitor(SyntaxKind::ThisKeyword, [&](auto) { bound = true; });
FilterVisitorAST<Node> superVisitor(SyntaxKind::SuperKeyword, [&](auto) { bound = true; });
thisVisitor.visit(initializer);
superVisitor.visit(initializer);

// ...nor, in a variable's literal, a local around it: a closure that captures one
// is copied out of the literal by a cast to an interface, its captures still the literal's
if (untypedVariable)
{
FilterVisitorAST<Node> localVisitor(SyntaxKind::Identifier, [&](auto identifier) {
bound = bound || symbolTable.count(MLIRHelper::getName(identifier.template as<Identifier>()));
});
localVisitor.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;
closure = !bound && typed;
}
}

Expand Down
5 changes: 5 additions & 0 deletions tslang/lib/TypeScript/MLIRGenVariables.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -841,6 +841,11 @@ namespace mlirgen
}
#endif

if (!item->type && item->initializer == SyntaxKind::ObjectLiteralExpression)
{
item->initializer->internalFlags |= InternalFlags::UntypedVariableInitializer;
}

auto initFunc = [&](mlir::Location location, const GenContext &genContext) {
// A module imported as source only declares what it defines, so a module-level
// variable gets its type and no initializer code. Not a local: a function body is only
Expand Down
27 changes: 26 additions & 1 deletion tslang/lib/TypeScript/OwnedReturnConsumptionPass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1563,7 +1563,32 @@ class OwnedReturnConsumptionPass
definingOp->getParentRegion() == resultOp->getParentRegion();
};

if (llvm::none_of(yields, carriesOwned))
// ...or some branch yields a field read out of a temporary it made (`given ?? pair(n).item`):
// the temporary is given back before the branch ends, and with it what the field holds,
// so the receiver's retain after the `ts.If` comes too late. The branch retains it first.
auto borrowsBranchTemporary = [&](mlir_ts::ResultOp resultOp) {
auto value = producerOf(resultOp->getOperand(0));
while (auto loadOp = value.getDefiningOp<mlir_ts::LoadOp>())
{
auto propertyRefOp = loadOp.getReference().getDefiningOp<mlir_ts::PropertyRefOp>();
if (!propertyRefOp)
{
return false;
}

value = propertyRefOp.getObjectRef();
auto *definingOp = value.getDefiningOp();
if (definingOp && ::typescript::mayTakeOverReference(value) &&
definingOp->getParentRegion() == resultOp->getParentRegion())
{
return true;
}
}

return false;
};

if (llvm::none_of(yields, carriesOwned) && llvm::none_of(yields, borrowsBranchTemporary))
{
return;
}
Expand Down
5 changes: 5 additions & 0 deletions tslang/lib/TypeScript/OwnershipInferencePass.h
Original file line number Diff line number Diff line change
Expand Up @@ -817,6 +817,8 @@ class OwnershipInferencePass
mlir::Operation *taker = nullptr;
mlir::Value field;
mlir_ts::VariableOp slot;
// a field of a temporary (spec 63): the temporary's release, before which the field is emptied
mlir::Operation *release = nullptr;
};

// Is `kept`, what keeps the borrow `read` (a read of a field of an owning record local), one
Expand All @@ -826,6 +828,9 @@ class OwnershipInferencePass
// ...made once for each value read, with nothing using the borrow after it, and nothing reading
// the field or the record before the local ends?
bool fieldTakeHolds(mlir::Operation *read, const FieldTake &take, llvm::ArrayRef<BorrowUse> uses);
// Is `kept` rc's retain of `read`, a read of a field of a call's own result released as a
// temporary right after, with nothing else reading that field (spec 63)?
bool temporaryFieldTakeOf(mlir::Operation *read, mlir::Operation *kept, FieldTake &take);
// Can `call` be given a copy of `value`, a string read out of a place, in its place (callCopies)?
static bool copiesInto(mlir::Operation *call, mlir::Value value);
// Does `store` put `value`, a string read out of a place, into another place with no retain of
Expand Down
93 changes: 93 additions & 0 deletions tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -831,6 +831,39 @@ namespace own_inference
}
}

// ...or out of a field of a call's own result, released as a temporary right after the read
// (spec 63): `init ?? next(xs, 0).value`. The temporary frees nothing of it then, so nothing
// after the move can see the release.
if (!take.taker && kept && !countedHandle && temporaryFieldTakeOf(read, kept, take))
{
takenOps.insert(take.retain);
takenOps.insert(take.taker);
kept = walk();
takenOps.clear();
auto usedAfter = llvm::any_of(uses, [&](auto &use) {
return use.op != take.taker && use.op != take.retain && reachableAfter(take.taker, use.op, use.kills);
});
if (kept || letKept || usedAfter)
{
take = {};
kept = walk();
}
else
{
if (!dryRun)
{
mlir::OpBuilder builder(take.release);
auto fieldType = mlir::cast<mlir_ts::RefType>(take.field.getType()).getElementType();
auto empty = builder.create<mlir_ts::DefaultOp>(take.release->getLoc(), fieldType);
builder.create<mlir_ts::StoreOp>(take.release->getLoc(), empty, take.field);
}

bookkeeping.push_back(take.retain);
toErase.insert(bookkeeping.begin(), bookkeeping.end());
return true;
}
}

if (take.taker)
{
bookkeeping.push_back(take.retain);
Expand Down Expand Up @@ -1106,6 +1139,66 @@ namespace own_inference
return true;
}

bool OwnershipInferencePass::temporaryFieldTakeOf(mlir::Operation *read, mlir::Operation *kept, FieldTake &take)
{
// rc's retain of the read itself, right after it: nothing runs between the read and the move. The
// walk may meet the taker first (`r = next(xs, 0).value`). A string too: it moves, as nothing
// else holds it.
auto loadOp = mlir::dyn_cast<mlir_ts::LoadOp>(read);
auto retainOp = loadOp ? mlir::dyn_cast_or_null<mlir_ts::RetainOp>(read->getNextNode()) : mlir_ts::RetainOp();
if (!retainOp || retainOp.getReference() != loadOp.getResult() ||
(kept != retainOp && takerOf(retainOp, loadOp.getResult()) != kept))
{
return false;
}

kept = retainOp;

// a field read in place out of a call's own result, not a borrow of what the call was given
auto field = loadOp.getReference().getDefiningOp<mlir_ts::PropertyRefOp>();
auto temporary = field ? field.getObjectRef() : mlir::Value();
auto *call = temporary ? temporary.getDefiningOp() : nullptr;
if (!call || !callResultIsOwn(call) || !field->hasOneUse())
{
return false;
}

// released once, as a temporary, after the move: the release is the only end of what the field
// holds, and no other read of the result reaches that field
mlir::Operation *release = nullptr;
for (auto *user : temporary.getUsers())
{
if (mlir::isa<mlir_ts::ReleaseOp>(user) && !release)
{
release = user;
continue;
}

auto propertyRefOp = mlir::dyn_cast<mlir_ts::PropertyRefOp>(user);
if (!propertyRefOp || (propertyRefOp != field && propertyRefOp.getPosition() == field.getPosition()))
{
return false;
}
}

if (!release || release->getBlock() != kept->getBlock() || !kept->isBeforeInBlock(release))
{
return false;
}

auto *taker = takerOf(kept, loadOp.getResult());
if (!taker)
{
return false;
}

take.retain = kept;
take.taker = taker;
take.field = field.getResult();
take.release = release;
return true;
}

bool OwnershipInferencePass::keptParamOutlives(const Chain &chain, llvm::function_ref<bool(mlir::Operation *)> outlives)
{
for (auto &root : chain.roots)
Expand Down
Loading
Loading