diff --git a/docs/own-memory-model.md b/docs/own-memory-model.md index ecb353362..23ed8ea9d 100644 --- a/docs/own-memory-model.md +++ b/docs/own-memory-model.md @@ -755,9 +755,13 @@ a reference gives it back on every path out of a function. (`s.values()` of a `Map` or a `Set`) a helper is bounded by the container too: `for (const v of s.values().take(2))` works, but the result cannot be returned or kept past `s`. An array's `values()`, `keys()`, `entries()` and `[Symbol.iterator]()` borrow it in the same - way. A closure that captures, given to two of them, is moved into the first. A `for...of` over a - generator, or a spread of one (`[...s.matchAll(re)]`), only borrows what each step yields: keeping - a yielded object or array (pushing it into another array) is an error for now. + way. A closure that captures, given to two of them, is moved into the first. What a generator + yields is the caller's: `for (const v of g()) a.push(v)` and `[...s.matchAll(re)]` take each value + over, as long as nothing reads it after it is pushed. +- Arguments given to a rest parameter (`a.concat(b)`) are borrowed for the call, so `b` is still + yours after it. +- `reduce` and `reduceRight` over an array of arrays or objects are errors: with no initial value + they start from an element, and may return it as it is, a second owner of it. - The `Array` class holds values, strings or `Shared` handles, as `Map` does: `new Array()` works, and `new Array()` is a compile error that points at `Shared`. `Array` and `Array>` do not compile yet. 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 b70c097cc..b7b79b491 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 @@ -4849,3 +4849,61 @@ not a read for a keep: `'.a' borrows an element and cannot be stored`. It now sk `isBorrow` already skips a `ts.DebugVariable` of a value. `own_for_of_nested_di` (debug build, JIT) sums the lengths of the strings of a `string[][]`. DefaultLib: `array_concat` and `owning_copies`, which failed under own in the debug pass only, compile and run. + +## 48. What a generator yields is taken out of the record `next()` returns, 2026-10-27 + +`for (const c of g()) kept.push(c)` and `[...g()]` (a spread lowers to the same loop and push) were +`borrows a field and cannot be stored`: the loop's variable borrows the `value` field of the local +the record `next()` returns is kept in, which is released and given the next record every turn. + +- **The take** (`fieldTakeOf`, `fieldTakeHolds`, checkPlaceRead). A read of a field of a local that + holds a record (a tuple), kept by one move - rc's retain and a push, a store into a place or a + global, a kept argument, or a store into an owning local declared without a value - is a move out + of the field when the taker runs once for each value read, nothing uses the borrow after it, and + nothing reads the field or the record before the local is released or given another value. The + walk is made again with the retain and the taker taken for uses, not keeps (`takenOps`); rc's + retain goes, and after the taker a `ts.Default` of the field's type is stored into the field, so + the local's release frees nothing of what moved. A value not taken (a push on some paths only) + stays in the field and goes with the record. Not a string, which is copied where it is kept. +- **The local** owns what it holds, or holds only results of calls that do not borrow: MLIRGen makes + no owner of a local at a script's top level or in a `try`, and nothing releases it there. +- **A local declared in a `try`** is declared without a value, where the cleanup can release it, and + given its value by a store right before rc's `ts.RetainSlot`. That store is now its declaration + (`slotDeclarationStore`, `declarationRetainOf`): the retained value is the stored one, the store + takes it as a declaration would (`takerAcquires`, `acquirePoint`), and a built array read + (`isBuiltArrayRead`) may be given that way. `try { const r = mk(); }` was `takes a second + reference`. +- **A global given its value once in the program's `main`** that nothing calls (a script's top + level) is a candidate immortal as one a static constructor gives it, and a class's `..new` (a + regular expression literal) makes its value as `ts.New` does. `const regexp = /a/g` at the top + level is immortal, so a call that may assign any global is no end of a borrow of it. +- **A built array stored into a global** (`const all = [...g()]` at the top level) moves in, as one + stored into a field (`isBuiltArrayReturned`). + +`own_generator_take` keeps what generators yield by push and by spread, on all paths and on some, in +and out of a `try`, churning between steps, under own, rc, gc and none; own emits no counting. +Negative: `own_err_generator_take_used_after` reads the value after pushing it. DefaultLib: +`string_matchAll` and `string_match_lastIndex` compile and run under own. + +## 49. The array a rest parameter is given borrows its elements, 2026-10-27 + +`a.concat(b)` moved `b` into the array `...other` is given (`b` was `used here after its value was +moved` after the call), and a callback's parameter given on that way (`(acc, cur) => +acc.concat(cur)`) was `a parameter, kept here, but -mm=own cannot make the callers ... give it up`. +MLIRGen releases no such array under own, so the elements moved into it leaked with it. + +`classifyArgumentArrays` takes a `ts.CreateArray` whose one use is an argument of a call that does +not keep it, and whose elements are each not fresh, or fresh and released by their owner: rc's +references for the elements go (`fieldBorrowRetains`), the array reads its elements without taking +them (`isBorrow`, `OWN_BORROWING_ARRAY_ATTR_NAME`), and a `ts.Release` marked +`OWN_SHALLOW_RELEASE_ATTR_NAME` after the call frees the array's data block and header and not the +elements (`emitReleaseArrayShallow`). Not after an invoke, a terminator. + +`own_rest_array_borrows` gives locals, the same one twice, and a callback's parameter to rest +parameters and uses them after, churning between steps, under own, rc, gc and none; own emits no +counting. DefaultLib: `a.concat(b, c)` and then `b` compile under own. + +Not here: `array_reduceRight` over an array of arrays, which may start from an element and return +it as it is, a second owner, is marked "not own". `Array` (`fill` keeps a defaulted string +parameter in a loop) and `Array>` (the class's methods hand callbacks on through a vtable, +whose families stay open) still do not compile. diff --git a/tslang/include/TypeScript/Defines.h b/tslang/include/TypeScript/Defines.h index 982905bcc..3159b2acf 100644 --- a/tslang/include/TypeScript/Defines.h +++ b/tslang/include/TypeScript/Defines.h @@ -39,6 +39,11 @@ // retain nobody performed and free live memory. #define OWNED_RESULT_ATTR_NAME "__owned_result" +// -mm=own only, set by OwnershipInferencePass for the lowering. On a `ts.Release` of an array: the +// array borrowed its elements (the array a rest parameter is given), so its release frees the +// array and not them. +#define OWN_SHALLOW_RELEASE_ATTR_NAME "__own_shallow_release" + // Marks an owned local that took its reference by consuming an OWNED_RESULT_ATTR_NAME value // instead of by retaining. The slot still releases at every scope exit - that release is what // gives the consumed reference back - so the pair is still balanced, but there is no diff --git a/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h b/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h index 3cb447026..3fdcb9edb 100644 --- a/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h +++ b/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h @@ -281,6 +281,21 @@ class OwnershipRoutineLogic FlatSymbolRefAttr::get(rewriter.getContext(), wrapperName), ValueRange{value}); } + // An array given back without what it holds (-mm=own, OWN_SHALLOW_RELEASE_ATTR_NAME): its data + // block and its header are freed, its elements are not, since it only borrowed them - the array a + // rest parameter is given. A null or static header is skipped as for any release. + void emitReleaseArrayShallow(mlir_ts::ArrayType arrayType, mlir::Value header) + { + TypeHelper th(rewriter); + ArrayLayout layout(op, rewriter, typeConverter, compileOptions); + emitIfLastReference(header, [&]() { + auto dataSlot = layout.dataAddress(arrayType, header); + auto dataValue = rewriter.create(op->getLoc(), th.getPtrType(), dataSlot); + emitFreeBlock(dataValue); + emitFreeBlock(header); + }); + } + // What destroying a block of `storageType` at `payloadPtr` releases, without freeing the block: // a promoted object's fields when its frame is done with it (ts.ReleaseContents). Each field // given back is nulled, so that the block never holds a reference it no longer owns: a second diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index 4a40194db..df75e3d22 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -496,7 +496,16 @@ class ReleaseOpLowering : public TsLlvmPattern if (tsLlvmContext->compileOptions.tracksOwnership()) { OwnershipRoutineLogic orl(op, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); - orl.emitReleaseValue(op.getReference().getType(), transformed.getReference()); + // an array that only borrowed its elements (-mm=own's classifyArgumentArrays) + auto arrayType = dyn_cast(op.getReference().getType()); + if (arrayType && op->hasAttr(OWN_SHALLOW_RELEASE_ATTR_NAME)) + { + orl.emitReleaseArrayShallow(arrayType, transformed.getReference()); + } + else + { + orl.emitReleaseValue(op.getReference().getType(), transformed.getReference()); + } } rewriter.eraseOp(op); diff --git a/tslang/lib/TypeScript/OwnershipInferenceBorrowers.cpp b/tslang/lib/TypeScript/OwnershipInferenceBorrowers.cpp index 711afaa31..054564823 100644 --- a/tslang/lib/TypeScript/OwnershipInferenceBorrowers.cpp +++ b/tslang/lib/TypeScript/OwnershipInferenceBorrowers.cpp @@ -364,6 +364,13 @@ namespace own_inference } uses.push_back({user, pending.kills, isCall(user) && isCalleeOperand(use)}); + // a move out of a record's field, its retain and its taker, which checkPlaceRead + // decides (spec 48): a use of the borrow until it is taken + if (takenOps.contains(user)) + { + continue; + } + // copied into a borrowing box: whatever runs the closure reads it if (auto found = captureStores.find(user); found != captureStores.end()) { diff --git a/tslang/lib/TypeScript/OwnershipInferenceClosures.cpp b/tslang/lib/TypeScript/OwnershipInferenceClosures.cpp index 60a9d109c..2899e5e19 100644 --- a/tslang/lib/TypeScript/OwnershipInferenceClosures.cpp +++ b/tslang/lib/TypeScript/OwnershipInferenceClosures.cpp @@ -228,6 +228,74 @@ namespace own_inference }); } + void OwnershipInferencePass::classifyArgumentArrays() + { + getFunction().walk([&](mlir_ts::CreateArrayOp arrayOp) { + // the array a rest parameter is given (`a.concat(b)`), made of values that are not its + // own to take, and given to one call that borrows it (spec 49) + auto result = arrayOp.getResult(); + if (arrayOp->getNumOperands() == 0 || !result.hasOneUse()) + { + return; + } + + auto *call = *result.getUsers().begin(); + if (!isCall(call) || call->hasTrait() || isKeptArgument(call, result) || + !llvm::is_contained(callArgs(call), result)) + { + return; + } + + llvm::SmallVector retains; + for (auto element : arrayOp->getOperands()) + { + if (holdsNoBlock(element)) + { + continue; + } + + // a fresh value nothing else releases would be the array's alone, which a release of + // the array alone leaks; a named one (`const a = new C()`) has its own release + auto root = rootOf(element); + auto releasedElsewhere = false; + forEachUse(root, [&](mlir::Operation *user, mlir::Value) { + releasedElsewhere = releasedElsewhere || mlir::isa(user); + }); + auto *retain = isFresh(root) && !releasedElsewhere ? nullptr : retainBefore(element, arrayOp); + if (!retain) + { + return; + } + + retains.push_back(retain); + } + + // nothing borrowed (an array of numbers), or a result that borrows the array and outlives + // the call: the array stays the plain value it is + auto args = callArgs(call); + auto resultBorrowsArray = llvm::any_of(resultBorrowsAll(call), [&](int index) { + return static_cast(index) < args.size() && args[index] == result; + }); + if (retains.empty() || resultBorrowsArray || (call->getNumResults() == 1 && borrowingCall(call->getResult(0)))) + { + return; + } + + // rc's references for the elements go: the array borrows them, for as long as the call + setAttrTracked(arrayOp, OWN_BORROWING_ARRAY_ATTR_NAME); + fieldBorrowRetains.insert(retains.begin(), retains.end()); + + // and the array itself is given back after the call, without what it holds + if (!dryRun) + { + mlir::OpBuilder builder(call->getContext()); + builder.setInsertionPointAfter(call); + auto releaseOp = builder.create(call->getLoc(), result); + releaseOp->setAttr(OWN_SHALLOW_RELEASE_ATTR_NAME, mlir::UnitAttr::get(&getContext())); + } + }); + } + bool OwnershipInferencePass::isBorrowingCell(mlir::Value ref) { auto cellLoad = ref.getDefiningOp(); diff --git a/tslang/lib/TypeScript/OwnershipInferenceMoves.cpp b/tslang/lib/TypeScript/OwnershipInferenceMoves.cpp index 4047003c7..215f491b5 100644 --- a/tslang/lib/TypeScript/OwnershipInferenceMoves.cpp +++ b/tslang/lib/TypeScript/OwnershipInferenceMoves.cpp @@ -41,7 +41,37 @@ namespace own_inference } auto varOp = mlir::cast(op).getSlot().getDefiningOp(); - return varOp && varOp.getInitializer() ? rootOf(varOp.getInitializer()) : mlir::Value(); + if (varOp && varOp.getInitializer()) + { + return rootOf(varOp.getInitializer()); + } + + // a local declared in a `try` is declared without a value where the cleanup can release it, + // and given its value by a store right before rc's `ts.RetainSlot` + auto storeOp = varOp ? slotDeclarationStore(op) : mlir_ts::StoreOp(); + return storeOp ? rootOf(storeOp.getValue()) : mlir::Value(); + } + + mlir::Operation *OwnershipInferencePass::declarationRetainOf(mlir::Operation *store) + { + auto *next = mlir::isa(store) ? store->getNextNode() : nullptr; + return mlir::isa_and_nonnull(next) && slotDeclarationStore(next).getOperation() == store + ? next + : nullptr; + } + + mlir_ts::StoreOp OwnershipInferencePass::slotDeclarationStore(mlir::Operation *retainSlot) + { + auto slot = mlir::cast(retainSlot).getSlot(); + auto varOp = slot.getDefiningOp(); + auto storeOp = mlir::dyn_cast_or_null(retainSlot->getPrevNode()); + if (!varOp || varOp.getInitializer() || !isOwningVariable(varOp) || !storeOp || storeOp.getReference() != slot) + { + return {}; + } + + // the local's one acquisition: an assignment after it is rc's retain, release and store + return onlyRetainSlotOf(varOp) == retainSlot ? storeOp : mlir_ts::StoreOp(); } bool OwnershipInferencePass::ownsHeap(mlir::Value value) @@ -200,9 +230,13 @@ namespace own_inference auto read = value.getDefiningOp(); auto builder = read ? read.getReference().getDefiningOp() : mlir_ts::VariableOp(); auto owner = mlir::cast(retain).getSlot().getDefiningOp(); + // the owner declared with the read, or (in a `try`) given it by the store before the retain + auto declarationStore = slotDeclarationStore(retain); + auto ownerGets = read && owner && (owner.getInitializer() == read.getResult() || + (declarationStore && declarationStore.getValue() == read.getResult())); if (!builder || isOwningVariable(builder) || builder.getCaptured().value_or(false) || !builder.getInitializer() || !mlir::isa_and_nonnull(builder.getInitializer().getDefiningOp()) || - !owner || owner.getInitializer() != read.getResult() || !read->hasOneUse() || onlyRetainSlotOf(owner) != retain) + !ownerGets || !read->hasOneUse() || onlyRetainSlotOf(owner) != retain) { return false; } @@ -243,8 +277,10 @@ namespace own_inference auto resultSlot = storeOp && storeOp.getValue() == read.getResult() ? storeOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + // ...or a field, or a global (`const all = [...g()]` at a script's top level) auto intoField = storeOp && storeOp.getValue() == read.getResult() && - storeOp.getReference().getDefiningOp(); + (storeOp.getReference().getDefiningOp() || + storeOp.getReference().getDefiningOp()); if (!((resultSlot && isResultSlot(resultSlot)) || intoField) || returned) { return false; @@ -366,6 +402,12 @@ namespace own_inference return true; } + // an array a rest parameter is given that borrows its elements (classifyArgumentArrays) + if (mlir::isa(user) && user->hasAttr(OWN_BORROWING_ARRAY_ATTR_NAME)) + { + return true; + } + // `assert(c, message)` prints the message when it fails, and keeps it either way if (mlir::isa(user)) { diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.cpp b/tslang/lib/TypeScript/OwnershipInferencePass.cpp index cb9edec2d..a22b580fa 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePass.cpp @@ -67,6 +67,8 @@ namespace own_inference classifyBorrowingFields(); // ...and whether an object literal borrows what its fields hold (spec 32) classifyRecords(); + // ...and whether an array given to a rest parameter takes its elements (spec 49) + classifyArgumentArrays(); // a kept parameter the body may not move on every path is released at its exits (spec 33) releaseKeptParamsAtExits(); auto exitReleasesGone = llvm::make_scope_exit([&]() { @@ -502,7 +504,7 @@ namespace own_inference OWN_FRESH_RESULT_ATTR_NAME, OWN_ASSIGNS_CAPTURES_ATTR_NAME, OWN_RESULT_BOUNDED_ATTR_NAME, OWN_STATE_BORROW_ATTR_NAME, - OWN_CAPTURE_BORROW_ATTR_NAME}) + OWN_CAPTURE_BORROW_ATTR_NAME, OWN_BORROWING_ARRAY_ATTR_NAME}) { op->removeAttr(name); } diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.h b/tslang/lib/TypeScript/OwnershipInferencePass.h index ec4fb2f27..23c19ddfd 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.h +++ b/tslang/lib/TypeScript/OwnershipInferencePass.h @@ -43,6 +43,9 @@ using namespace own_facts; // This pass's own mark, for its own reading: a store of a value into the capture box of a closure // that borrows what it captures (phase 5). The store is a read, not a taker. #define OWN_CAPTURE_BORROW_ATTR_NAME "__own_capture_borrow" +// -mm=own only, on a `ts.CreateArray` given to one call that borrows it: it borrows its elements +// for the call (classifyArgumentArrays) +#define OWN_BORROWING_ARRAY_ATTR_NAME "__own_borrowing_array" // Ownership inference for -mm=own, phases 0 and 1. See // docs/superpowers/specs/2026-09-24-own-memory-model-design.md, sections 4.2, 10.6, 11 and 12. @@ -231,6 +234,9 @@ class OwnershipInferencePass // A string read out of a place, given to a call that may release it (the dry run's): the call // gets a copy instead, released after it (spec 22), read -> call llvm::SmallVector> callCopies; + // A move out of a field of an owning record local, being checked (spec 48): its retain and its + // taker, which walkBorrowed takes for uses of the borrow and not for keeps + llvm::SmallPtrSet takenOps; // each closure over a cell, by its `ts.CreateBoundFunction` llvm::DenseMap closureOf; // each generator's capture box, which escapes with its state object (classifyStateBoxes) @@ -262,6 +268,9 @@ class OwnershipInferencePass // copy does (OWN_CAPTURE_BORROW_ATTR_NAME), and rc's retain for it goes; its release, when the // field is overwritten, gives nothing back. void classifyBorrowingFields(); + // The arrays a rest parameter is given that only borrow their elements for the call (spec 49): + // rc's references for the elements go, and the array is released after the call without them. + void classifyArgumentArrays(); // Is `ref` a cell read out of the `.captured` box of a generator's state object that borrows: // `ts.Load(ts.PropertyRef(ts.Load(ts.PropertyRef(state, captured)), i))`? @@ -798,6 +807,24 @@ class OwnershipInferencePass // Reports and returns false when the read is kept, or used after something that may destroy // it. bool checkPlaceRead(mlir::Operation *read, llvm::SetVector &toErase); + + // A move out of a field of an owning local that holds a record (spec 48): rc's retain of what + // is read, the one taker that keeps it, and the field, which the move leaves empty. + struct FieldTake + { + mlir::Operation *retain = nullptr; + mlir::Operation *taker = nullptr; + mlir::Value field; + mlir_ts::VariableOp slot; + }; + + // Is `kept`, what keeps the borrow `read` (a read of a field of an owning record local), one + // move of it into a container: rc's retain and a push, a store into a place or a kept argument? + bool fieldTakeOf(mlir::Operation *read, mlir::Operation *kept, FieldTake &take); + + // ...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 uses); // 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); @@ -895,6 +922,11 @@ class OwnershipInferencePass // seen through its views. None for a variable with no initializer - its storage was hoisted // in front of a try and its value arrives by a store this phase does not follow. static mlir::Value retainedValue(mlir::Operation *op); + // The store that gives a local declared without a value (one in a `try`) its value, right + // before rc's `retainSlot` of it, when that is the local's one store; null otherwise. + static mlir_ts::StoreOp slotDeclarationStore(mlir::Operation *retainSlot); + // ...and the other way round: rc's `ts.RetainSlot` right after such a store, or null. + static mlir::Operation *declarationRetainOf(mlir::Operation *store); // Does a value of this type own a block that a release would destroy? bool ownsHeap(mlir::Value value); diff --git a/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp b/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp index 0aa5dc191..04c9ac8ae 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp @@ -730,33 +730,62 @@ namespace own_inference collectCountedStoreReleases(result, countedStoreReleases); } - llvm::StringRef name = ownerName(result); + llvm::StringRef name; llvm::SmallVector uses; llvm::SmallVector bookkeeping; mlir_ts::VariableOp letKeeps; mlir::Operation *letKept = nullptr; // A result that borrows arrives with rc's temporary release, or rc's consuming `let`, and // in a function returning a borrow of the same parameter it may go out with rc's retain. - Keeping keeping{&bookkeeping, returnsBorrowOf >= 0 && borrowedParam(result) == returnsBorrowOf}; - auto *kept = walkBorrowed(result, read, uses, [&](mlir::Operation *user, - llvm::ArrayRef kills) { - auto borrower = borrowerOf(user, /*consumed=*/true); - if (!borrower || borrower.getOperation() != user) - { - return false; - } + auto walk = [&]() { + name = ownerName(result); + uses.clear(); + bookkeeping.clear(); + letKeeps = {}; + letKept = nullptr; + Keeping keeping{&bookkeeping, returnsBorrowOf >= 0 && borrowedParam(result) == returnsBorrowOf}; + return walkBorrowed(result, read, uses, [&](mlir::Operation *user, llvm::ArrayRef kills) { + auto borrower = borrowerOf(user, /*consumed=*/true); + if (!borrower || borrower.getOperation() != user) + { + return false; + } + + name = varName(borrower); + if (auto *keeps = collectBorrowerUses(borrower, kills, uses, bookkeeping); keeps && !letKept) + { + letKept = keeps; + letKeeps = borrower; + } + + return true; + }, keeping, countedHandle); + }; - name = varName(borrower); - if (auto *keeps = collectBorrowerUses(borrower, kills, uses, bookkeeping); keeps && !letKept) + auto *kept = walk(); + + // A value taken out of a field of an owning record local by one move, the field read again + // by nothing before the local ends (spec 48): the `value` of the record `next()` returns, in + // a `for...of` or a spread that keeps what a generator yields + FieldTake take; + if ((kept || letKept) && !countedHandle && fieldTakeOf(read, kept ? kept : letKept, take)) + { + takenOps.insert(take.retain); + takenOps.insert(take.taker); + kept = walk(); + takenOps.clear(); + if (kept || letKept || !fieldTakeHolds(read, take, uses)) { - letKept = keeps; - letKeeps = borrower; + take = {}; + kept = walk(); } + } - return true; - }, keeping, countedHandle); - - if (kept || letKept) + if (take.taker) + { + bookkeeping.push_back(take.retain); + } + else if (kept || letKept) { // a string given to a call that keeps it: the call takes a copy (spec 22) if (dryRun && kept && isCall(kept) && copiesInto(kept, result)) @@ -833,10 +862,192 @@ namespace own_inference } } + // the taker has the value now: the field is left empty, so the local's release frees nothing + // of it + if (take.taker && !dryRun) + { + mlir::OpBuilder builder(take.taker->getContext()); + builder.setInsertionPointAfter(take.taker); + auto fieldType = mlir::cast(take.field.getType()).getElementType(); + auto empty = builder.create(take.taker->getLoc(), fieldType); + builder.create(take.taker->getLoc(), empty, take.field); + } + toErase.insert(bookkeeping.begin(), bookkeeping.end()); return true; } + bool OwnershipInferencePass::fieldTakeOf(mlir::Operation *read, mlir::Operation *kept, FieldTake &take) + { + // a field of an owning local that holds a record (a tuple), read in place: the local's release + // frees what the field holds, so a move out of it leaves the field empty. Not a string, which + // is copied where it is kept (spec 22). + auto loadOp = mlir::dyn_cast(read); + auto field = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); + auto slot = field ? field.getObjectRef().getDefiningOp() : mlir_ts::VariableOp(); + auto slotType = slot ? mlir::dyn_cast(slot.getType()) : mlir_ts::RefType(); + // MLIRGen makes no owner of a local at a script's top level or in a `try`: nothing releases + // what it holds. A move out of it frees nothing twice when every value it is given is a call's + // own result, not a borrow of a record someone else holds. + auto ownsOrHoldsResults = [&]() { + if (isOwningVariable(slot)) + { + return true; + } + + llvm::SmallVector given; + if (slot.getInitializer()) + { + given.push_back(slot.getInitializer()); + } + + for (auto *user : slot->getUsers()) + { + if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getReference() == slot.getResult()) + { + given.push_back(storeOp.getValue()); + } + } + + return !given.empty() && llvm::all_of(given, [&](mlir::Value value) { + auto *def = value.getDefiningOp(); + return def && isCall(def) && !borrowingCall(value); + }); + }; + + if (!slot || slot.getCaptured().value_or(false) || !ownsOrHoldsResults() || !slotType || + !mlir::isa(slotType.getElementType()) || isCopyableString(read->getResult(0).getType())) + { + return false; + } + + // rc's retain for the taker, and the taker: the retain's value is the read, or a read of a + // local that borrows it (`for (const v of g()) a.push(v)`) + // ...or a store into an owning local declared without a value (in a `try`), and rc's + // `ts.RetainSlot` of it: the local owns what it is given, and its own check decides the rest + if (auto storeOp = mlir::dyn_cast(kept); storeOp && rootOf(storeOp.getValue()) == read->getResult(0)) + { + auto owner = storeOp.getReference().getDefiningOp(); + auto *retainSlot = owner && isOwningVariable(owner) && !owner.getCaptured().value_or(false) && !owner.getInitializer() + ? onlyRetainSlotOf(owner) + : nullptr; + if (!retainSlot || retainSlot->getBlock() != storeOp->getBlock() || !storeOp->isBeforeInBlock(retainSlot)) + { + return false; + } + + take.retain = retainSlot; + take.taker = storeOp; + take.field = field.getResult(); + take.slot = slot; + return true; + } + + auto retainOp = mlir::dyn_cast(kept); + mlir::Value value; + if (retainOp) + { + value = retainOp.getReference(); + } + else + { + for (auto operand : kept->getOperands()) + { + for (auto *user : rootOf(operand).getUsers()) + { + if (auto found = mlir::dyn_cast(user); found && !retainOp) + { + retainOp = found; + value = found.getReference(); + } + } + } + } + + auto *taker = retainOp ? takerOf(retainOp, value) : nullptr; + if (!taker || (!retainOp && taker != kept)) + { + return false; + } + + // a move into a container: an item pushed, a store into a place or a global, a call that keeps it + auto movesIn = false; + if (auto pushOp = mlir::dyn_cast(taker)) + { + movesIn = llvm::is_contained(pushOp.getItems(), value); + } + else if (auto storeOp = mlir::dyn_cast(taker)) + { + movesIn = storeOp.getValue() == value && + (isPlace(storeOp.getReference()) || storeOp.getReference().getDefiningOp()); + } + else if (isCall(taker)) + { + movesIn = isKeptArgument(taker, value); + } + + if (!movesIn || retainOp->getBlock() != taker->getBlock() || !retainOp->isBeforeInBlock(taker)) + { + return false; + } + + take.retain = retainOp; + take.taker = taker; + take.field = field.getResult(); + take.slot = slot; + return true; + } + + bool OwnershipInferencePass::fieldTakeHolds(mlir::Operation *read, const FieldTake &take, llvm::ArrayRef uses) + { + auto slot = take.slot; + auto ends = slotEnds(slot.getResult()); + // the taker runs once for each value read: never again before the field is read anew + llvm::SmallVector again(ends.begin(), ends.end()); + again.push_back(read); + // (from the op after it, around a loop: reachableAfter takes an op and itself for reachable) + auto *next = take.taker->getNextNode(); + if (!next || reachableAfter(next, take.taker, again)) + { + return false; + } + + // nothing uses the borrow after the taker has it + for (auto &use : uses) + { + if (use.op != take.taker && use.op != take.retain && reachableAfter(take.taker, use.op, use.kills)) + { + return false; + } + } + + // and nothing reads the field, or the whole record, before the local ends + auto position = mlir::cast(take.field.getDefiningOp()).getPosition(); + for (auto *user : slot->getUsers()) + { + llvm::SmallVector reads; + if (mlir::isa(user)) + { + reads.push_back(user); + } + else if (auto propertyRefOp = mlir::dyn_cast(user); + propertyRefOp && propertyRefOp.getPosition() == position) + { + reads.append(propertyRefOp->getUsers().begin(), propertyRefOp->getUsers().end()); + } + + for (auto *fieldRead : reads) + { + if (reachableAfter(take.taker, fieldRead, ends)) + { + return false; + } + } + } + + return true; + } + bool OwnershipInferencePass::keptParamOutlives(const Chain &chain, llvm::function_ref outlives) { for (auto &root : chain.roots) diff --git a/tslang/lib/TypeScript/OwnershipInferenceReadsAfterMove.cpp b/tslang/lib/TypeScript/OwnershipInferenceReadsAfterMove.cpp index d5699e09e..25813b56b 100644 --- a/tslang/lib/TypeScript/OwnershipInferenceReadsAfterMove.cpp +++ b/tslang/lib/TypeScript/OwnershipInferenceReadsAfterMove.cpp @@ -178,6 +178,12 @@ namespace own_inference return retain; } + // a store that declares a local in a `try`: the local acquires it at rc's `ts.RetainSlot` + if (auto *retainSlot = declarationRetainOf(taker)) + { + return retainSlot; + } + mlir::Operation *first = taker; forEachUse(value, [&](mlir::Operation *user, mlir::Value) { if (mlir::isa(user) && user->getBlock() == taker->getBlock() && @@ -209,6 +215,12 @@ namespace own_inference [](mlir::Operation *user) { return mlir::isa(user); }); } + // the store that declares a local in a `try`, rc's `ts.RetainSlot` right after it + if (declarationRetainOf(taker)) + { + return true; + } + auto acquires = false; forEachUse(value, [&](mlir::Operation *user, mlir::Value) { acquires = acquires || (mlir::isa(user) && user->getBlock() == taker->getBlock() && diff --git a/tslang/lib/TypeScript/OwnershipSignatureGlobals.cpp b/tslang/lib/TypeScript/OwnershipSignatureGlobals.cpp index e587678d1..17a9f31b8 100644 --- a/tslang/lib/TypeScript/OwnershipSignatureGlobals.cpp +++ b/tslang/lib/TypeScript/OwnershipSignatureGlobals.cpp @@ -195,12 +195,14 @@ namespace own_signature continue; } - // the one store, in a static constructor, run once + // the one store, in a static constructor or the program's `main` that nothing + // calls (a script's top level), run once auto storeOp = mlir::dyn_cast(addressUser); auto funcOp = storeOp ? storeOp->getParentOfType() : mlir_ts::FuncOp(); + auto runsOnce = funcOp && (constructors.contains(funcOp.getSymName()) || + (funcOp.getSymName() == MAIN_ENTRY_NAME && usesOf.lookup(MAIN_ENTRY_NAME).empty())); if (initStore || !storeOp || storeOp.getReference() != addressOfOp.getResult() || - storeOp.getValue() == addressOfOp.getResult() || !funcOp || !constructors.contains(funcOp.getSymName()) || - mayRunAgain(storeOp)) + storeOp.getValue() == addressOfOp.getResult() || !runsOnce || mayRunAgain(storeOp)) { return; } @@ -229,7 +231,13 @@ namespace own_signature llvm::StringSet<> immortal; auto madeHere = [&](mlir::Value value) { auto root = rootOf(value); - if (!root.getDefiningOp() || isCall(root.getDefiningOp()) || !isFresh(root)) + // a class's `..new` (a regular expression literal is `RegExp..new`) makes the object as + // `ts.New` does: a library's is a declaration, which runOnOperation marks fresh only after + // this runs + auto *def = root.getDefiningOp(); + auto callee = def ? directCallee(def) : mlir::FlatSymbolRefAttr(); + auto constructs = callee && callee.getValue().ends_with("." NEW_METHOD_NAME); + if (!def || (isCall(def) ? !constructs : !isFresh(root))) { return false; } diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 29e10747c..3c75bfcde 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -3939,7 +3939,8 @@ foreach(own_test own_fresh_string own_fresh_array own_return_new own_try_local o own_loop_read_again own_length_drops_elements own_tuple_param_kept own_for_of_borrowed_iterator own_generator_keeps own_generator_keeps_bounded 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_callback_forwarded own_optional_record_read own_param_default_kept own_literal_into_array + own_generator_take own_rest_array_borrows) tslang_add_test(NAME test-jit-own-${own_test} COMMAND test-runner -jit -mm=own "${PROJECT_SOURCE_DIR}/test/tester/own/${own_test}.ts") tslang_add_test(NAME test-compile-own-${own_test} COMMAND test-runner -mm=own "${PROJECT_SOURCE_DIR}/test/tester/own/${own_test}.ts") # The promise: no counting at all in what own emits. @@ -3973,7 +3974,8 @@ endforeach() foreach(keeps_mm rc gc none) foreach(keeps_test own_generator_keeps own_generator_keeps_bounded 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_optional_record_read own_param_default_kept own_literal_into_array + own_generator_take own_rest_array_borrows) 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") tslang_add_test(NAME test-compile-${keeps_mm}-${keeps_test} COMMAND test-runner -mm=${keeps_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/${keeps_test}.ts") endforeach() @@ -4668,7 +4670,8 @@ set(own_error_cases "own_err_swap_call_borrow_used_after|:15:11: error: '[^']*' borrows an element but is used here after it may be released or overwritten" "own_err_callback_forwarded_drops|:15:17: error: 'this value' borrows an element but is used here after it may be released or overwritten" "own_err_callback_forwarded_keeps|:32:18: error: '[^']*' keeps '#0' for good, but -mm=own cannot make every caller make it immortal: the function is called through a value" - "own_err_optional_record_field_kept|:15:12: error: '[^']*' borrows the result of 'findIn' and cannot be stored, returned or captured") + "own_err_optional_record_field_kept|:15:12: error: '[^']*' borrows the result of 'findIn' and cannot be stored, returned or captured" + "own_err_generator_take_used_after|:14:9: error: '[^']*' borrows a field and cannot be stored, returned or captured") foreach(own_error_case ${own_error_cases}) string(FIND "${own_error_case}" "|" own_error_sep) string(SUBSTRING "${own_error_case}" 0 ${own_error_sep} own_error_name) diff --git a/tslang/test/tester/own/own_err_generator_take_used_after.ts b/tslang/test/tester/own/own_err_generator_take_used_after.ts new file mode 100644 index 000000000..33283e9b8 --- /dev/null +++ b/tslang/test/tester/own/own_err_generator_take_used_after.ts @@ -0,0 +1,17 @@ +// A yielded value pushed into an array and read again after: the push does not take it, since the +// read after would be through what the array now owns, so the value still borrows the record. +class C { + constructor(public x: number) {} +} + +function* made(n: int) { + for (let i = 0; i < n; i++) yield new C(i); +} + +function main() { + const kept: C[] = []; + for (const c of made(2)) { + kept.push(c); + print(c.x); + } +} diff --git a/tslang/test/tester/own/own_generator_take.ts b/tslang/test/tester/own/own_generator_take.ts new file mode 100644 index 000000000..2fa5fda35 --- /dev/null +++ b/tslang/test/tester/own/own_generator_take.ts @@ -0,0 +1,82 @@ +// What a generator yields is the caller's: a for...of or a spread that keeps it - pushes it into +// an array - moves it out of the record `next()` returns, and the record's release then frees +// nothing of it (spec 48). A value not kept stays in the record and goes with it. A local declared +// in a `try` (given its value by a store, where the cleanup can release it) owns what it is given +// as any local does. Each step churns, so a value freed too early reads garbage; every model runs it. +class C { + constructor(public x: number) {} +} + +function label(i: number) { + return "s" + i; +} + +function churn() { + const junk: C[] = []; + for (let i = 0; i < 64; i++) junk.push(new C(999)); +} + +function* made(n: int) { + for (let i = 0; i < n; i++) yield new C(i); +} + +function* rows(n: int) { + for (let i = 0; i < n; i++) yield [label(i), label(i + 1)]; +} + +function pushed(n: int) { + const kept: C[] = []; + for (const c of made(n)) kept.push(c); + return kept; +} + +function evens(n: int) { + const kept: C[] = []; + for (const c of made(n)) { + if (c.x % 2 == 0) kept.push(c); + } + + return kept; +} + +function spread(n: int) { + const all = [...made(n)]; + return all; +} + +function inTry(n: int) { + let count = 0; + try { + const all = [...rows(n)]; + churn(); + count = all.length + all[n - 1][1].length; + const kept = pushed(n); + count += kept.length; + } catch { + count = -1; + } + + return count; +} + +function main() { + let ok = 0; + for (let i = 0; i < 300; i++) { + const a = pushed(4); + churn(); + if (a.length == 4 && a[3].x == 3) ok++; + const e = evens(5); + churn(); + if (e.length == 3 && e[2].x == 4) ok++; + const s = spread(3); + churn(); + if (s.length == 3 && s[1].x == 1) ok++; + const r = [...rows(2)]; + churn(); + if (r[1][0] == "s1" && r[1][1] == "s2") ok++; + if (inTry(3) == 3 + 2 + 3) ok++; + } + + assert(ok == 300 * 5, "ok"); + print("done."); +} diff --git a/tslang/test/tester/own/own_rest_array_borrows.ts b/tslang/test/tester/own/own_rest_array_borrows.ts new file mode 100644 index 000000000..165fcbfb1 --- /dev/null +++ b/tslang/test/tester/own/own_rest_array_borrows.ts @@ -0,0 +1,46 @@ +// The array a rest parameter is given (`total(a, b)`) only borrows its elements when the callee does +// not keep the parameter: rc's references for them go, the arguments stay their owners' and are used +// after the call, and the array is given back after it without them (spec 49). A parameter given on +// to a rest parameter (a callback's `cur`) is borrowed as well. Each step churns, so a value freed +// too early reads garbage; every model runs it. +class C { + constructor(public x: number) {} +} + +function churn() { + const junk: C[] = []; + for (let i = 0; i < 64; i++) junk.push(new C(999)); +} + +function total(...items: C[]) { + let n: number = 0; + for (const c of items) n += c.x; + return n; +} + +function lengths(this: int[][], ...other: int[][]) { + let n = this.length; + for (const o of other) n += o.length; + return n; +} + +function main() { + let ok = 0; + for (let i = 0; i < 300; i++) { + const a = new C(i); + const b = new C(1); + if (total(a, b) == i + 1) ok++; + churn(); + if (a.x == i && b.x == 1 && total(b, b, a) == i + 2) ok++; + + const xs = [[1, 2]]; + const ys = [3, 4, 5]; + const f = (acc: int[][], cur: int[]) => acc.lengths(cur); + if (xs.lengths(ys) == 4 && f(xs, ys) == 4) ok++; + churn(); + if (ys.length == 3 && ys[2] == 5) ok++; + } + + assert(ok == 300 * 4, "ok"); + print("done."); +}