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 a3aa8f239..0e738b468 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 @@ -1626,7 +1626,7 @@ place, so all four places that report it get the same names. Each message has a | a global's value, stored, returned or boxed | `'x' borrows the global 'g' and cannot be stored, returned or captured` | `own_err_global_returned` | | a global's value read into a local | `'x' borrows the global 'g', but -mm=own cannot prove the global keeps its value while 'x' is used: a call or an assignment may overwrite it` | `own_err_global_let_call` | | a captured variable's value, through its cell | `'x' borrows a captured variable and cannot be stored, returned or captured` | `own_err_captured_returned` | -| an object literal holding a value it does not own outright: a literal nested by value, or one holding a borrowed or `let` field when it is not seen as an interface (§32; since §27, before which any literal given anywhere but an owning local or the result was one) | `an object literal that holds a value it owns is moved only into a local or returned as it is; -mm=own cannot move it here yet` | the interface-typed shapes are borrowing records (§32): `own_err_borrowing_record_*`, `own_literal_moves`, `own_borrowing_record_arg`; `own_err_record_literal_let` (§27) is gone | +| an object literal holding a value it does not own outright: a literal nested by value, or one holding a borrowed or `let` field when it is not seen as an interface (§32; since §27, before which any literal given anywhere but an owning local or the result was one) | `an object literal that holds a value it owns is moved only into a local or returned as it is; -mm=own cannot move it here yet` | `own_err_record_literal_nested` (nested by value), `own_err_record_literal_let_field` (a `let`'s value, not an interface); the interface-typed shapes are borrowing records (§32): `own_err_borrowing_record_*`, `own_literal_moves`, `own_borrowing_record_arg`; `own_err_record_literal_let` (§27) is gone | The mixed-return message is the largest bucket (17 files). Most are two synthesized helpers: - `___unbox` (`` of an `any`) returns the box's own string when it holds one, and a @@ -2978,7 +2978,9 @@ either: - or rejected: "the value read from 'G' is kept where its end at an assignment of 'G' cannot be checked; -mm=own cannot prove it outlives the global's value" - a field of an object, an array, a global, a callee that keeps it, a capture, a return. A string read is copied where it is kept - (§22), so its keeps are not this check's. + (§22), so its keeps are not this check's. This is a backstop: almost every keep fails an earlier + rule first. One shape is known to reach it alone, `k.w = M.w` with `k: typeof M` (a static kept + through a vtable slot), pinned by `own_err_global_kept_typeof`. Tests: `own_err_global_let_holder_static` (a static block's loop), `own_err_global_let_destructure` (`let { v } = { v: K.w }` in a top-level loop) and `own_err_global_let_holder` (a module global), @@ -3943,8 +3945,9 @@ is gone; its shape is in `own_borrowing_record_arg`. `own_err_lends_two_params` of 'pick' names 'a', 'b', but its body returns a borrow of 'a'`. A method's `@lends` reaches its class's declaration as a 64-bit mask, so since the final review (M4, 2026-10-23) an index past 63 is an error rather than a borrow an importer silently never learns of: `own_err_lends_index_64` (a -method of 64 parameters lending its last, argument 64), "'@lends' of 'K.pick' names its argument -64, but a method's '@lends' can name only its arguments 0 to 63". The compiler before it accepted +method of 64 parameters lending its last, argument 64), "'@lends' of 'K.pick' names 'p63', but a +method's '@lends' can name only its first 64 arguments, the receiver among them" (the parameter by +its name since the follow-up; it read "names its argument 64" before). The compiler before it accepted the program; the 63rd parameter (argument 63) is still accepted, under own, rc and gc. Teeth (temporary `OWN_TEETH` switches, removed): with the per-site routine dropped (every record's diff --git a/tslang/lib/TypeScript/CMakeLists.txt b/tslang/lib/TypeScript/CMakeLists.txt index dbfc61897..988e91784 100644 --- a/tslang/lib/TypeScript/CMakeLists.txt +++ b/tslang/lib/TypeScript/CMakeLists.txt @@ -33,7 +33,24 @@ add_mlir_dialect_library(MLIRTypeScript RelocateConstantPass.cpp OwnershipVerifierPass.cpp OwnershipInferencePass.cpp + OwnershipInferenceBorrowers.cpp + OwnershipInferenceClosures.cpp + OwnershipInferenceDiagnostics.cpp + OwnershipInferenceEdgeReleases.cpp + OwnershipInferenceGlobalReads.cpp + OwnershipInferenceKeptParams.cpp + OwnershipInferenceMoves.cpp + OwnershipInferencePlaceReads.cpp + OwnershipInferenceReadsAfterMove.cpp + OwnershipInferenceRecords.cpp OwnershipSignaturePass.cpp + OwnershipSignatureCallbacks.cpp + OwnershipSignatureClosedWorld.cpp + OwnershipSignatureGenerators.cpp + OwnershipSignatureGlobals.cpp + OwnershipSignatureInterfaces.cpp + OwnershipSignatureRecords.cpp + OwnershipSignatureResolve.cpp OwnedReturnConsumptionPass.cpp StackPromotionPass.cpp BorrowFactsExportPass.cpp diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index ce82cca98..09233e7e1 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -12723,8 +12723,22 @@ class MLIRGenImpl // 64 bits: an index past it would be a borrow an importer never learns of if (index >= 64) { - emitError(location) << "'@lends' of '" << funcOp.getSymName() << "' names its argument " << index - << ", but a method's '@lends' can name only its arguments 0 to 63"; + // the parameter by the name `@lends` gave it (processOwnParamMarkers) + auto names = funcOp->getAttrOfType(OWN_DECLARED_PARAM_NAMES_ATTR_NAME); + auto name = names && index < static_cast(names.size()) + ? mlir::dyn_cast(names[index]) + : mlir::StringAttr(); + auto diag = emitError(location) << "'@lends' of '" << funcOp.getSymName() << "' names "; + if (name) + { + diag << "'" << name.getValue() << "'"; + } + else + { + diag << "its argument " << index; + } + + diag << ", but a method's '@lends' can name only its first 64 arguments, the receiver among them"; return false; } diff --git a/tslang/lib/TypeScript/OwnershipFacts.h b/tslang/lib/TypeScript/OwnershipFacts.h index 9501ade8a..abb342acf 100644 --- a/tslang/lib/TypeScript/OwnershipFacts.h +++ b/tslang/lib/TypeScript/OwnershipFacts.h @@ -421,13 +421,60 @@ inline bool isInterfaceField(mlir::Value ref) return ref.getDefiningOp() && mlir::isa(ref.getType()); } +// The kinds of place (isPlace), and the value each is reached through (placeBase). +enum class PlaceKind +{ + None, + Field, // `o.f`: the object + Element, // `a[i]`: the array + SharedValue, // `s.value`, a handle's payload (spec 23.1): the handle + InterfaceField // `i.f`, a field reached through an interface (spec 28): the interface value +}; + +struct PlaceBase +{ + mlir::Value base; + PlaceKind kind = PlaceKind::None; + + explicit operator bool() const + { + return kind != PlaceKind::None; + } +}; + +// The one definition of a place: every walk from a place up to what holds it starts here, so a +// new kind of place is seen by all of them. Each walk decides for itself what a kind means. +inline PlaceBase placeBase(mlir::Value ref) +{ + auto *def = ref.getDefiningOp(); + if (auto propertyRefOp = mlir::dyn_cast_or_null(def)) + { + return {propertyRefOp.getObjectRef(), PlaceKind::Field}; + } + + if (auto elementRefOp = mlir::dyn_cast_or_null(def)) + { + return {elementRefOp.getArray(), PlaceKind::Element}; + } + + if (auto sharedValueRefOp = mlir::dyn_cast_or_null(def)) + { + return {sharedValueRefOp.getShared(), PlaceKind::SharedValue}; + } + + if (auto interfaceRefOp = mlir::dyn_cast_or_null(def); interfaceRefOp && isInterfaceField(ref)) + { + return {interfaceRefOp.getInterfaceVal(), PlaceKind::InterfaceField}; + } + + return {}; +} + // A field, an element, or `s.value` - the payload of a handle (spec 23.1) - and a field reached // through an interface (spec 28). inline bool isPlace(mlir::Value ref) { - return mlir::isa_and_nonnull( - ref.getDefiningOp()) || - isInterfaceField(ref); + return static_cast(placeBase(ref)); } // The storage a reference addresses - a local's `ts.Variable` or a global's `ts.AddressOf` - itself @@ -679,22 +726,14 @@ inline bool reachesSharedValue(mlir::Value ref) continue; } - if (value.getDefiningOp()) + if (auto place = placeBase(value)) { - return true; - } + if (place.kind == PlaceKind::SharedValue) + { + return true; + } - if (auto propertyRefOp = value.getDefiningOp()) - { - work.push_back(propertyRefOp.getObjectRef()); - } - else if (auto elementRefOp = value.getDefiningOp()) - { - work.push_back(elementRefOp.getArray()); - } - else if (auto interfaceRefOp = value.getDefiningOp(); interfaceRefOp && isInterfaceField(value)) - { - work.push_back(interfaceRefOp.getInterfaceVal()); + work.push_back(place.base); } else if (auto loadOp = value.getDefiningOp(); loadOp && isPlace(loadOp.getReference())) { @@ -733,11 +772,21 @@ inline mlir_ts::LoadOp slotLoadOf(mlir::Value value) return loadOp; } +// The ownership passes ask MLIRTypeHelper about types only, under the default options. +inline MLIRTypeHelper typeHelper(mlir::MLIRContext *context) +{ + return MLIRTypeHelper(context, CompileOptions{}); +} + // Does a value of this type own a block that a release would destroy? +inline bool ownsHeap(mlir::Location location, mlir::Type type) +{ + return typeHelper(type.getContext()).ownsHeapMemory(location, type); +} + inline bool ownsHeap(mlir::Value value) { - MLIRTypeHelper mth(value.getContext(), CompileOptions{}); - return mth.ownsHeapMemory(value.getLoc(), value.getType()); + return ownsHeap(value.getLoc(), value.getType()); } // Holds no block: a number, `null` or `undefined` widened, an empty optional, a string literal. @@ -811,8 +860,7 @@ inline bool isCopyableString(mlir::Type type) } // only a union that lowers to one pointer: a tagged union is a struct - MLIRTypeHelper mth(type.getContext(), CompileOptions{}); - return !mth.isUnionTypeNeedsTag(mlir::UnknownLoc::get(type.getContext()), unionType); + return !typeHelper(type.getContext()).isUnionTypeNeedsTag(mlir::UnknownLoc::get(type.getContext()), unionType); } return isText(type); @@ -976,7 +1024,7 @@ inline bool isOwningRecord(mlir::Value value) return false; } - MLIRTypeHelper mth(value.getContext(), CompileOptions{}); + auto mth = typeHelper(value.getContext()); for (auto *user : varOp->getUsers()) { auto propertyRefOp = mlir::dyn_cast(user); @@ -1175,19 +1223,11 @@ inline int borrowedParam(mlir::Value value, int depth = 0) return borrowedParam(unboxOp.getIn(), depth + 1); } - if (auto propertyRefOp = mlir::dyn_cast_or_null(def)) - { - return borrowedParam(propertyRefOp.getObjectRef(), depth + 1); - } - - if (auto elementRefOp = mlir::dyn_cast_or_null(def)) - { - return borrowedParam(elementRefOp.getArray(), depth + 1); - } - - if (auto interfaceRefOp = mlir::dyn_cast_or_null(def); interfaceRefOp && isInterfaceField(value)) + // a field, an element, a field through an interface: the argument's block holds them. A + // handle's payload is any handle's, no parameter's. + if (auto place = placeBase(value); place && place.kind != PlaceKind::SharedValue) { - return borrowedParam(interfaceRefOp.getInterfaceVal(), depth + 1); + return borrowedParam(place.base, depth + 1); } if (auto loadOp = mlir::dyn_cast_or_null(def)) diff --git a/tslang/lib/TypeScript/OwnershipInferenceBorrowers.cpp b/tslang/lib/TypeScript/OwnershipInferenceBorrowers.cpp new file mode 100644 index 000000000..e93ff2f03 --- /dev/null +++ b/tslang/lib/TypeScript/OwnershipInferenceBorrowers.cpp @@ -0,0 +1,483 @@ +// OwnershipInferencePass (see OwnershipInferencePass.h): locals that borrow - the borrower of a +// value, and every use of the borrow up to where the owner ends; a local lent to a call and +// given a new value before the call takes it. + +#include "OwnershipInferencePass.h" + +#define DEBUG_TYPE "own" + +namespace own_inference +{ + + void OwnershipInferencePass::checkArgumentsAssigned() + { + getFunction().walk([&](mlir::Operation *call) { + if (!isCall(call)) + { + return mlir::WalkResult::advance(); + } + + for (auto argument : callArgs(call)) + { + // a method's object: `this` of the method bound to it + if (auto getThisOp = argument.getDefiningOp()) + { + auto *boundOp = getThisOp->getOperand(0).getDefiningOp(); + if (mlir::isa_and_nonnull(boundOp)) + { + argument = boundOp->getOperand(0); + } + } + + auto *read = argument.getDefiningOp(); + auto local = mlir::dyn_cast_or_null(slotOf(argument)); + if (!read || !local || !ownsHeap(argument) || isHandle(argument)) + { + continue; + } + + auto assignedByClosure = closureAssigns(local); + auto *write = findBetween(read, call, [&](mlir::Operation *op) { + if (assignedByClosure && isCall(op)) + { + return true; + } + + auto storeOp = mlir::dyn_cast(op); + auto releaseSlotOp = mlir::dyn_cast(op); + return (storeOp && storeOp.getReference() == local.getResult()) || + (releaseSlotOp && releaseSlotOp.getSlot() == local.getResult()); + }); + if (!write) + { + continue; + } + + if (reporting()) + { + auto name = varName(local); + auto callee = calleeName(call); + auto diag = call->emitError("'") + << name << "' is lent to '" << (callee.empty() ? "a function" : callee) + << "', but is given a new value before the call takes it; -mm=own cannot prove " + "it outlives the call"; + diag.attachNote(write->getLoc()) << "'" << name << "' is given a new value here"; + signalPassFailure(); + } + + return mlir::WalkResult::interrupt(); + } + + return mlir::WalkResult::advance(); + }); + } + + bool OwnershipInferencePass::closureAssigns(mlir_ts::VariableOp local) + { + if (!local.getCaptured().value_or(false)) + { + return false; + } + + for (auto *user : local->getUsers()) + { + auto storeOp = mlir::dyn_cast(user); + if (mlir::isa(user) || + (storeOp && storeOp.getReference() == local.getResult())) + { + continue; + } + + // the cell put into a closure's capture box + auto propertyRefOp = storeOp ? storeOp.getReference().getDefiningOp() + : mlir_ts::PropertyRefOp(); + auto boundOp = propertyRefOp ? closureOfBox(propertyRefOp.getObjectRef()) : mlir_ts::CreateBoundFunctionOp(); + if (!boundOp) + { + return true; + } + + auto assigned = boundOp->getAttrOfType(OWN_ASSIGNS_CAPTURES_ATTR_NAME); + if (assigned && llvm::is_contained(assigned.asArrayRef(), static_cast(propertyRefOp.getPosition()))) + { + return true; + } + } + + return false; + } + + mlir_ts::VariableOp OwnershipInferencePass::borrowerOf(mlir::Operation *acquisition, bool consumed) + { + mlir_ts::VariableOp varOp; + if (auto retainSlotOp = mlir::dyn_cast(acquisition)) + { + varOp = retainSlotOp.getSlot().getDefiningOp(); + } + else if (auto retainOp = mlir::dyn_cast(acquisition)) + { + // rc's reference for a `let` that takes the value over: `let ri = raw` retains the + // interface and declares `ri` consuming it + varOp = consumingLetOf(retainOp.getReference()); + consumed = true; + } + else + { + varOp = mlir::dyn_cast(acquisition); + } + + if (!varOp || !varOp.getInitializer() || !isOwningVariable(varOp) || varOp.getCaptured().value_or(false)) + { + return {}; + } + + if (varOp->hasAttr(OWNED_LOCAL_CONSUMED_ATTR_NAME)) + { + return consumed ? varOp : mlir_ts::VariableOp(); + } + + auto retains = llvm::any_of(varOp.getResult().getUsers(), + [](mlir::Operation *user) { return mlir::isa(user); }); + return retains ? varOp : mlir_ts::VariableOp(); + } + + bool OwnershipInferencePass::borrowWith(mlir_ts::VariableOp borrower, mlir::Operation *acquisition, llvm::ArrayRef ends, + llvm::StringRef owner, llvm::SetVector &toErase) + { + if (!tryBorrow(borrower, ends, owner, toErase)) + { + return false; + } + + if (mlir::isa(acquisition)) + { + toErase.insert(acquisition); + } + + return true; + } + + mlir_ts::VariableOp OwnershipInferencePass::consumingLetOf(mlir::Value value) + { + for (auto *user : value.getUsers()) + { + auto varOp = mlir::dyn_cast(user); + if (varOp && varOp.getInitializer() == value && varOp->hasAttr(OWNED_LOCAL_CONSUMED_ATTR_NAME)) + { + return varOp; + } + } + + return {}; + } + + llvm::SmallVector OwnershipInferencePass::slotEnds(mlir::Value slot) + { + llvm::SmallVector ends; + for (auto *user : slot.getUsers()) + { + if (mlir::isa(user)) + { + ends.push_back(user); + } + } + + return ends; + } + + bool OwnershipInferencePass::tryBorrow(mlir_ts::VariableOp borrower, llvm::ArrayRef ends, llvm::StringRef owner, + llvm::SetVector &toErase) + { + auto name = varName(borrower); + llvm::SmallVector uses; + llvm::SmallVector bookkeeping; + mlir::Operation *declaration = borrower; + if (auto *kept = collectBorrowerUses(borrower, declaration, uses, bookkeeping)) + { + if (isAssignmentOf(kept, borrower)) + { + reportBorrowerAssigned(kept, name, owner); + } + else + { + reportBorrowEscapes(kept, name, owner); + } + + return false; + } + + for (auto *end : ends) + { + for (auto &use : uses) + { + if (reachableAfter(end, use.op, use.kills)) + { + reportBorrowOutlives(use.op, end, name, owner); + return false; + } + } + } + + toErase.insert(bookkeeping.begin(), bookkeeping.end()); + return true; + } + + mlir::Operation *OwnershipInferencePass::collectBorrowerUses(mlir_ts::VariableOp borrower, llvm::ArrayRef kills, + llvm::SmallVectorImpl &uses, + llvm::SmallVectorImpl &bookkeeping) + { + for (auto *user : borrower.getResult().getUsers()) + { + if (mlir::isa(user)) + { + bookkeeping.push_back(user); + continue; + } + + auto readOp = mlir::dyn_cast(user); + if (!readOp) + { + return user; + } + + uses.push_back({user, {kills.begin(), kills.end()}}); + if (auto *kept = walkBorrowed(readOp.getResult(), kills, uses)) + { + return kept; + } + } + + return nullptr; + } + + bool OwnershipInferencePass::isAssignmentOf(mlir::Operation *op, mlir_ts::VariableOp varOp) + { + auto storeOp = mlir::dyn_cast(op); + return storeOp && storeOp.getReference() == varOp.getResult(); + } + + bool OwnershipInferencePass::passesOn(mlir::OpOperand &use, llvm::SmallVectorImpl &values, + llvm::SmallVectorImpl *renewedAt) + { + auto *user = use.getOwner(); + if (mlir::isa(user) || isView(user)) + { + values.push_back(user->getResult(0)); + return true; + } + + if (auto branchOp = mlir::dyn_cast(user)) + { + if (auto argument = branchOp.getSuccessorBlockArgument(use.getOperandNumber())) + { + values.push_back(*argument); + if (renewedAt) + { + renewedAt->push_back(&argument->getOwner()->front()); + } + + return true; + } + + return false; + } + + if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getValue() == use.get()) + { + if (auto varOp = storeOp.getReference().getDefiningOp(); + varOp && !isOwningVariable(varOp) && !varOp.getCaptured().value_or(false)) + { + if (renewedAt && varOp.getInitializer()) + { + renewedAt->push_back(varOp); + } + + for (auto *varUser : varOp.getResult().getUsers()) + { + if (auto loadOp = mlir::dyn_cast(varUser)) + { + values.push_back(loadOp.getResult()); + } + else if (renewedAt && isAssignmentOf(varUser, varOp)) + { + renewedAt->push_back(varUser); + } + } + + return true; + } + } + + return false; + } + + mlir::Operation *OwnershipInferencePass::walkBorrowed( + mlir::Value start, llvm::ArrayRef kills, llvm::SmallVectorImpl &uses, + llvm::function_ref)> isBorrower, + Keeping keeping, bool countedHandle, bool startBorrowed) + { + struct Pending + { + mlir::Value value; + bool borrowed; + llvm::SmallVector kills; + }; + + llvm::SmallVector values; + // `startBorrowed` false: `start` is what a read produced (a field's reference), whose uses + // are uses of the borrow but not the borrowed value itself + values.push_back({start, startBorrowed, {kills.begin(), kills.end()}}); + llvm::SmallPtrSet seen{start}; + while (!values.empty()) + { + auto pending = values.pop_back_val(); + for (auto &use : pending.value.getUses()) + { + auto *user = use.getOwner(); + if (pending.borrowed && keeping.bookkeeping && + ((mlir::isa(user) && borrowingCall(rootOf(pending.value))) || + (keeping.returns && mlir::isa(user)))) + { + keeping.bookkeeping->push_back(user); + continue; + } + + // rc's reference for a borrowing box's copy goes with the closure (decideClosures), and + // one for a borrowing field's with the field (classifyBorrowingFields) + if (captureRetains.contains(user) || fieldBorrowRetains.contains(user)) + { + continue; + } + + uses.push_back({user, pending.kills, isCall(user) && isCalleeOperand(use)}); + // copied into a borrowing box: whatever runs the closure reads it + if (auto found = captureStores.find(user); found != captureStores.end()) + { + for (auto *closureUse : found->second->uses) + { + uses.push_back({closureUse, pending.kills}); + } + } + + // stored into a field a borrowing record borrows (spec 32): whatever uses the record + // reads it + if (auto found = recordStoreUses.find(user); found != recordStoreUses.end()) + { + for (auto &recordUse : found->second->walk.uses) + { + uses.push_back({recordUse.op, pending.kills, recordUse.callee}); + } + } + + if (pending.borrowed && keeping.returns && mlir::isa(user)) + { + continue; + } + + if (pending.borrowed) + { + // a handle read that rc counts (spec 23.3): a return or an `any` box rc + // retained for is a counted copy of its own; passesOn would hand them on + if (countedHandle && isCountedHandOff(user, pending.value)) + { + continue; + } + + llvm::SmallVector same; + llvm::SmallVector renewedAt; + if (passesOn(use, same, &renewedAt)) + { + for (auto value : same) + { + if (mayPointInto(value) && seen.insert(value).second) + { + values.push_back({value, true, renewedAt.empty() ? pending.kills : renewedAt}); + } + } + + continue; + } + + // a handle read that rc counts (spec 23.3): its retain, and what takes the value + // it retains for, are a counted copy of their own, as a string copy is; the + // read's other uses are the borrow's (Ruling 7) + if (countedHandle && isCountedHandleReceiver(user, pending.value)) + { + continue; + } + + if (isBorrower && isBorrower(user, pending.kills)) + { + continue; + } + + if (mlir::isa(user) || !isBorrow(user, pending.value)) + { + return user; + } + } + + // a copy (spec 22.2) reads the borrow into a block of its own, which points into + // nothing, and so does a concatenation: `s + t` is a new string whatever s and t are + // (a `+` of two strings lowers to ts.StringConcat, which always allocates) + if (mlir::isa(user) || + (mlir::isa(user) && mlir::isa(user->getResult(0).getType()))) + { + continue; + } + + // a call whose result is the caller's own (callResultIsOwn) ends the walk at the call + if (callResultIsOwn(user)) + { + continue; + } + + // A result that borrows what it was given borrows this too, and is walked as it. + for (auto result : user->getResults()) + { + if (mayPointInto(result) && seen.insert(result).second) + { + values.push_back({result, !!borrowingCall(result), pending.kills}); + } + } + } + } + + return nullptr; + } + + bool OwnershipInferencePass::callResultIsOwn(mlir::Operation *call) + { + if (!isCall(call) || call->hasAttr(OWN_RESULT_BOUNDED_ATTR_NAME)) + { + return false; + } + + return call->hasAttr(OWN_FRESH_RESULT_ATTR_NAME) || + (call->getNumResults() == 1 && !isRecordResult(call) && isFresh(call->getResult(0))); + } + + bool OwnershipInferencePass::isCalleeOperand(mlir::OpOperand &use) + { + auto *user = use.getOwner(); + auto callee = isCall(user) ? calleeValue(user) : mlir::Value(); + if (!callee) + { + return false; + } + + if (use.getOperandNumber() == 0) + { + return true; + } + + auto getThisOp = use.get().getDefiningOp(); + auto getMethodOp = callee.getDefiningOp(); + auto methodField = getThisOp ? getThisOp.getOperand().getDefiningOp() : mlir_ts::LoadOp(); + return use.getOperandNumber() == 1 && getMethodOp && methodField && + getThisOp.getOperand() == getMethodOp.getBoundFunc() && + mlir::isa(methodField.getReference().getType()); + } + +} // namespace own_inference diff --git a/tslang/lib/TypeScript/OwnershipInferenceClosures.cpp b/tslang/lib/TypeScript/OwnershipInferenceClosures.cpp new file mode 100644 index 000000000..36b48de93 --- /dev/null +++ b/tslang/lib/TypeScript/OwnershipInferenceClosures.cpp @@ -0,0 +1,1216 @@ +// OwnershipInferencePass (see OwnershipInferencePass.h): closures (spec 2.5, phase 5) - captures, +// cells, generator state boxes and the fields they borrow, and the slots a closure takes. + +#include "OwnershipInferencePass.h" + +#define DEBUG_TYPE "own" + +namespace own_inference +{ + + void OwnershipInferencePass::classifyClosures() + { + getFunction().walk([&](mlir_ts::CreateBoundFunctionOp boundOp) { + if (!boundOp->hasAttr(OWNS_CAPTURE_ATTR_NAME) || closureOfBox(boundOp.getThisVal()) != boundOp) + { + return; + } + + auto closure = std::make_shared(); + closure->op = boundOp; + closure->box = boundOp.getThisVal(); + for (auto *user : closure->box.getUsers()) + { + if (user == boundOp.getOperation()) + { + continue; + } + + if (auto propertyRefOp = mlir::dyn_cast(user)) + { + for (auto *fieldUser : propertyRefOp->getUsers()) + { + auto storeOp = mlir::dyn_cast(fieldUser); + if (!storeOp || storeOp.getReference() != propertyRefOp.getResult()) + { + closure->escape = closure->escape ? closure->escape : fieldUser; + continue; + } + + closure->fills.push_back({propertyRefOp.getPosition(), storeOp}); + closure->hasCells = closure->hasCells || mlir::isa(storeOp.getValue().getType()); + } + + continue; + } + + if (isCall(user)) + { + closure->uses.push_back(user); + closure->boxUses.push_back(user); + continue; + } + + closure->escape = closure->escape ? closure->escape : user; + } + + if (auto *escape = followClosure(boundOp.getResult(), closure->uses, closure->ends, closure->aliased); + escape && !closure->escape) + { + closure->escape = escape; + } + + // copies only: the box owns them (phase 4); only an alias needs deciding - unless the box + // is given to a generator's maker whose result borrows from it + if (!closure->hasCells) + { + // ...and only when every copy has an owner of its own: the box frees itself alone, so a + // value made for it (a callback that captures, `a.filter((x) => x > n)`) would be + // nobody's + closure->borrowsCopies = + !closure->escape && + llvm::any_of(closure->boxUses, + [](mlir::Operation *use) { return use->hasAttr(OWN_RESULT_BOUNDED_ATTR_NAME); }) && + llvm::none_of(closure->fills, [&](auto &fill) { + auto value = fill.second.getValue(); + return !holdsNoBlock(value) && isFresh(rootOf(value)); + }); + if (!closure->borrowsCopies) + { + if (!closure->escape && closure->aliased) + { + closures.push_back(std::move(closure)); + } + + return; + } + } + + for (auto &fill : closure->fills) + { + auto storeOp = fill.second; + auto isCell = mlir::isa(storeOp.getValue().getType()); + // an escaping closure's copies move into its box as in phase 4, rc's retain with them + if (closure->escape && !isCell) + { + continue; + } + + if (auto *retain = retainBefore(storeOp.getValue(), storeOp)) + { + closure->retains.push_back(retain); + captureRetains.insert(retain); + } + + if (!closure->escape && !isCell) + { + setAttrTracked(storeOp, OWN_CAPTURE_BORROW_ATTR_NAME); + captureStores[storeOp] = closure.get(); + } + } + + if (!closure->escape) + { + setAttrTracked(boundOp, OWN_BORROWS_CAPTURES_ATTR_NAME); + } + + closureOf[boundOp.getOperation()] = closure.get(); + closures.push_back(std::move(closure)); + }); + } + + llvm::SmallVector OwnershipInferencePass::borrowingFieldsOf(mlir::Type recordType) + { + llvm::SmallVector fields; + if (auto objectType = mlir::dyn_cast(recordType)) + { + recordType = objectType.getStorageType(); + } + + if (auto storageType = mlir::dyn_cast(recordType)) + { + recordType = mlir_ts::TupleType::get(&getContext(), storageType.getFields()); + } + + auto module = getFunction()->getParentOfType(); + auto entries = module ? module->getAttrOfType(OWN_BORROWING_FIELDS_ATTR_NAME) : mlir::ArrayAttr(); + if (!entries) + { + return fields; + } + + for (auto entry : entries.getAsRange()) + { + if (mlir::cast(entry[0]).getValue() == recordType) + { + auto indices = mlir::cast(entry[1]).asArrayRef(); + fields.append(indices.begin(), indices.end()); + } + } + + return fields; + } + + bool OwnershipInferencePass::isYieldedField(mlir::Value ref) + { + auto propertyRefOp = ref.getDefiningOp(); + if (!propertyRefOp) + { + return false; + } + + auto recordType = propertyRefOp->getOperand(0).getType(); + if (auto objectType = mlir::dyn_cast(recordType)) + { + recordType = objectType.getStorageType(); + } + + if (auto storageType = mlir::dyn_cast(recordType)) + { + recordType = mlir_ts::TupleType::get(&getContext(), storageType.getFields()); + } + + auto module = getFunction()->getParentOfType(); + auto entries = module ? module->getAttrOfType(OWN_YIELDED_FIELDS_ATTR_NAME) : mlir::ArrayAttr(); + if (!entries) + { + return false; + } + + for (auto entry : entries.getAsRange()) + { + if (mlir::cast(entry[0]).getValue() == recordType && + llvm::is_contained(mlir::cast(entry[1]).asArrayRef(), + static_cast(propertyRefOp.getPosition()))) + { + return true; + } + } + + return false; + } + + bool OwnershipInferencePass::isBorrowingField(mlir::Value ref) + { + auto propertyRefOp = ref.getDefiningOp(); + if (!propertyRefOp) + { + return false; + } + + auto fields = borrowingFieldsOf(propertyRefOp->getOperand(0).getType()); + return fields.size() > 1 && llvm::is_contained(llvm::ArrayRef(fields).drop_front(), + static_cast(propertyRefOp.getPosition())); + } + + void OwnershipInferencePass::classifyBorrowingFields() + { + getFunction().walk([&](mlir::Operation *op) { + if (auto storeOp = mlir::dyn_cast(op); storeOp && isBorrowingField(storeOp.getReference())) + { + setAttrTracked(storeOp, OWN_CAPTURE_BORROW_ATTR_NAME); + if (auto *retain = retainBefore(storeOp.getValue(), storeOp)) + { + fieldBorrowRetains.insert(retain); + } + } + else if (auto releaseSlotOp = mlir::dyn_cast(op); + releaseSlotOp && isBorrowingField(releaseSlotOp.getSlot())) + { + fieldBorrowReleases.push_back(op); + } + }); + } + + bool OwnershipInferencePass::isBorrowingCell(mlir::Value ref) + { + auto cellLoad = ref.getDefiningOp(); + return cellLoad && isBorrowingBoxField(cellLoad.getReference()); + } + + bool OwnershipInferencePass::isBorrowingBoxField(mlir::Value ref) + { + auto cellRef = ref.getDefiningOp(); + auto boxLoad = cellRef ? cellRef->getOperand(0).getDefiningOp() : mlir_ts::LoadOp(); + auto boxRef = boxLoad ? boxLoad.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); + if (!boxRef) + { + return false; + } + + auto fields = borrowingFieldsOf(boxRef->getOperand(0).getType()); + return !fields.empty() && fields.front() == static_cast(boxRef.getPosition()); + } + + void OwnershipInferencePass::checkBorrowedReads() + { + getFunction().walk([&](mlir_ts::LoadOp loadOp) { + auto ref = loadOp.getReference(); + if (!ownsHeap(loadOp.getResult()) || !(isBorrowingCell(ref) || isBorrowingBoxField(ref) || isBorrowingField(ref))) + { + return; + } + + auto reported = false; + forEachUse(loadOp.getResult(), [&](mlir::Operation *user, mlir::Value used) { + if (reported || isBorrow(user, used) || fieldBorrowRetains.contains(user)) + { + return; + } + + reportPlaceBorrowEscapes(user, ownerName(loadOp.getResult()), "the generator's argument"); + reported = true; + }); + }); + } + + void OwnershipInferencePass::checkBoundedResults() + { + getFunction().walk([&](mlir::Operation *call) { + auto bounded = call->getAttrOfType(OWN_RESULT_BOUNDED_ATTR_NAME); + if (!bounded || !isCall(call) || call->getNumResults() != 1) + { + return; + } + + auto name = ownerName(call->getResult(0)); + llvm::SmallVector uses; + if (auto *kept = boundedUses(call->getResult(0), uses)) + { + reportPlaceBorrowEscapes(kept, name, "its generator's arguments"); + return; + } + + auto args = callArgs(call); + for (auto index : bounded.asArrayRef()) + { + if (static_cast(index) >= args.size()) + { + continue; + } + + // a closure's box (`.map(f)` calls its maker with it): it lives while the closures on + // it do, and what it holds while their owners do. A reference to storage, so first. + auto root = rootOf(args[index]); + if (auto boxOp = root.getDefiningOp(); boxOp && boxOp->hasAttr(CAPTURE_BOX_ATTR_NAME)) + { + if (!checkBoundedByBox(call, root, uses, name)) + { + return; + } + + continue; + } + + if (holdsNoBlock(args[index]) || isImmortalLiteral(args[index])) + { + continue; + } + + if (borrowedParam(root) >= 0) + { + continue; // the caller's caller's: alive for all of this function + } + + Chain chain; + auto *read = placeReadOf(root); + if (read) + { + chain = chainOf(read); + } + else if (auto slotLoad = slotLoadOf(root)) + { + chain.roots.push_back({slotLoad.getReference(), Chain::Slot}); + } + else if (isFresh(root)) + { + chain.roots.push_back({root, Chain::Owned}); + } + else + { + chain.roots.push_back({root, Chain::NotOwned}); + } + + // the root's own ends; and, for a read out of a place, whatever may destroy what the + // place holds (a local or a made value is not destroyed by a call it is given) + auto ends = rootEnds(chain); + for (auto *drop : read ? llvm::ArrayRef(drops) : llvm::ArrayRef()) + { + if (drop != call && dropsChain(drop, chain, read)) + { + ends.push_back(drop); + } + } + + auto owner = ownerName(root); + for (auto *end : ends) + { + if (end == call) + { + continue; + } + + for (auto *use : uses) + { + if (use != end && reachableAfter(end, use, call)) + { + reportBorrowOutlives(use, end, name, owner); + return; + } + } + } + } + }); + } + + bool OwnershipInferencePass::checkBoundedByBox(mlir::Operation *call, mlir::Value box, llvm::ArrayRef uses, + llvm::StringRef name) + { + llvm::SmallVector ends; + for (auto *user : box.getUsers()) + { + if (auto boundOp = mlir::dyn_cast(user); boundOp && boundOp.getThisVal() == box) + { + llvm::SmallVector closureUses; + auto aliased = false; + if (followClosure(boundOp.getResult(), closureUses, ends, aliased)) + { + reportPlaceBorrowEscapes(call, name, "a closure that escapes"); // the box may go anywhere + return false; + } + + continue; + } + + auto propertyRefOp = mlir::dyn_cast(user); + if (!propertyRefOp) + { + continue; + } + + for (auto *fieldUser : propertyRefOp->getUsers()) + { + auto storeOp = mlir::dyn_cast(fieldUser); + if (!storeOp || storeOp.getReference() != propertyRefOp.getResult() || holdsNoBlock(storeOp.getValue())) + { + continue; + } + + auto root = rootOf(storeOp.getValue()); + Chain chain; + if (auto slotLoad = slotLoadOf(root)) + { + chain.roots.push_back({slotLoad.getReference(), Chain::Slot}); + } + else if (borrowedParam(root) >= 0 || isImmortalLiteral(root)) + { + continue; + } + else if (isFresh(root)) + { + chain.roots.push_back({root, Chain::Owned}); + } + else + { + reportPlaceBorrowEscapes(call, name, "a value this function cannot bound"); + return false; + } + + auto rootEndsOf = rootEnds(chain); + ends.append(rootEndsOf.begin(), rootEndsOf.end()); + } + } + + for (auto *end : ends) + { + for (auto *use : uses) + { + if (end != call && use != end && reachableAfter(end, use, call)) + { + reportBorrowOutlives(use, end, name, "the generator's source"); + return false; + } + } + } + + return true; + } + + mlir::Operation *OwnershipInferencePass::boundedUses(mlir::Value result, llvm::SmallVectorImpl &uses) + { + llvm::SmallVector values{result}; + llvm::SmallPtrSet seen; + mlir::Operation *kept = nullptr; + while (!values.empty() && !kept) + { + auto value = values.pop_back_val(); + if (!seen.insert(value).second) + { + continue; + } + + forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { + if (kept || mlir::isa(user)) + { + return; + } + + mlir::Value slot; + if (auto varOp = mlir::dyn_cast(user)) + { + slot = varOp.getResult(); + } + else if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getValue() == used) + { + slot = storeOp.getReference(); + } + + if (slot) + { + auto slotVar = slot.getDefiningOp(); + if (!slotVar || slotVar.getCaptured().value_or(false)) + { + kept = user; + return; + } + + for (auto *slotUser : slot.getUsers()) + { + if (auto loadOp = mlir::dyn_cast(slotUser)) + { + values.push_back(loadOp.getResult()); + } + } + + return; + } + + // returned by a function that says so (`@borrows`): its callers hold the result to + // the same bound, and the signature pass checked the arguments are its own + if (mlir::isa(user) && getFunction()->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME)) + { + return; + } + + uses.push_back(user); + if (isCall(user)) + { + if (isKeptArgument(user, used)) + { + kept = user; + } + + return; + } + + // `it.next()`: the field read, the bound function, its `this` + if (mlir::isa(user)) + { + for (auto partResult : user->getResults()) + { + values.push_back(partResult); + } + + return; + } + + if (!isBorrow(user, used)) + { + kept = user; + } + }); + } + + return kept; + } + + void OwnershipInferencePass::classifyStateBoxes() + { + getFunction().walk([&](mlir_ts::VariableOp boxOp) { + auto box = boxOp.getResult(); + if (!boxOp->hasAttr(CAPTURE_BOX_ATTR_NAME) || closureOfBox(box)) + { + return; + } + + auto closure = std::make_shared(); + closure->box = box; + llvm::SmallVector boxRetains; + for (auto *user : box.getUsers()) + { + if (auto propertyRefOp = mlir::dyn_cast(user)) + { + for (auto *fieldUser : propertyRefOp->getUsers()) + { + auto storeOp = mlir::dyn_cast(fieldUser); + if (!storeOp || storeOp.getReference() != propertyRefOp.getResult()) + { + return; + } + + closure->fills.push_back({propertyRefOp.getPosition(), storeOp}); + closure->hasCells = closure->hasCells || mlir::isa(storeOp.getValue().getType()); + } + + continue; + } + + if (mlir::isa(user)) + { + boxRetains.push_back(user); + continue; + } + + if (!closure->escape && isStoreIntoState(user, box)) + { + closure->escape = user; + continue; + } + + return; + } + + if (!closure->escape) + { + return; + } + + auto stateRef = mlir::cast(closure->escape).getReference().getDefiningOp(); + auto stateType = mlir::cast(stateRef->getOperand(0).getType()).getElementType(); + auto borrowing = borrowingFieldsOf(stateType); + closure->borrowsValues = !borrowing.empty() && borrowing.front() == stateRef.getPosition(); + // `.map`'s maker copies what its parameter box holds into the generator's box: a borrow + if (closure->borrowsValues) + { + for (auto &fill : closure->fills) + { + if (!mlir::isa(fill.second.getValue().getType())) + { + setAttrTracked(fill.second, OWN_CAPTURE_BORROW_ATTR_NAME); + } + } + } + + for (auto &fill : closure->fills) + { + // a parameter this function does not own, in a box that does not borrow: the box + // cannot take rc's reference for it, the caller's still holds it; left to the + // second-reference error, which says the callers would have to give it up + auto param = borrowedParam(fill.second.getValue()); + if (!closure->borrowsValues && param >= 0 && !llvm::is_contained(ownedParams(getFunction()), param)) + { + continue; + } + + if (auto *retain = retainBefore(fill.second.getValue(), fill.second)) + { + closure->retains.push_back(retain); + captureRetains.insert(retain); + } + } + + for (auto *retain : boxRetains) + { + closure->retains.push_back(retain); + captureRetains.insert(retain); + } + + stateBoxes[box] = closure.get(); + closures.push_back(std::move(closure)); + }); + } + + bool OwnershipInferencePass::isStoreIntoState(mlir::Operation *user, mlir::Value box) + { + auto storeOp = mlir::dyn_cast(user); + auto propertyRefOp = storeOp && storeOp.getValue() == box + ? storeOp.getReference().getDefiningOp() + : mlir_ts::PropertyRefOp(); + auto stateOp = propertyRefOp ? propertyRefOp->getOperand(0).getDefiningOp() : mlir_ts::VariableOp(); + if (!stateOp || !isConstantRecordLocal(stateOp)) + { + return false; + } + + auto reads = 0; + for (auto *stateUser : stateOp->getUsers()) + { + auto loadOp = mlir::dyn_cast(stateUser); + if (!loadOp) + { + continue; + } + + ++reads; + for (auto *readUser : loadOp->getUsers()) + { + // rc's retain of the initial state, for fields that own (holdsConstantsOrMoves) + if (mlir::isa(readUser)) + { + continue; + } + + auto fillOp = mlir::dyn_cast(readUser); + if (!fillOp || fillOp.getValue() != loadOp.getResult() || !fillOp.getReference().getDefiningOp()) + { + return false; + } + } + } + + return reads > 0; + } + + mlir::Operation *OwnershipInferencePass::retainBefore(mlir::Value value, mlir::Operation *store) + { + // rc's retain of an earlier read of the same place (twinReadAfter) + if (auto loadOp = value.getDefiningOp()) + { + for (auto *user : loadOp.getReference().getUsers()) + { + auto earlier = mlir::dyn_cast(user); + auto *retain = earlier ? soleUser(earlier.getResult()) : nullptr; + if (retain && mlir::isa(retain) && !captureRetains.contains(retain) && + twinReadAfter(retain, earlier.getResult()) == value && retain->getBlock() == store->getBlock() && + retain->isBeforeInBlock(store)) + { + return retain; + } + } + } + + mlir::Operation *found = nullptr; + for (auto *user : value.getUsers()) + { + if (!mlir::isa(user) || user->getBlock() != store->getBlock() || + !user->isBeforeInBlock(store) || captureRetains.contains(user)) + { + continue; + } + + if (!found || found->isBeforeInBlock(user)) + { + found = user; + } + } + + return found; + } + + mlir::Operation *OwnershipInferencePass::followClosure(mlir::Value closure, llvm::SmallVectorImpl &uses, + llvm::SmallVectorImpl &ends, bool &aliased) + { + llvm::SmallVector values{closure}; + llvm::SmallPtrSet seen; + mlir::Operation *escape = nullptr; + auto followSlot = [&](mlir::Value slot) { + for (auto *slotUser : slot.getUsers()) + { + if (mlir::isa(slotUser)) + { + continue; + } + + if (mlir::isa(slotUser)) + { + ends.push_back(slotUser); + } + else if (auto loadOp = mlir::dyn_cast(slotUser)) + { + values.push_back(loadOp.getResult()); + } + else if (auto storeOp = mlir::dyn_cast(slotUser); !storeOp || storeOp.getReference() != slot) + { + escape = escape ? escape : slotUser; + } + } + }; + + while (!values.empty() && !escape) + { + auto value = values.pop_back_val(); + if (!seen.insert(value).second) + { + continue; + } + + forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { + if (escape || mlir::isa(user)) + { + return; + } + + if (mlir::isa(user)) + { + ends.push_back(user); + return; + } + + if (isCall(user)) + { + if (isKeptArgument(user, used)) + { + escape = user; + } + else + { + uses.push_back(user); + } + + return; + } + + // the function and the box a call through the closure is made with + if (mlir::isa(user)) + { + for (auto *partUser : user->getResult(0).getUsers()) + { + if (isCall(partUser) && !isKeptArgument(partUser, user->getResult(0))) + { + uses.push_back(partUser); + } + else + { + escape = escape ? escape : partUser; + } + } + + return; + } + + mlir::Value slot; + if (auto varOp = mlir::dyn_cast(user)) + { + slot = varOp.getResult(); + } + else if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getValue() == used) + { + slot = storeOp.getReference(); + } + + // a local, owning or not (`let f: () => number;` owns nothing): its reads are the + // closure; a function's result slot is a local too, and its read is returned + auto slotVar = slot ? slot.getDefiningOp() : mlir_ts::VariableOp(); + if (slotVar && !slotVar.getCaptured().value_or(false)) + { + aliased = aliased || !isOwningVariable(slotVar); + followSlot(slot); + return; + } + + escape = user; + }); + } + + return escape; + } + + bool OwnershipInferencePass::holdsCaptures(mlir::Value value) + { + if (mlir::isa(value.getType())) + { + return true; + } + + auto root = rootOf(value); + if (auto varOp = root.getDefiningOp()) + { + return varOp->hasAttr(CAPTURE_BOX_ATTR_NAME); + } + + // a closure's `this` is its box, a reference; a bound method's is the object + auto getThisOp = root.getDefiningOp(); + return getThisOp && mlir::isa(getThisOp.getType()); + } + + void OwnershipInferencePass::decideClosures(llvm::SetVector &toErase) + { + for (auto &closure : closures) + { + if (!closure->op) + { + decideEscaping(*closure, toErase); // a generator's box + continue; + } + + auto *boundOp = closure->op.getOperation(); + auto name = ownerName(closure->op.getResult()); + auto ok = true; + // Held by a local that owns nothing: rc's birth reference reads to the move logic as the + // store's, which would leave the box with no owner. The closure keeps its releases, and + // nothing may run it after them. + auto keepAliasedReleases = [&]() { + for (auto *end : closure->ends) + { + if (!mlir::isa(end)) + { + continue; + } + + toErase.remove(end); + for (auto *use : closure->uses) + { + if (ok && reachableAfter(end, use, boundOp)) + { + reportClosureOutlives(use, end, name, name); + ok = false; + } + } + } + }; + + if (!closure->hasCells && !closure->borrowsCopies) + { + keepAliasedReleases(); + continue; + } + + if (closure->escape) + { + decideEscaping(*closure, toErase); + continue; + } + + for (auto &fill : closure->fills) + { + auto value = fill.second.getValue(); + llvm::SmallVector ends; + llvm::StringRef captured = "this value"; + if (mlir::isa(value.getType())) + { + if (isCellVariable(value)) + { + captured = varName(value.getDefiningOp()); + for (auto *user : value.getUsers()) + { + if (mlir::isa(user) || movesCellAway(user)) + { + ends.push_back(user); + } + } + } + else if (!isLoadedCell(value)) + { + reportCaptureNotOwned(fill.second, captured); + ok = false; + continue; + } + } + else + { + auto root = rootOf(value); + captured = ownerName(root); + Chain chain; + if (holdsNoBlock(value) || placeReadOf(root) || borrowedParam(root) >= 0 || isHandle(root)) + { + // nothing to own, a read the read's check bounds, a parameter, or a handle, + // whose copy in the box is counted (spec 23.2) + } + else if (auto slotLoad = slotLoadOf(root)) + { + chain.roots.push_back({slotLoad.getReference(), Chain::Slot}); + } + else if (isFresh(root)) + { + chain.roots.push_back({root, Chain::Owned}); + } + else + { + reportCaptureNotOwned(fill.second, captured); + ok = false; + continue; + } + + ends = rootEnds(chain); + } + + for (auto *end : ends) + { + if (captureRetains.contains(end) || captureStores.count(end)) + { + continue; // a box's own copy, this one's or another borrowing closure's + } + + for (auto *use : closure->uses) + { + if (ok && reachableAfter(end, use, boundOp)) + { + reportClosureOutlives(use, end, name, captured); + ok = false; + } + } + } + } + + if (closure->aliased) + { + keepAliasedReleases(); + } + + // a call through the box, after the closure value that owns the box is given back + for (auto *end : closure->ends) + { + for (auto *use : closure->boxUses) + { + if (ok && reachableAfter(end, use, boundOp)) + { + reportClosureOutlives(use, end, name, name); + ok = false; + } + } + } + + toErase.insert(closure->retains.begin(), closure->retains.end()); + } + } + + bool OwnershipInferencePass::movesCellAway(mlir::Operation *op) + { + auto storeOp = mlir::dyn_cast(op); + auto propertyRefOp = storeOp ? storeOp.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); + if (!propertyRefOp || !mlir::isa(storeOp.getValue().getType())) + { + return false; + } + + if (stateBoxes.count(propertyRefOp.getObjectRef())) + { + return true; + } + + auto boundOp = closureOfBox(propertyRefOp.getObjectRef()); + auto found = boundOp ? closureOf.find(boundOp.getOperation()) : closureOf.end(); + return found != closureOf.end() && found->second->escape; + } + + void OwnershipInferencePass::decideEscaping(Closure &closure, llvm::SetVector &toErase) + { + auto ok = true; + for (auto &fill : closure.fills) + { + auto value = fill.second.getValue(); + mlir::Operation *move = fill.second; + if (!mlir::isa(value.getType())) + { + continue; + } + + if (isLoadedCell(value)) + { + reportEscapingCellNotOwned(move, closure.escape, "it is captured from an enclosing function"); + ok = false; + continue; + } + + auto varOp = isCellVariable(value) ? value.getDefiningOp() : mlir_ts::VariableOp(); + if (!varOp) + { + reportCaptureNotOwned(move, "this value"); + ok = false; + continue; + } + + auto name = varName(varOp); + auto elementType = mlir::cast(value.getType()).getElementType(); + // a handle's cell holds a count of its own, whoever's the value was (spec 23.2) + if (!isOwningVariable(varOp) && !closure.borrowsValues && ownsHeap(varOp.getLoc(), elementType) && + !MLIRTypeHelper::isSharedHandleType(elementType)) + { + reportEscapingCellNotOwned(move, closure.escape, "its value is the caller's"); + ok = false; + continue; + } + + if (takerLoops(move, varOp)) + { + reportMovedInLoop(move, name); + ok = false; + continue; + } + + llvm::SmallVector moved; + for (auto *user : value.getUsers()) + { + if (user == move || llvm::is_contained(closure.retains, user) || !reachableAfter(move, user, varOp)) + { + continue; + } + + if (!mlir::isa(user)) + { + reportUsedAfterEscapingCapture(user, move, name); + ok = false; + break; + } + + if (!dominance->properlyDominates(move, user)) + { + reportMovedOnSomePaths(move, user, name); + ok = false; + break; + } + + moved.push_back(user); + } + + toErase.insert(moved.begin(), moved.end()); + } + + // A call through the box itself after the closure was given away or given back. followClosure + // stops at the escape, so `ends` may miss releases past it; that is enough while the only + // calls through a box are those of a folded `const f`, whose release ends its block. A + // generator's box has no closure value and no such call. + if (!closure.op) + { + toErase.insert(closure.retains.begin(), closure.retains.end()); + return; + } + + auto name = ownerName(closure.op.getResult()); + llvm::SmallVector gone(closure.ends.begin(), closure.ends.end()); + gone.push_back(closure.escape); + for (auto *end : gone) + { + for (auto *use : closure.boxUses) + { + if (ok && use != end && reachableAfter(end, use, closure.op.getOperation())) + { + reportUseAfterMove(use, end, name); + ok = false; + } + } + } + + toErase.insert(closure.retains.begin(), closure.retains.end()); + } + + void OwnershipInferencePass::decideCells() + { + getFunction().walk([&](mlir_ts::VariableOp varOp) { + auto cell = varOp.getResult(); + if (!isCellVariable(cell) || isOwningVariable(varOp)) + { + return; + } + + auto elementType = mlir::cast(cell.getType()).getElementType(); + if (!ownsHeap(varOp.getLoc(), elementType)) + { + return; + } + + // A handle's cell holds a count of its own, taken where it is made (spec 23.2): its + // release gives that back, as under rc, and an assignment is a counted copy. + if (MLIRTypeHelper::isSharedHandleType(elementType)) + { + return; + } + + auto name = varName(varOp); + auto init = varOp.getInitializer(); + auto argument = init ? mlir::dyn_cast(init) : mlir::BlockArgument(); + if (!argument || !argument.getOwner()->isEntryBlock() || + !mlir::isa(argument.getOwner()->getParentOp())) + { + reportCaptureNotOwned(varOp, name); + return; + } + + for (auto *user : cell.getUsers()) + { + if (mlir::isa(user)) + { + setAttrTracked(user, OWN_CELL_BORROWS_ATTR_NAME); + continue; + } + + auto storeOp = mlir::dyn_cast(user); + if (mlir::isa(user) || (storeOp && storeOp.getReference() == cell)) + { + reportCapturedParamAssigned(user, name); + return; + } + + // captured: the closure's body must not assign it either + auto propertyRefOp = storeOp ? storeOp.getReference().getDefiningOp() + : mlir_ts::PropertyRefOp(); + auto boundOp = propertyRefOp ? closureOfBox(propertyRefOp.getObjectRef()) : mlir_ts::CreateBoundFunctionOp(); + if (!boundOp) + { + continue; + } + + auto assigned = boundOp->getAttrOfType(OWN_ASSIGNS_CAPTURES_ATTR_NAME); + if (assigned && llvm::is_contained(assigned.asArrayRef(), static_cast(propertyRefOp.getPosition()))) + { + reportCapturedParamAssigned(boundOp, name); + return; + } + } + }); + } + + void OwnershipInferencePass::decideSlot(mlir::Value slot, llvm::ArrayRef receivers, llvm::SetVector &toErase) + { + auto owner = varName(slot.getDefiningOp()); + if (receivers.size() == 1 || takeValuesOfTheirOwn(slot, receivers)) + { + // a move on some paths only is placed when it is the local's one receiver (spec 30) + for (auto &receiver : receivers) + { + decideSlotReceiver(slot, receiver, owner, toErase, receivers.size() == 1); + } + + return; + } + + decideSlotReceivers(slot, receivers, owner, toErase); + } + + void OwnershipInferencePass::decideSlotReceiver(mlir::Value slot, const SlotReceiver &receiver, llvm::StringRef owner, + llvm::SetVector &toErase, bool sole) + { + if (quietly([&] { return checkSlotMove(receiver.retain, receiver.value, receiver.load, toErase, sole); })) + { + // rc's retain goes with the move; a call that keeps the argument is the move itself + if (!isCall(receiver.retain)) + { + toErase.insert(receiver.retain); + } + + return; + } + + if (auto borrower = borrowerOf(receiver.retain)) + { + borrowWith(borrower, receiver.retain, slotEnds(slot), owner, toErase); + return; + } + + checkSlotMove(receiver.retain, receiver.value, receiver.load, toErase, sole); // reports + } + + bool OwnershipInferencePass::takeValuesOfTheirOwn(mlir::Value slot, llvm::ArrayRef receivers) + { + auto assignments = slotAssignments(slot); + llvm::SmallVector kills(assignments.begin(), assignments.end()); + if (auto varOp = slot.getDefiningOp()) + { + kills.push_back(varOp); + } + + for (auto &from : receivers) + { + for (auto &to : receivers) + { + if (&from != &to && reachableAfter(from.load, to.load, kills)) + { + return false; + } + } + } + + return true; + } + +} // namespace own_inference diff --git a/tslang/lib/TypeScript/OwnershipInferenceDiagnostics.cpp b/tslang/lib/TypeScript/OwnershipInferenceDiagnostics.cpp new file mode 100644 index 000000000..f913bbf25 --- /dev/null +++ b/tslang/lib/TypeScript/OwnershipInferenceDiagnostics.cpp @@ -0,0 +1,748 @@ +// OwnershipInferencePass (see OwnershipInferencePass.h): names and diagnostics - what a value, +// a place or a call is called in an error, and the errors themselves. + +#include "OwnershipInferencePass.h" + +#define DEBUG_TYPE "own" + +namespace own_inference +{ + + std::string OwnershipInferencePass::describeCall(mlir::Operation *call) + { + auto callee = calleeName(call); + return callee.empty() ? "a call's result" : "the result of '" + callee + "'"; + } + + std::string OwnershipInferencePass::calleeName(mlir::Operation *call) + { + mlir::StringAttr callee; + auto value = calleeValue(call); + if (auto symbol = directCallee(call)) + { + callee = symbol.getAttr(); + } + else if (auto calleeOp = value ? value.getDefiningOp() : nullptr) + { + if (auto getMethodOp = mlir::dyn_cast(calleeOp)) + { + calleeOp = getMethodOp.getBoundFunc().getDefiningOp(); + } + + if (auto refOp = mlir::dyn_cast_or_null(calleeOp)) + { + callee = refOp.getIdentifierAttr().getAttr(); + } + else if (auto refOp = mlir::dyn_cast_or_null(calleeOp)) + { + callee = refOp.getIdentifierAttr().getAttr(); + } + else if (auto refOp = mlir::dyn_cast_or_null(calleeOp)) + { + callee = refOp.getIdentifierAttr().getAttr(); + } + else if (auto refOp = mlir::dyn_cast_or_null(calleeOp)) + { + // a method reached through an interface (spec 31): `.` + auto method = mlir::dyn_cast(refOp.getIdentifier()); + auto interfaceType = mlir::dyn_cast(refOp.getInterfaceVal().getType()); + if (method && interfaceType) + { + return (interfaceType.getName().getValue() + "." + method.getValue()).str(); + } + } + } + + return callee ? callee.getValue().str() : std::string(); + } + + std::string OwnershipInferencePass::describePlace(mlir::Value ref) + { + auto path = pathOf(ref); + if (path.empty()) + { + return ref.getDefiningOp() ? "an element" : "a field"; + } + + return "'" + path + "'"; + } + + std::string OwnershipInferencePass::pathOf(mlir::Value ref) + { + auto place = placeBase(ref); + std::string step; + switch (place.kind) + { + case PlaceKind::Field: { + auto field = fieldName(ref.getDefiningOp()); + if (field.empty()) + { + return {}; + } + + step = "." + field; + break; + } + case PlaceKind::Element: + step = "[]"; + break; + case PlaceKind::SharedValue: + step = ".value"; + break; + case PlaceKind::InterfaceField: { + auto field = mlir::dyn_cast(ref.getDefiningOp().getIdentifier()); + if (!field) + { + return {}; + } + + step = "." + field.getValue().str(); + break; + } + case PlaceKind::None: + return {}; + } + + auto base = rootOf(place.base); + std::string prefix; + if (isPlace(base)) + { + prefix = pathOf(base); + } + else if (auto loadOp = base.getDefiningOp()) + { + auto from = loadOp.getReference(); + if (isPlace(from)) + { + prefix = pathOf(from); + } + else if (auto varOp = from.getDefiningOp()) + { + prefix = varName(varOp).str(); + } + else if (auto global = from.getDefiningOp()) + { + prefix = global.getGlobalName().str(); + } + } + else if (auto varOp = base.getDefiningOp()) + { + prefix = varName(varOp).str(); + } + else + { + prefix = ownerName(base).str(); + } + + // a name MLIRGen made up (`.a`, the array a `for...of` walks) means nothing to the reader + if (prefix.empty() || prefix == "this value" || prefix.front() == '.') + { + return {}; + } + + return prefix + step; + } + + std::string OwnershipInferencePass::fieldName(mlir_ts::PropertyRefOp propertyRefOp) + { + auto type = propertyRefOp.getObjectRef().getType(); + if (auto refType = mlir::dyn_cast(type)) + { + type = refType.getElementType(); + } + + if (auto classType = mlir::dyn_cast(type)) + { + type = classType.getStorageType(); + } + + auto position = propertyRefOp.getPosition(); + mlir::Attribute id; + if (auto storageType = mlir::dyn_cast(type); storageType && position < storageType.size()) + { + id = storageType.getId(position); + } + else if (auto tupleType = mlir::dyn_cast(type); tupleType && position < tupleType.size()) + { + id = tupleType.getId(position); + } + + if (auto name = mlir::dyn_cast_or_null(id)) + { + return name.str(); + } + + if (auto name = mlir::dyn_cast_or_null(id)) + { + return name.getValue().str(); + } + + return {}; + } + + llvm::StringRef OwnershipInferencePass::ownerName(mlir::Value value) + { + llvm::StringRef name = "this value"; + forEachUse(value, [&](mlir::Operation *user, mlir::Value) { + if (mlir::isa(user)) + { + name = nameAt(user->getLoc()); + } + }); + + return name; + } + + llvm::StringRef OwnershipInferencePass::varName(mlir_ts::VariableOp varOp) + { + return nameAt(varOp.getLoc()); + } + + llvm::StringRef OwnershipInferencePass::nameAt(mlir::Location location) + { + if (auto fused = mlir::dyn_cast>(location)) + { + if (auto name = fused.getMetadata().getName()) + { + return name.getValue(); + } + } + + if (auto scoped = mlir::dyn_cast>(location)) + { + if (auto named = mlir::dyn_cast_or_null(scoped.getLocations().front())) + { + return named.getName().getValue(); + } + } + + return "this value"; + } + + llvm::StringRef OwnershipInferencePass::describe(mlir::Value value) + { + llvm::StringRef name = "this value"; + forEachUse(value, [&](mlir::Operation *user, mlir::Value) { + if (auto varOp = mlir::dyn_cast(user); varOp && name == "this value") + { + name = varName(varOp); + } + }); + + return name; + } + + void OwnershipInferencePass::reportSecondReference(mlir::Operation *op, mlir::Value value) + { + if (!reporting()) + { + return; + } + + llvm::StringRef name = "this value"; + if (auto retainSlotOp = mlir::dyn_cast(op)) + { + if (auto varOp = retainSlotOp.getSlot().getDefiningOp()) + { + name = varName(varOp); + } + } + else if (auto varOp = mlir::dyn_cast(op)) + { + name = varName(varOp); + } + + if (!value && mlir::isa(op)) + { + value = retainedValue(op); + } + + if (!value || !reportShapeOfSecondReference(op, rootOf(value), name)) + { + auto diag = op->emitError("'") << name << "' takes a second reference; -mm=own cannot prove a move or a borrow here yet"; + noteLostFacts(diag); + } + + signalPassFailure(); + } + + bool OwnershipInferencePass::reportShapeOfSecondReference(mlir::Operation *op, mlir::Value root, llvm::StringRef name) + { + auto f = getFunction(); + + // returned, where another return gives a borrow of an argument and this one does not, or + // the other way round: the callers cannot be told which they get + if (isReturned(root)) + { + auto param = borrowedParam(root); + if (auto *other = otherReturn(param)) + { + auto diag = op->emitError("'") << f.getName() + << "' returns a borrow of its argument on some paths and another value on " + "others; -mm=own cannot tell its callers whether they own the result"; + diag.attachNote(other->getLoc()) + << (param >= 0 ? "returns another value here" : "returns a borrow of its argument here"); + return true; + } + } + + // a parameter's value, or a part of it, returned or kept: a fact its callers must know + // (spec 15), and noteLostFacts says why they cannot + if (auto param = borrowedParam(root); param >= 0) + { + auto diag = op->emitError("'") << (name == "this value" ? paramName(param) : name); + if (isReturned(root)) + { + diag << "' is a parameter, returned here, but -mm=own cannot tell the callers of '" << f.getName() + << "' that the result borrows it"; + } + else + { + diag << "' is a parameter, kept here, but -mm=own cannot make the callers of '" << f.getName() + << "' give it up"; + } + + noteLostFacts(diag); + return true; + } + + // a store into a local whose value is not this function's to replace: a parameter's slot, or + // a local that owns nothing (MLIRGen's takeOwnershipOfLocal) + auto storeOp = mlir::dyn_cast(op); + if (auto slot = storeOp ? storeOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + slot && !isOwningVariable(slot) && !isResultSlot(slot)) + { + // a parameter's slot, assigned (isParameterSlot is the unassigned one) + auto argument = mlir::dyn_cast_or_null(slot.getInitializer()); + if (argument && argument.getOwner()->isEntryBlock() && + mlir::isa(argument.getOwner()->getParentOp())) + { + op->emitError("'") << varName(slot) << "' is a parameter and cannot be assigned under -mm=own yet"; + return true; + } + + // MLIRGen makes no owner of a local declared without a value, nor of one declared in a + // catch or finally clause, whose release would be a call inside an exception funclet + if (!slot.getInitializer()) + { + op->emitError("'") << varName(slot) + << "' owns nothing under -mm=own (it is declared without a value, or in a catch or " + "finally clause), so it cannot be given a new value here"; + return true; + } + } + + // merged from branches or loop iterations + if (auto argument = mlir::dyn_cast(root); argument && !argument.getOwner()->isEntryBlock()) + { + op->emitError("'") << name + << "' is merged from several branches (`?:`, `&&`, `||`, `??`, a loop); -mm=own cannot " + "prove which one owns it yet"; + return true; + } + + auto loadOp = root.getDefiningOp(); + + // a read of a global + if (auto addressOfOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::AddressOfOp()) + { + // a local that reads it: phase 3's borrow, which a call or an assignment of the global + // may end + if (mlir::isa(op)) + { + op->emitError("'") << name << "' borrows the global '" << addressOfOp.getGlobalName() + << "', but -mm=own cannot prove the global keeps its value while '" << name + << "' is used: a call or an assignment may overwrite it"; + return true; + } + + op->emitError("'") << name << "' borrows the global '" << addressOfOp.getGlobalName() + << "' and cannot be stored, returned or captured"; + return true; + } + + // a read of a captured variable, through its cell + if (loadOp && isLoadedCell(loadOp.getReference())) + { + op->emitError("'") << name << "' borrows a captured variable and cannot be stored, returned or captured"; + return true; + } + + // an object literal built in a local, holding a value it owns: moved only into an owning + // local or the result (recordRetainOf) + auto record = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + if (!record && storeOp) + { + auto propertyRefOp = storeOp.getReference().getDefiningOp(); + record = propertyRefOp ? propertyRefOp->getOperand(0).getDefiningOp() : mlir_ts::VariableOp(); + } + + if (record && !isOwningVariable(record) && record.getInitializer() && + record.getInitializer().getDefiningOp()) + { + op->emitError("an object literal that holds a value it owns is moved only into a local or " + "returned as it is; -mm=own cannot move it here yet"); + return true; + } + + return false; + } + + llvm::StringRef OwnershipInferencePass::paramName(int index) + { + llvm::StringRef name = "this value"; + getFunction()->walk([&](mlir_ts::VariableOp varOp) { + auto argument = mlir::dyn_cast_or_null(varOp.getInitializer()); + if (argument && argument.getOwner()->isEntryBlock() && static_cast(argument.getArgNumber()) == index) + { + name = varName(varOp); + } + }); + + return name; + } + + bool OwnershipInferencePass::isReturned(mlir::Value root) + { + auto returned = false; + forEachUse(root, [&](mlir::Operation *user, mlir::Value used) { + auto storeOp = mlir::dyn_cast(user); + auto slot = storeOp && storeOp.getValue() == used ? storeOp.getReference().getDefiningOp() + : mlir_ts::VariableOp(); + returned = returned || (slot && isResultSlot(slot)); + }); + + return returned; + } + + mlir::Operation *OwnershipInferencePass::otherReturn(int param) + { + mlir::Operation *found = nullptr; + getFunction()->walk([&](mlir_ts::StoreOp storeOp) { + auto slot = storeOp.getReference().getDefiningOp(); + if (found || !slot || !isResultSlot(slot) || holdsNoBlock(storeOp.getValue())) + { + return; + } + + auto borrows = borrowedParam(storeOp.getValue()); + if (param >= 0 ? borrows != param : borrows >= 0) + { + found = storeOp; + } + }); + + return found; + } + + void OwnershipInferencePass::noteLostFacts(mlir::InFlightDiagnostic &diag) + { + auto f = getFunction(); + if (auto why = f->getAttrOfType(OWN_FACTS_LOST_ATTR_NAME)) + { + diag.attachNote(f.getLoc()) << "'" << f.getName() << "' could return a borrow or keep a parameter, but " + << why.getValue(); + } + } + + void OwnershipInferencePass::reportKeptOnSomePaths(mlir::Operation *move, mlir::Operation *exit, llvm::StringRef name) + { + if (!reporting()) + { + return; + } + + auto diag = move->emitError("'") << name + << "' is moved here on some paths only; -mm=own cannot release it " + "on the others yet"; + diag.attachNote(exit->getLoc()) << "returns here without moving it; the caller gave it up"; + signalPassFailure(); + } + + void OwnershipInferencePass::reportHandleInOwningUnion(mlir::Operation *op) + { + auto handleIn = llvm::any_of(op->getOperandTypes(), [](mlir::Type type) { + return MLIRTypeHelper::isSharedHandleType(type); + }); + if (!handleIn) + { + return; + } + + for (auto resultType : op->getResultTypes()) + { + if (!MLIRTypeHelper::isSharedInOwningUnionType(resultType)) + { + continue; + } + + if (!reporting()) + { + return; + } + + auto unionType = resultType; + if (auto optionalType = mlir::dyn_cast(unionType)) + { + unionType = optionalType.getElementType(); + } + + mlir::Type shared; + mlir::Type owning; + for (auto member : mlir::cast(unionType).getTypes()) + { + if (mlir::isa(member)) + { + shared = shared ? shared : member; + } + else if (!owning && MLIRTypeHelper::ownsHeapMemoryOfUnionMember(member)) + { + owning = member; + } + } + + auto diag = op->emitError("a ") << printed(shared) << " in a union with "; + if (owning) + { + diag << "'" << printed(owning) << "', which -mm=own does not count,"; + } + else + { + diag << "a member -mm=own does not count,"; + } + + diag << " is not supported"; + signalPassFailure(); + return; + } + } + + std::string OwnershipInferencePass::printed(mlir::Type type) + { + llvm::SmallString<128> text; + llvm::raw_svector_ostream out(text); + MLIRPrinter printer{}; + printer.printType(out, type); + return text.str().str(); + } + + void OwnershipInferencePass::reportGivenNotOwned(mlir::Operation *call, mlir::Value arg) + { + if (!reporting()) + { + return; + } + + llvm::StringRef name = ownerName(arg); + if (auto loadOp = rootOf(arg).getDefiningOp()) + { + if (auto varOp = loadOp.getReference().getDefiningOp()) + { + name = varName(varOp); + } + } + + auto callee = calleeName(call); + auto diag = call->emitError("argument '") << name << "' is given to '" << (callee.empty() ? "a function" : callee) + << "', which keeps it, but -mm=own cannot move it here: this " + "function does not own it"; + noteLostFacts(diag); + signalPassFailure(); + } + + void OwnershipInferencePass::reportUseAfterMove(mlir::Operation *use, mlir::Operation *move, llvm::StringRef name) + { + if (!reporting()) + { + return; + } + + auto diag = use->emitError("'") << name << "' is used here after its value was moved"; + diag.attachNote(move->getLoc()) << "value moved here"; + signalPassFailure(); + } + + void OwnershipInferencePass::reportMovedInLoop(mlir::Operation *move, llvm::StringRef name) + { + if (!reporting()) + { + return; + } + + move->emitError("'") << name + << "' is moved inside a loop but was made outside it; only a borrow could do " + "that, and -mm=own cannot prove one yet"; + signalPassFailure(); + } + + void OwnershipInferencePass::reportBorrowOutlives(mlir::Operation *use, mlir::Operation *end, llvm::StringRef name, llvm::StringRef owner) + { + if (!reporting()) + { + return; + } + + auto diag = use->emitError("'") << name << "' borrows '" << owner << "' but is used here after '" << owner + << "' is released or overwritten"; + diag.attachNote(end->getLoc()) << "'" << owner << "' is released or overwritten here"; + signalPassFailure(); + } + + void OwnershipInferencePass::reportPlaceBorrowOutlives(mlir::Operation *use, mlir::Operation *drop, llvm::StringRef name, + llvm::StringRef place) + { + if (!reporting()) + { + return; + } + + auto diag = use->emitError("'") << name << "' borrows " << place + << " but is used here after it may be released or overwritten"; + diag.attachNote(drop->getLoc()) << "it may be released or overwritten here"; + signalPassFailure(); + } + + void OwnershipInferencePass::reportPlaceBorrowerAssigned(mlir::Operation *op, llvm::StringRef name, llvm::StringRef place) + { + if (!reporting()) + { + return; + } + + op->emitError("'") << name << "' borrows " << place << " and cannot be assigned; -mm=own cannot prove that yet"; + signalPassFailure(); + } + + void OwnershipInferencePass::reportPlaceBorrowEscapes(mlir::Operation *op, llvm::StringRef name, llvm::StringRef place) + { + if (!reporting()) + { + return; + } + + auto diag = op->emitError("'") << name << "' borrows " << place << " and cannot be stored, returned or captured"; + noteLostFacts(diag); + signalPassFailure(); + } + + void OwnershipInferencePass::reportMovedWhileBorrowed(mlir::Operation *op, llvm::StringRef owner, llvm::StringRef borrower) + { + if (!reporting()) + { + return; + } + + op->emitError("'") << owner << "' is moved here while '" << borrower + << "' borrows it; a borrowed value cannot be moved"; + signalPassFailure(); + } + + void OwnershipInferencePass::reportBorrowEscapes(mlir::Operation *op, llvm::StringRef name, llvm::StringRef owner) + { + if (!reporting()) + { + return; + } + + auto diag = op->emitError("'") << name << "' borrows '" << owner << "' and cannot be stored, returned or captured"; + noteLostFacts(diag); + signalPassFailure(); + } + + void OwnershipInferencePass::reportBorrowerAssigned(mlir::Operation *op, llvm::StringRef name, llvm::StringRef owner) + { + if (!reporting()) + { + return; + } + + op->emitError("'") << name << "' borrows '" << owner << "' and cannot be assigned; -mm=own cannot prove that yet"; + signalPassFailure(); + } + + void OwnershipInferencePass::reportEscapingCellNotOwned(mlir::Operation *capture, mlir::Operation *escape, llvm::StringRef why) + { + if (!reporting()) + { + return; + } + + auto diag = capture->emitError("a closure that escapes owns what it captures, but -mm=own cannot move this " + "variable into it: ") + << why; + diag.attachNote(escape->getLoc()) << "the closure escapes here"; + signalPassFailure(); + } + + void OwnershipInferencePass::reportUsedAfterEscapingCapture(mlir::Operation *use, mlir::Operation *capture, llvm::StringRef name) + { + if (!reporting()) + { + return; + } + + auto diag = use->emitError("'") << name << "' is captured by a closure that escapes, and is still used here"; + diag.attachNote(capture->getLoc()) << "the closure takes it here"; + signalPassFailure(); + } + + void OwnershipInferencePass::reportClosureOutlives(mlir::Operation *use, mlir::Operation *end, llvm::StringRef closure, + llvm::StringRef captured) + { + if (!reporting()) + { + return; + } + + auto diag = use->emitError("'") << closure << "' borrows '" << captured << "', which it captures, but runs here after '" + << captured << "' is released"; + diag.attachNote(end->getLoc()) << "'" << captured << "' is released here"; + signalPassFailure(); + } + + void OwnershipInferencePass::reportCaptureNotOwned(mlir::Operation *op, llvm::StringRef captured) + { + if (!reporting()) + { + return; + } + + op->emitError("'") << captured << "' is captured by a closure, but -mm=own cannot tell who owns its value here"; + signalPassFailure(); + } + + void OwnershipInferencePass::reportCapturedParamAssigned(mlir::Operation *op, llvm::StringRef name) + { + if (!reporting()) + { + return; + } + + op->emitError("'") << name + << "' is a captured parameter and cannot be assigned under -mm=own: its value is the " + "caller's"; + signalPassFailure(); + } + + void OwnershipInferencePass::reportMovedOnSomePaths(mlir::Operation *move, mlir::Operation *release, llvm::StringRef name, + mlir::Operation *usedAt) + { + if (!reporting()) + { + return; + } + + auto diag = move->emitError("'") << name + << "' is moved here on some paths only; -mm=own cannot release it " + "on the others yet"; + diag.attachNote(release->getLoc()) << "released here on every path"; + if (usedAt) + { + diag.attachNote(usedAt->getLoc()) + << "used here, after the paths join, so it cannot be released before they do"; + } + + signalPassFailure(); + } + +} // namespace own_inference diff --git a/tslang/lib/TypeScript/OwnershipInferenceEdgeReleases.cpp b/tslang/lib/TypeScript/OwnershipInferenceEdgeReleases.cpp new file mode 100644 index 000000000..678e7f158 --- /dev/null +++ b/tslang/lib/TypeScript/OwnershipInferenceEdgeReleases.cpp @@ -0,0 +1,571 @@ +// OwnershipInferencePass (see OwnershipInferencePass.h): a move on some paths only (spec 30) - +// the release placed on the edges where the paths that did not move join the ones that did. + +#include "OwnershipInferencePass.h" + +#define DEBUG_TYPE "own" + +namespace own_inference +{ + + bool OwnershipInferencePass::isJoined(unsigned state) + { + return (state & SlotLive) && (state & SlotMoved); + } + + bool OwnershipInferencePass::carriesPointer(mlir::Type type) + { + return !mlir::isa(type); + } + + bool OwnershipInferencePass::reachableFrom(mlir::Block *start, mlir::Operation *to, llvm::ArrayRef kills) + { + auto &body = getFunction().getFunctionBody(); + to = body.findAncestorOpInRegion(*to); + if (!to) + { + return true; + } + + llvm::SmallVector killAt; + for (auto *kill : kills) + { + if (auto *top = body.findAncestorOpInRegion(*kill)) + { + killAt.push_back(top); + } + } + + llvm::SmallPtrSet seen; + llvm::SmallVector work{start}; + while (!work.empty()) + { + auto *block = work.pop_back_val(); + if (!seen.insert(block).second) + { + continue; + } + + auto killed = [&](mlir::Operation *before) { + return llvm::any_of(killAt, [&](mlir::Operation *kill) { + return kill->getBlock() == block && (!before || kill->isBeforeInBlock(before) || kill == before); + }); + }; + + if (to->getBlock() == block && !killed(to)) + { + return true; + } + + if (!killed(nullptr)) + { + work.append(block->getSuccessors().begin(), block->getSuccessors().end()); + } + } + + return false; + } + + llvm::SmallPtrSet OwnershipInferencePass::handlerBlocks() + { + llvm::SmallPtrSet handler; + llvm::SmallVector work; + for (auto &block : getFunction().getFunctionBody()) + { + if (!block.empty() && mlir::isa(block.front())) + { + work.push_back(&block); + } + } + + while (!work.empty()) + { + auto *block = work.pop_back_val(); + if (!handler.insert(block).second) + { + continue; + } + + auto ends = llvm::any_of(*block, [](mlir::Operation &op) { + return mlir::isa(op); + }); + if (!ends) + { + work.append(block->getSuccessors().begin(), block->getSuccessors().end()); + } + } + + return handler; + } + + bool OwnershipInferencePass::placeOnUnmovedEdges(mlir_ts::VariableOp varOp, mlir::Operation *acquire, mlir::Operation *retain, + mlir::Operation *taker, llvm::ArrayRef assignments, + llvm::SetVector &toErase) + { + joinedUse = nullptr; + llvm::SmallVector releases; + if (!slotReleasesInBody(varOp, acquire, assignments, releases)) + { + return false; + } + + auto states = slotStates(varOp, acquire, assignments, releases); + + // every release a moved value reaches goes; one some path reaches with nothing in the slot + // is not this rule's + llvm::SmallVector erased; + for (auto *release : releases) + { + auto state = states.before.lookup(release); + if (state & SlotMoved) + { + if (state & SlotDead) + { + return false; + } + + erased.push_back(release); + } + } + + llvm::SmallVector placed; + llvm::SetVector releasedInto; + if (!unmovedEdges(varOp, states, placed, releasedInto)) + { + return false; + } + + if (!noUseWhereJoined(varOp, retain, taker, assignments, states, releasedInto)) + { + return false; + } + + toErase.insert(erased.begin(), erased.end()); + edgeReleases.append(placed.begin(), placed.end()); + return true; + } + + bool OwnershipInferencePass::slotReleasesInBody(mlir_ts::VariableOp varOp, mlir::Operation *acquire, + llvm::ArrayRef assignments, llvm::SmallVectorImpl &releases) + { + auto &body = getFunction().getFunctionBody(); + if (!varOp || varOp.getCaptured().value_or(false) || varOp->getParentRegion() != &body || + acquire->getParentRegion() != &body || + llvm::any_of(assignments, [&](mlir::Operation *op) { return op->getParentRegion() != &body; })) + { + return false; + } + + auto slot = varOp.getResult(); + for (auto *user : slot.getUsers()) + { + if (mlir::isa(user)) + { + if (user->getParentRegion() != &body) + { + return false; + } + + releases.push_back(user); + } + } + + return true; + } + + OwnershipInferencePass::SlotStates OwnershipInferencePass::slotStates(mlir_ts::VariableOp varOp, mlir::Operation *acquire, llvm::ArrayRef assignments, + llvm::ArrayRef releases) + { + auto &body = getFunction().getFunctionBody(); + auto step = [&](mlir::Operation *op, unsigned state) -> unsigned { + if (op == varOp.getOperation() || llvm::is_contained(assignments, op)) + { + return SlotLive; + } + + if (op == acquire) + { + return SlotMoved; + } + + if (llvm::is_contained(releases, op)) + { + return SlotDead; + } + + return state; + }; + + SlotStates states; + auto &entry = states.entry; + auto exitOf = [&](mlir::Block *block) { + auto state = entry.lookup(block); + for (auto &op : *block) + { + state = step(&op, state); + } + + return state; + }; + + entry[&body.front()] = SlotDead; + llvm::SmallVector work{&body.front()}; + while (!work.empty()) + { + auto *block = work.pop_back_val(); + auto state = exitOf(block); + for (auto *successor : block->getSuccessors()) + { + auto &into = entry[successor]; + if ((into | state) != into) + { + into |= state; + work.push_back(successor); + } + } + } + + // ...and before each op of the body + for (auto &block : body) + { + auto state = entry.lookup(&block); + for (auto &op : block) + { + states.before[&op] = state; + state = step(&op, state); + } + + states.exit[&block] = state; + } + + return states; + } + + bool OwnershipInferencePass::unmovedEdges(mlir_ts::VariableOp varOp, const SlotStates &states, llvm::SmallVectorImpl &placed, + llvm::SetVector &releasedInto) + { + auto &body = getFunction().getFunctionBody(); + auto slot = varOp.getResult(); + auto &entry = states.entry; + + // the edges into the join that hold the value and did not move it + auto handler = handlerBlocks(); + for (auto &block : body) + { + if (!entry.count(&block)) + { + continue; // unreachable + } + + auto state = states.exit.lookup(&block); + for (auto index : llvm::seq(0, block.getNumSuccessors())) + { + auto into = entry.lookup(block.getSuccessor(index)); + if (!isJoined(into)) + { + continue; + } + + // a join some path reaches with nothing in the slot is not this rule's + if (into & SlotDead) + { + return false; + } + + // an edge that moved it, or from where the paths have joined already + if (state != SlotLive) + { + continue; + } + + auto *terminator = block.getTerminator(); + if (!mlir::isa(terminator) || handler.contains(&block) || + !dominance->dominates(varOp.getOperation(), terminator)) + { + return false; + } + + placed.push_back({slot, &block, static_cast(index), terminator->getLoc()}); + releasedInto.insert(block.getSuccessor(index)); + } + } + + return !placed.empty(); + } + + bool OwnershipInferencePass::noUseWhereJoined(mlir_ts::VariableOp varOp, mlir::Operation *retain, mlir::Operation *taker, + llvm::ArrayRef assignments, const SlotStates &states, + const llvm::SetVector &releasedInto) + { + auto &body = getFunction().getFunctionBody(); + auto slot = varOp.getResult(); + + auto joinedAt = [&](mlir::Operation *user) { + auto *top = body.findAncestorOpInRegion(*user); + return !top || isJoined(states.before.lookup(top)); + }; + + // Nothing uses the value where the paths have joined - a read there, on the path that did + // not move it, needs what the edge released - neither through the slot nor through anything + // read out of it: a field, a view, a borrowing local, a closure. The move itself hands the + // value to its new owner, whose reads are its own. A value the slot is given - its + // initializer, an assignment's - is the slot's only once it is stored: a use before that (a + // constructor's, or one around a loop before it is made again) is not a use of it. + // + // Past the joined points too: what was read out of the value before an edge released it + // may not be used after that edge - after a new value is stored into the slot, or after its + // scope ends - unless it was read out again (spec 30). A read of the slot after a new value + // is stored reads that one. + struct Origin + { + mlir::Operation *stored; + mlir::Operation *made; + }; + + llvm::SmallVector origins{{nullptr, nullptr}}; + llvm::SmallVector> values; + llvm::DenseSet> seen; + auto taint = [&](mlir::Value value, unsigned origin) { + if (carriesPointer(value.getType()) && seen.insert({value, origin}).second) + { + values.push_back({value, origin}); + } + }; + + auto given = [&](mlir::Value value, mlir::Operation *stored) { + origins.push_back({stored, value.getDefiningOp()}); + taint(value, origins.size() - 1); + }; + + taint(slot, 0); + if (varOp.getInitializer()) + { + given(varOp.getInitializer(), varOp); + } + + for (auto *assignment : assignments) + { + given(mlir::cast(assignment).getValue(), assignment); + } + + // reached from an edge that released the value, before the read it came from (or the op + // that made it) runs again + auto usedAfterRelease = [&](mlir::Operation *made, mlir::Operation *user) { + llvm::SmallVector madeAgain; + if (made) + { + madeAgain.push_back(made); + } + + return llvm::any_of(releasedInto, [&](mlir::Block *join) { return reachableFrom(join, user, madeAgain); }); + }; + + auto heldAt = [&](unsigned origin, mlir::Operation *user) { + auto &from = origins[origin]; + if (!from.stored) + { + return true; + } + + llvm::SmallVector madeAgain; + if (from.made) + { + madeAgain.push_back(from.made); + } + + return reachableAfter(from.stored, user, madeAgain); + }; + + while (!values.empty()) + { + auto [value, origin] = values.pop_back_val(); + for (auto &use : value.getUses()) + { + auto *user = use.getOwner(); + if (mlir::isa(user) || user == retain) + { + continue; + } + + // the slot's own releases and assignments are decided above + if (value == slot && (mlir::isa(user) || llvm::is_contained(assignments, user))) + { + continue; + } + + if (joinedAt(user) && heldAt(origin, user)) + { + joinedUse = user; + return false; + } + + if (value != slot && usedAfterRelease(origins[origin].made, user)) + { + joinedUse = user; + return false; + } + + if (user == taker) + { + continue; + } + + // a store of it into a local makes every read of that local one; one into a place + // is not followed, and the release is not placed + if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getValue() == value) + { + auto into = storeOp.getReference().getDefiningOp(); + if (!into || into.getCaptured().value_or(false)) + { + return false; + } + + taint(into.getResult(), origin); + continue; + } + + if (auto branch = mlir::dyn_cast(user)) + { + for (auto index : llvm::seq(0, user->getNumSuccessors())) + { + auto operands = branch.getSuccessorOperands(index); + for (auto [position, operand] : llvm::enumerate(operands.getForwardedOperands())) + { + if (operand == value) + { + taint(user->getSuccessor(index)->getArgument(operands.getProducedOperandCount() + position), + origin); + } + } + } + } + + // a call's result is its own block, unless its facts say it borrows this argument - a + // plain borrow, or a borrowing record that holds it (spec 32) + auto borrows = true; + if (isCall(user)) + { + auto args = callArgs(user); + borrows = llvm::any_of(resultBorrowsAll(user), [&](int index) { + return static_cast(index) < args.size() && args[index] == value; + }); + } + + // a read of the slot is what its uses came from + auto from = origin; + if (value == slot && user->getNumResults() > 0) + { + origins.push_back({nullptr, user}); + from = origins.size() - 1; + } + + for (auto result : user->getResults()) + { + if (borrows) + { + taint(result, from); + } + } + + // inside a region - a closure, an async body - it is the op that holds the region + // that hands it on + if (user->getParentRegion() != &body) + { + for (auto result : body.findAncestorOpInRegion(*user)->getResults()) + { + taint(result, origin); + } + } + } + } + + return true; + } + + void OwnershipInferencePass::placeEdgeReleases() + { + for (auto &edge : edgeReleases) + { + auto *terminator = edge.from->getTerminator(); + mlir::OpBuilder builder(terminator); + if (terminator->getNumSuccessors() == 1) + { + builder.create(edge.loc, edge.slot); + continue; + } + + auto *join = terminator->getSuccessor(edge.successor); + auto operands = mlir::cast(terminator).getSuccessorOperands(edge.successor); + llvm::SmallVector forwarded(operands.getForwardedOperands()); + auto *split = new mlir::Block(); + split->insertAfter(edge.from); + builder.setInsertionPointToEnd(split); + builder.create(edge.loc, edge.slot); + builder.create(edge.loc, join, forwarded); + if (!forwarded.empty()) + { + operands.erase(0, forwarded.size()); + } + + terminator->setSuccessor(split, edge.successor); + } + + edgeReleases.clear(); + } + + llvm::SmallVector OwnershipInferencePass::slotAssignments(mlir::Value slot) + { + llvm::SmallVector assignments; + for (auto *user : slot.getUsers()) + { + auto storeOp = mlir::dyn_cast(user); + auto releaseOp = storeOp ? mlir::dyn_cast_or_null(storeOp->getPrevNode()) + : mlir_ts::ReleaseSlotOp(); + if (releaseOp && storeOp.getReference() == slot && releaseOp.getSlot() == slot) + { + assignments.push_back(storeOp); + } + } + + return assignments; + } + + mlir::Operation *OwnershipInferencePass::readUsedAfter(mlir_ts::LoadOp read, mlir::Operation *retain, mlir::Operation *taker, + mlir::Operation *kill, + llvm::function_ref isRead) + { + llvm::SmallVector values{read.getResult()}; + while (!values.empty()) + { + auto current = values.pop_back_val(); + for (auto *user : current.getUsers()) + { + if (user == retain || user == taker || mlir::isa(user)) + { + continue; + } + + if (mlir::isa(user) || isView(user)) + { + values.push_back(user->getResult(0)); + continue; + } + + llvm::SmallVector kills{read.getOperation()}; + if (kill) + { + kills.push_back(kill); + } + + if (reachableAfter(taker, user, kills) && !(isRead && isRead(user, current))) + { + return user; + } + } + } + + return nullptr; + } + +} // namespace own_inference diff --git a/tslang/lib/TypeScript/OwnershipInferenceGlobalReads.cpp b/tslang/lib/TypeScript/OwnershipInferenceGlobalReads.cpp new file mode 100644 index 000000000..bcbbcee57 --- /dev/null +++ b/tslang/lib/TypeScript/OwnershipInferenceGlobalReads.cpp @@ -0,0 +1,265 @@ +// OwnershipInferencePass (see OwnershipInferencePass.h): a read of a mortal global, ended at the +// global's assignment, and the locals that hold it (spec 25). + +#include "OwnershipInferencePass.h" + +#define DEBUG_TYPE "own" + +namespace own_inference +{ + + std::optional OwnershipInferencePass::borrowHolderOf(mlir::Operation *user) + { + BorrowHolder holder; + mlir::Value ref; + if (auto storeOp = mlir::dyn_cast(user)) + { + ref = storeOp.getReference(); + while (auto propertyRefOp = ref.getDefiningOp()) + { + holder.path.insert(holder.path.begin(), propertyRefOp.getPosition()); + ref = propertyRefOp.getObjectRef(); + } + } + else if (auto varOp = mlir::dyn_cast(user)) + { + ref = varOp.getResult(); + } + + auto varOp = ref ? ref.getDefiningOp() : mlir_ts::VariableOp(); + if (!varOp || varOp.getCaptured().value_or(false) || isCellVariable(varOp.getResult())) + { + return std::nullopt; + } + + holder.local = varOp; + return holder; + } + + llvm::SmallVector OwnershipInferencePass::holderRenewals(BorrowHolder holder) + { + llvm::SmallVector renewals; + if (holder.local.getInitializer()) + { + renewals.push_back(holder.local); + } + + llvm::SmallVector> refs{{holder.local.getResult(), 0}}; + while (!refs.empty()) + { + auto [ref, depth] = refs.pop_back_val(); + for (auto *refUser : ref.getUsers()) + { + if (auto propertyRefOp = mlir::dyn_cast(refUser)) + { + if (depth < holder.path.size() && propertyRefOp.getPosition() == holder.path[depth]) + { + refs.push_back({propertyRefOp.getResult(), depth + 1}); + } + } + else if (auto storeOp = mlir::dyn_cast(refUser); storeOp && storeOp.getReference() == ref) + { + renewals.push_back(storeOp); + } + } + } + + return renewals; + } + + bool OwnershipInferencePass::pathsMeet(llvm::ArrayRef a, llvm::ArrayRef b) + { + auto common = std::min(a.size(), b.size()); + return a.take_front(common) == b.take_front(common); + } + + bool OwnershipInferencePass::isGlobalRead(mlir_ts::LoadOp loadOp) + { + auto addressOfOp = loadOp.getReference().getDefiningOp(); + auto result = loadOp.getResult(); + return addressOfOp && !addressOfOp->hasAttr(OWN_IMMORTAL_GLOBAL_ATTR_NAME) && ownsHeap(result) && !isHandle(result); + } + + void OwnershipInferencePass::checkGlobalRead(mlir_ts::LoadOp read) + { + // each access of a global is a `ts.AddressOf` of its own + auto global = read.getReference().getDefiningOp().getGlobalName(); + llvm::SmallVector uses; + llvm::StringSet<> followed; + mlir::Operation *keptAt = nullptr; + // rc's temporary release of a call's result that borrows an argument goes with that result + // (walkBorrowed's `keeping`); the owner checks decide it + llvm::SmallVector bookkeeping; + Keeping keeping{&bookkeeping, false}; + std::function)> intoLocal; + + // the uses of what `holder` holds, made again at its renewals: those depend on the holder + // alone, not on the way the walk reached it, so a holder is followed once. A value loaded + // out of it is made again only by a path back through that load (the load itself is a use, + // bounded by the renewals): a store into the path after the load gives the loaded value + // nothing new. + auto followHolder = [&](BorrowHolder holder) { + std::string key; + llvm::raw_string_ostream keyOut(key); + keyOut << holder.local.getOperation(); + for (auto position : holder.path) + { + keyOut << "." << position; + } + + if (!followed.insert(keyOut.str()).second) + { + return true; + } + + auto localKills = holderRenewals(holder); + llvm::SmallVector>> refs{{holder.local.getResult(), {}}}; + while (!refs.empty()) + { + auto [ref, path] = refs.pop_back_val(); + for (auto *refUser : ref.getUsers()) + { + if (auto propertyRefOp = mlir::dyn_cast(refUser)) + { + auto fieldPath = path; + fieldPath.push_back(propertyRefOp.getPosition()); + if (pathsMeet(fieldPath, holder.path)) + { + refs.push_back({propertyRefOp.getResult(), fieldPath}); + } + + continue; + } + + if (ref == holder.local.getResult() && + mlir::isa(refUser)) + { + continue; + } + + uses.push_back({refUser, {localKills.begin(), localKills.end()}}); + if (auto loadOp = mlir::dyn_cast(refUser)) + { + if (auto *kept = walkBorrowed(loadOp.getResult(), loadOp.getOperation(), uses, intoLocal, keeping)) + { + keptAt = keptAt ? keptAt : kept; + return false; + } + } + } + } + + return true; + }; + + intoLocal = [&](mlir::Operation *user, llvm::ArrayRef kills) { + // a tuple taken apart (`{ ...b }`, a destructuring): each field is the borrow again + if (mlir::isa(user)) + { + for (auto result : user->getResults()) + { + if (!mayPointInto(result)) + { + continue; + } + + if (auto *kept = walkBorrowed(result, kills, uses, intoLocal, keeping)) + { + keptAt = keptAt ? keptAt : kept; + return false; + } + } + + return true; + } + + auto holder = borrowHolderOf(user); + return holder && followHolder(*holder); + }; + + auto *kept = walkBorrowed(read.getResult(), read.getOperation(), uses, intoLocal, keeping); + + // a value made from the borrow (a field of a tuple read out of a holder, `{ ...b }`) stored + // into another local, or declaring one: that local holds the borrow too + for (size_t index = 0; !kept && !keptAt && index < uses.size(); index++) + { + auto *op = uses[index].op; + auto storeOp = mlir::dyn_cast(op); + auto varOp = mlir::dyn_cast(op); + auto value = storeOp ? storeOp.getValue() : varOp ? varOp.getInitializer() : mlir::Value(); + if (!value || !mayPointInto(value)) + { + continue; + } + + if (auto holder = borrowHolderOf(op)) + { + followHolder(*holder); + } + } + + if (kept || keptAt) + { + if (mlir::isa(read.getType())) + { + return; + } + + if (reporting()) + { + auto *at = keptAt ? keptAt : kept; + at->emitError("the value read from '") + << global << "' is kept where its end at an assignment of '" << global + << "' cannot be checked; -mm=own cannot prove it outlives the global's value"; + signalPassFailure(); + } + + return; + } + + llvm::SmallVector ends; + getFunction().walk([&](mlir::Operation *op) { + mlir::Value slot; + if (auto releaseSlotOp = mlir::dyn_cast(op)) + { + slot = releaseSlotOp.getSlot(); + } + else if (auto storeOp = mlir::dyn_cast(op)) + { + slot = storeOp.getReference(); + } + + auto other = slot ? slot.getDefiningOp() : mlir_ts::AddressOfOp(); + if (other && other.getGlobalName() == global) + { + ends.push_back(op); + } + + // a call that may give the global a new value (OwnershipSignaturePass, pinAssignedGlobals): + // `const c = G; reset(); print(c.x)` + if (isCall(op)) + { + auto assigned = op->getAttrOfType(OWN_ASSIGNS_GLOBALS_ATTR_NAME); + if (op->hasAttr(OWN_ASSIGNS_ANY_GLOBAL_ATTR_NAME) || + (assigned && llvm::any_of(assigned.getAsRange(), + [&](mlir::StringAttr name) { return name.getValue() == global; }))) + { + ends.push_back(op); + } + } + }); + + for (auto *end : ends) + { + for (auto &use : uses) + { + if (use.op != end && reachableAfter(end, use.op, use.kills)) + { + reportBorrowOutlives(use.op, end, ownerName(read.getResult()), global); + return; + } + } + } + } + +} // namespace own_inference diff --git a/tslang/lib/TypeScript/OwnershipInferenceKeptParams.cpp b/tslang/lib/TypeScript/OwnershipInferenceKeptParams.cpp new file mode 100644 index 000000000..2d7c6c060 --- /dev/null +++ b/tslang/lib/TypeScript/OwnershipInferenceKeptParams.cpp @@ -0,0 +1,202 @@ +// OwnershipInferencePass (see OwnershipInferencePass.h): a kept parameter released where it is +// not moved (spec 33). + +#include "OwnershipInferencePass.h" + +#define DEBUG_TYPE "own" + +namespace own_inference +{ + + mlir_ts::VariableOp OwnershipInferencePass::parameterSlot(int32_t index) + { + auto &body = getFunction().getFunctionBody(); + if (body.empty() || static_cast(index) >= body.front().getNumArguments()) + { + return {}; + } + + for (auto *user : body.front().getArgument(index).getUsers()) + { + auto varOp = mlir::dyn_cast(user); + auto position = -1; + if (varOp && isParameterSlot(varOp, position) && position == index) + { + return varOp; + } + } + + return {}; + } + + bool OwnershipInferencePass::isReleasedParam(mlir::Value slot) + { + auto f = getFunction(); + auto attr = f->getAttrOfType(OWN_PARAMS_RELEASED_ATTR_NAME); + auto varOp = slot.getDefiningOp(); + auto index = -1; + return attr && varOp && isParameterSlot(varOp, index) && llvm::is_contained(attr.asArrayRef(), index) && + llvm::is_contained(ownedParams(f), index); + } + + bool OwnershipInferencePass::readsSlot(mlir::Value value, mlir::Value slot, int depth) + { + auto *def = value.getDefiningOp(); + if (!def) + { + return false; + } + + // too deep to tell: as if it did + if (depth > 16) + { + return true; + } + + if (auto loadOp = mlir::dyn_cast(def); loadOp && loadOp.getReference() == slot) + { + return true; + } + + return llvm::any_of(def->getOperands(), [&](mlir::Value operand) { return readsSlot(operand, slot, depth + 1); }); + } + + void OwnershipInferencePass::releaseKeptParamsAtExits() + { + exitReleases.clear(); + auto f = getFunction(); + auto attr = f->getAttrOfType(OWN_PARAMS_RELEASED_ATTR_NAME); + if (!attr) + { + return; + } + + for (auto index : attr.asArrayRef()) + { + auto varOp = parameterSlot(index); + if (!varOp || !llvm::is_contained(ownedParams(f), index)) + { + continue; + } + + for (auto &block : f.getFunctionBody()) + { + auto *exit = block.empty() ? nullptr : &block.back(); + // a thrown value made of the parameter takes it with it: no release there (a leak, + // if it does not) + auto leaves = mlir::isa_and_nonnull(exit) || + (mlir::isa_and_nonnull(exit) && + !readsSlot(exit->getOperand(0), varOp.getResult())); + if (!leaves) + { + continue; + } + + mlir::OpBuilder builder(exit); + exitReleases.push_back(builder.create(exit->getLoc(), varOp.getResult())); + } + } + } + + void OwnershipInferencePass::decideParam(mlir::Value slot, llvm::ArrayRef receivers, llvm::SetVector &toErase) + { + auto name = varName(slot.getDefiningOp()); + auto &receiver = receivers.front(); + auto *taker = takerOf(receiver.retain, receiver.value); + if (receivers.size() > 1) + { + auto &second = receivers[1]; + auto *secondTaker = takerOf(second.retain, second.value); + reportUseAfterMove(secondTaker ? secondTaker : second.retain, taker ? taker : receiver.retain, name); + return; + } + + // one the body releases on the paths that do not move it (spec 33): the move's own check + // erases the releases at the exits it reaches, and places one on each edge into where the + // paths join that did not move it (spec 30) + auto released = isReleasedParam(slot); + if (!checkSlotMove(receiver.retain, receiver.value, receiver.load, toErase, released)) + { + return; + } + + if (released) + { + if (!isCall(receiver.retain)) + { + toErase.insert(receiver.retain); + } + + return; + } + + auto &body = getFunction().getFunctionBody(); + auto *moved = body.findAncestorOpInRegion(*taker); + auto everyPath = true; + getFunction().walk([&](mlir_ts::ReturnInternalOp returnOp) { + auto *exit = body.findAncestorOpInRegion(*returnOp); + if (everyPath && (!moved || !exit || !dominance->dominates(moved, exit))) + { + reportKeptOnSomePaths(taker, returnOp, name); + everyPath = false; + } + }); + + // rc's retain goes with the move - a `ts.RetainSlot` too, for an owning local the parameter + // moves into (`let r = init`, spec 33); a call that keeps it is the move itself + if (everyPath && !isCall(receiver.retain)) + { + toErase.insert(receiver.retain); + } + } + + bool OwnershipInferencePass::isReadOutOfException(mlir::Value value) + { + auto loadOp = rootOf(value).getDefiningOp(); + auto argument = loadOp ? mlir::dyn_cast(loadOp.getReference()) : mlir::BlockArgument(); + return argument && argument.getOwner()->isEntryBlock() && getFunction().getName().starts_with(".eh.copy."); + } + + mlir_ts::LoadOp OwnershipInferencePass::keptParamLoadOf(mlir::Value value) + { + auto loadOp = rootOf(value).getDefiningOp(); + auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + auto index = -1; + if (!varOp || !isParameterSlot(varOp, index) || !llvm::is_contained(ownedParams(getFunction()), index)) + { + return {}; + } + + return loadOp; + } + + void OwnershipInferencePass::decideSlotReceivers(mlir::Value slot, llvm::ArrayRef receivers, llvm::StringRef owner, + llvm::SetVector &toErase) + { + auto ends = slotEnds(slot); + mlir_ts::VariableOp someBorrower; + for (auto &receiver : receivers) + { + if (auto borrower = borrowerOf(receiver.retain)) + { + someBorrower = borrower; + borrowWith(borrower, receiver.retain, ends, owner, toErase); + } + } + + for (auto &receiver : receivers) + { + if (borrowerOf(receiver.retain)) + { + continue; + } + + llvm::SetVector unused; + if (checkSlotMove(receiver.retain, receiver.value, receiver.load, unused)) + { + reportMovedWhileBorrowed(receiver.retain, owner, someBorrower ? varName(someBorrower) : "another variable"); + } + } + } + +} // namespace own_inference diff --git a/tslang/lib/TypeScript/OwnershipInferenceMoves.cpp b/tslang/lib/TypeScript/OwnershipInferenceMoves.cpp new file mode 100644 index 000000000..7bfcbde41 --- /dev/null +++ b/tslang/lib/TypeScript/OwnershipInferenceMoves.cpp @@ -0,0 +1,1081 @@ +// OwnershipInferencePass (see OwnershipInferencePass.h): moves - the one taker of a fresh value, +// the retains that are its bookkeeping, and the paths that use it after the move. + +#include "OwnershipInferencePass.h" + +#define DEBUG_TYPE "own" + +namespace own_inference +{ + + bool OwnershipInferencePass::slotHoldsInitializerAt(mlir::Operation *op) + { + auto slot = mlir::cast(op).getSlot(); + auto varOp = slot.getDefiningOp(); + if (!varOp || !varOp.getInitializer() || isCellVariable(slot) || varOp->getBlock() != op->getBlock()) + { + return false; + } + + for (auto *between = varOp->getNextNode(); between; between = between->getNextNode()) + { + if (between == op) + { + return true; + } + + if (llvm::is_contained(between->getOperands(), slot) || isCall(between) || between->getNumRegions() > 0) + { + return false; + } + } + + return false; + } + + mlir::Value OwnershipInferencePass::retainedValue(mlir::Operation *op) + { + if (auto retainOp = mlir::dyn_cast(op)) + { + return rootOf(retainOp.getReference()); + } + + auto varOp = mlir::cast(op).getSlot().getDefiningOp(); + return varOp && varOp.getInitializer() ? rootOf(varOp.getInitializer()) : mlir::Value(); + } + + bool OwnershipInferencePass::ownsHeap(mlir::Value value) + { + // a `value_ref` points at storage it does not own, but one `ts.New` made and seen as its + // object is the object's block: the root of a generator's state object or an object literal + if (value.getDefiningOp() && mlir::isa(value.getType()) && + llvm::any_of(value.getUsers(), [](mlir::Operation *user) { return mlir::isa(user) && isView(user); })) + { + return true; + } + + return ownsHeap(value.getLoc(), value.getType()); + } + + bool OwnershipInferencePass::ownsHeap(mlir::Location location, mlir::Type type) + { + return own_facts::ownsHeap(location, type); + } + + bool OwnershipInferencePass::holdsConstantsOrMoves(mlir::Value value) + { + auto loadOp = value.getDefiningOp(); + auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + // a borrowing record (spec 32) takes nothing for what it borrows, and its fresh values move in + // as an owning record's do + if (!varOp || !isConstantRecordLocal(varOp) || !(isOwningRecord(value) || borrowingRecordOf(varOp))) + { + return false; + } + + // a fresh value moved in, its move decided by its own check against this retain + auto movesIn = llvm::any_of(varOp->getUsers(), [&](mlir::Operation *user) { + auto propertyRefOp = mlir::dyn_cast(user); + if (!propertyRefOp) + { + return false; + } + + // a method's field is a `!ts.bound_ref`, which holds a function, not a block + auto refType = mlir::dyn_cast(propertyRefOp.getType()); + if (refType && !ownsHeap(propertyRefOp.getLoc(), refType.getElementType())) + { + return false; + } + + return llvm::any_of(propertyRefOp->getUsers(), + [](mlir::Operation *fieldUser) { return mlir::isa(fieldUser); }); + }); + + return !movesIn || recordRetainOf(varOp) != nullptr; + } + + mlir::Operation *OwnershipInferencePass::recordRetainOf(mlir_ts::VariableOp varOp) + { + if (!isConstantRecordLocal(varOp)) + { + return nullptr; + } + + mlir_ts::LoadOp read; + for (auto *user : varOp->getUsers()) + { + if (auto loadOp = mlir::dyn_cast(user)) + { + if (read) + { + return nullptr; + } + + read = loadOp; + } + } + + if (!read) + { + return nullptr; + } + + mlir::Operation *retain = nullptr; + auto boxed = false; + auto other = false; + for (auto *user : read->getUsers()) + { + if (mlir::isa(user)) + { + if (retain) + { + return nullptr; + } + + retain = user; + continue; + } + + // copied into the block it is given in as an object or an interface (spec 27): the box + // owns what the record held, and its own check decides where the box goes + auto storeOp = mlir::dyn_cast(user); + auto box = storeOp && storeOp.getValue() == read.getResult() ? recordBoxOf(storeOp.getReference()) + : mlir_ts::NewOp(); + if (box && box.getResult() == storeOp.getReference()) + { + if (boxed) + { + return nullptr; + } + + boxed = true; + continue; + } + + other = true; + if (mlir::isa(user)) + { + continue; + } + + // `let a = { item: new C() }`: the read is what an owning local starts with, and rc's + // `ts.RetainSlot` of that local is the record's retain + if (auto ownerOp = mlir::dyn_cast(user)) + { + auto *slotRetain = ownerOp.getInitializer() == read.getResult() && isOwningVariable(ownerOp) + ? onlyRetainSlotOf(ownerOp) + : nullptr; + if (!slotRetain || retain) + { + return nullptr; + } + + retain = slotRetain; + continue; + } + + // stored into the result slot, or into an owning local (`a = { item: new C() }`) + auto slot = storeOp && storeOp.getValue() == read.getResult() + ? storeOp.getReference().getDefiningOp() + : mlir_ts::VariableOp(); + if (!slot || !(isResultSlot(slot) || isOwningVariable(slot))) + { + return nullptr; + } + } + + // a boxed record goes nowhere else: the box is its one owner + if (boxed && other) + { + return nullptr; + } + + return retain; + } + + bool OwnershipInferencePass::isBuiltArrayRead(mlir::Operation *retain, mlir::Value value) + { + auto read = value.getDefiningOp(); + auto builder = read ? read.getReference().getDefiningOp() : mlir_ts::VariableOp(); + auto owner = mlir::cast(retain).getSlot().getDefiningOp(); + 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) + { + return false; + } + + // only filled, and only before it is read + return llvm::all_of(builder->getUses(), [&](mlir::OpOperand &use) { + auto *user = use.getOwner(); + if (user == read) + { + return true; + } + + return mlir::isa(user) && use.getOperandNumber() == 0 && + !reachableAfter(read, user, builder); + }); + } + + bool OwnershipInferencePass::isBuiltArrayReturned(mlir::Operation *retain, mlir::Value value) + { + auto read = value.getDefiningOp(); + auto builder = read ? read.getReference().getDefiningOp() : mlir_ts::VariableOp(); + if (!builder || isOwningVariable(builder) || builder.getCaptured().value_or(false) || !builder.getInitializer() || + !mlir::isa_and_nonnull(builder.getInitializer().getDefiningOp())) + { + return false; + } + + // the read is retained and returned - stored into the function's result slot - and nothing else + auto returned = false; + for (auto *user : read->getUsers()) + { + if (user == retain) + { + continue; + } + + auto storeOp = mlir::dyn_cast(user); + auto resultSlot = storeOp && storeOp.getValue() == read.getResult() + ? storeOp.getReference().getDefiningOp() + : mlir_ts::VariableOp(); + auto intoField = storeOp && storeOp.getValue() == read.getResult() && + storeOp.getReference().getDefiningOp(); + if (!((resultSlot && isResultSlot(resultSlot)) || intoField) || returned) + { + return false; + } + + returned = true; + } + + if (!returned) + { + return false; + } + + // only filled, and only before it is read + return llvm::all_of(builder->getUses(), [&](mlir::OpOperand &use) { + auto *user = use.getOwner(); + if (user == read) + { + return true; + } + + if (reachableAfter(read, user, builder)) + { + return false; + } + + if (auto loadOp = mlir::dyn_cast(user)) + { + return llvm::all_of(loadOp->getUsers(), [](mlir::Operation *loadUser) { + return mlir::isa(loadUser) && loadUser->use_empty(); + }); + } + + return mlir::isa(user) && use.getOperandNumber() == 0; + }); + } + + mlir::Operation *OwnershipInferencePass::onlyRetainSlotOf(mlir_ts::VariableOp varOp) + { + mlir::Operation *found = nullptr; + for (auto *user : varOp->getUsers()) + { + if (mlir::isa(user)) + { + if (found) + { + return nullptr; + } + + found = user; + } + } + + return found; + } + + bool OwnershipInferencePass::isResultSlot(mlir_ts::VariableOp varOp) + { + if (isOwningVariable(varOp) || varOp.getCaptured().value_or(false)) + { + return false; + } + + return llvm::all_of(varOp->getUsers(), [&](mlir::Operation *user) { + if (auto storeOp = mlir::dyn_cast(user)) + { + return storeOp.getReference() == varOp.getResult(); + } + + auto loadOp = mlir::dyn_cast(user); + return loadOp && llvm::all_of(loadOp->getUsers(), [](mlir::Operation *loadUser) { + return mlir::isa(loadUser); + }); + }); + } + + mlir::Operation *OwnershipInferencePass::recordRetainOfStore(mlir::Operation *taker) + { + auto storeOp = mlir::dyn_cast(taker); + auto propertyRefOp = storeOp ? storeOp.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); + auto varOp = propertyRefOp ? propertyRefOp->getOperand(0).getDefiningOp() : mlir_ts::VariableOp(); + auto *retain = varOp ? recordRetainOf(varOp) : nullptr; + if (!retain || retain->getBlock() != taker->getBlock() || !taker->isBeforeInBlock(retain)) + { + return nullptr; + } + + return retain; + } + + bool OwnershipInferencePass::isImmortalLiteral(mlir::Value value) + { + if (isImmortalGlobalRead(value)) + { + return true; + } + + auto castOp = rootOf(value).getDefiningOp(); + return castOp && mlir::isa(castOp.getType()) && + castOp.getIn().getDefiningOp(); + } + + bool OwnershipInferencePass::isBorrow(mlir::Operation *user, mlir::Value value) + { + if (mlir::isa(user)) + { + return true; + } + + // `assert(c, message)` prints the message when it fails, and keeps it either way + if (mlir::isa(user)) + { + return true; + } + + // `s.value`'s place and `Shared.count(s)` read the handle and keep nothing (spec 23.1) + if (mlir::isa(user)) + { + return true; + } + + // `--di`'s record of a folded const's value for the debugger: names it, keeps nothing + if (mlir::isa(user)) + { + return true; + } + + // a test - `if (d)`, `d?.x`, `typeof u` - asks about the value, and keeps nothing + if (mlir::isa(user)) + { + return true; + } + + // ...and so does a cast to `null` or `undefined`, which holds no block: `--di` records the + // `r` that `if (r == null)` narrows as one + if (auto castOp = mlir::dyn_cast(user)) + { + return mlir::isa(castOp.getType()) || + castOp.getType().isInteger(1) || isBoundForCall(castOp); + } + + if (mlir::isa(user)) + { + return isBoundForCall(user); + } + + // arguments are borrowed, but one the callee keeps (`__own_params`) is taken; a callee + // with no facts that keeps one retains it, and that retain is its own error + if (isCall(user)) + { + return !isKeptArgument(user, value); + } + + // the array being pushed onto is written, not taken; what is inserted is taken + if (auto pushOp = mlir::dyn_cast(user)) + { + return pushOp.getOp() == value && !llvm::is_contained(pushOp.getItems(), value); + } + + if (auto unshiftOp = mlir::dyn_cast(user)) + { + return unshiftOp.getOp() == value && !llvm::is_contained(unshiftOp.getItems(), value); + } + + if (auto spliceOp = mlir::dyn_cast(user)) + { + return spliceOp.getOp() == value && !llvm::is_contained(spliceOp.getItems(), value); + } + + // a declaration that does not own what it holds only reads it + if (auto varOp = mlir::dyn_cast(user)) + { + return !isOwningVariable(varOp); + } + + // the box of a closure that borrows what it captures holds a copy that owns nothing; the + // closure's own uses are the borrow's (walkBorrowed, decideClosures) + if (mlir::isa(user) && user->hasAttr(OWN_CAPTURE_BORROW_ATTR_NAME)) + { + return true; + } + + // a store through a `ts.New`'d `value_ref` fills its block and keeps nothing of it + if (auto storeOp = mlir::dyn_cast(user)) + { + return mlir::isa(value.getType()) && storeOp.getReference() == value && + storeOp.getValue() != value; + } + + return false; + } + + bool OwnershipInferencePass::isBoundForCall(mlir::Operation *thisOp) + { + if (thisOp->getNumResults() != 1 || !mlir::isa(thisOp->getResult(0).getType()) || + thisOp->use_empty()) + { + return false; + } + + return llvm::all_of(thisOp->getUses(), [](mlir::OpOperand &use) { + auto boundOp = mlir::dyn_cast(use.getOwner()); + if (!boundOp || boundOp.getThisVal() != use.get()) + { + return false; + } + + return llvm::all_of(boundOp->getUsers(), [](mlir::Operation *boundUser) { + if (mlir::isa(boundUser)) + { + return true; + } + + return mlir::isa(boundUser) && + llvm::all_of(boundUser->getUsers(), [](mlir::Operation *thisUser) { return isCall(thisUser); }); + }); + }); + } + + bool OwnershipInferencePass::isKeptArgument(mlir::Operation *call, mlir::Value value) + { + auto args = callArgs(call); + return llvm::any_of(ownedParams(call), [&](int32_t index) { + return static_cast(index) < args.size() && args[index] == value; + }); + } + + bool OwnershipInferencePass::checkValueMoves(mlir::Value value, llvm::SetVector &toErase) + { + // Nothing to move: a number owns no block, and a string literal is the immortal global, + // which any number of places may hold. Under --opt, CSE merges identical literals before + // this pass, so one such value is routinely stored into several places. A read out of a + // container owns nothing either: checkPlaceRead decides it. Nor does other data nothing + // owns: an empty optional, a function, a tuple of literals (isConstantData). + if (!ownsHeap(value) || isImmortalLiteral(value) || isConstantData(value) || placeReadOf(value)) + { + return true; + } + + // A temporary's releases are one owner, one release on each way out. Two on one path are + // two owners: each view rc gave a reference of its own (`use(o); use(o)`, with `o` a + // folded object literal and `use` taking an interface: two `ts.NewInterface`s of one block). + if (!checkReleasedOnce(value)) + { + return false; + } + + if (quietly([&] { return movesOf(value, toErase); })) + { + return true; + } + + // Not a move. Every taker a `let` that can borrow, and the temporary's own release the + // owner that bounds them (spec 2.2, verdict 2) - else the move's error stands. + llvm::SmallVector takers; + llvm::SmallVector releases; + forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { + if (mlir::isa(user)) + { + return; + } + + if (mlir::isa(user)) + { + releases.push_back(user); + } + else if (!isBorrow(user, used)) + { + takers.push_back(user); + } + }); + + if (!releases.empty() && !takers.empty() && + llvm::all_of(takers, [](mlir::Operation *taker) { return !!borrowerOf(taker); })) + { + auto owner = ownerName(value); + auto ok = true; + for (auto *taker : takers) + { + ok = tryBorrow(borrowerOf(taker), releases, owner, toErase) && ok; + } + + return ok; + } + + return movesOf(value, toErase); + } + + bool OwnershipInferencePass::checkReleasedOnce(mlir::Value value) + { + llvm::SmallVector releases; + forEachUse(value, [&](mlir::Operation *user, mlir::Value) { + if (mlir::isa(user)) + { + releases.push_back(user); + } + }); + + auto *definition = value.getDefiningOp(); + for (auto *first : releases) + { + for (auto *second : releases) + { + if (first != second && reachableAfter(first, second, definition)) + { + // the view the second release gives back, where rc took its reference + auto *view = second->getOperand(0).getDefiningOp(); + reportSecondReference(view ? view : second, value); + return false; + } + } + } + + return true; + } + + bool OwnershipInferencePass::movesOf(mlir::Value value, llvm::SetVector &toErase) + { + llvm::SmallVector takers; + llvm::SmallVector releases; + llvm::SmallVector reads; + llvm::DenseMap readOf; + forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { + if (mlir::isa(user)) + { + return; + } + + if (mlir::isa(user)) + { + releases.push_back(user); + } + else if (isBorrow(user, used)) + { + reads.push_back(user); + readOf.try_emplace(user, used); + } + else + { + takers.push_back(user); + } + }); + + auto *definition = value.getDefiningOp(); + auto name = describe(value); + llvm::SmallVector moved; + for (auto *taker : takers) + { + if (takerLoops(taker, definition)) + { + reportMovedInLoop(taker, name); + return false; + } + + llvm::SmallVector made; + if (definition) + { + made.push_back(definition); + } + + // the call that keeps it, given it again through another operand + if (auto *again = callGivenAgain(taker, value, {value})) + { + reportUseAfterMove(again, taker, name); + return false; + } + + for (auto *use : llvm::concat(takers, reads)) + { + if (use != taker && reachableAfter(taker, use, definition)) + { + // a read through the new owner, while it holds the value (spec 29) + if (auto found = readOf.find(use); + found != readOf.end() && readsThroughOwner(taker, found->second, use, made)) + { + continue; + } + + reportUseAfterMove(use, taker, name); + return false; + } + } + + if (releases.empty()) + { + continue; // rc handed the taker the value's own reference: nothing to cancel + } + + if (!takerAcquires(taker, value)) + { + reportSecondReference(taker, value); + return false; + } + + auto *acquire = acquirePoint(taker, value); + for (auto *release : releases) + { + if (!reachableAfter(acquire, release, definition)) + { + continue; + } + + if (!dominance->properlyDominates(acquire, release)) + { + reportMovedOnSomePaths(taker, release, name); + return false; + } + + moved.push_back(release); + } + } + + toErase.insert(moved.begin(), moved.end()); + return true; + } + + mlir::Operation *OwnershipInferencePass::callGivenAgain(mlir::Operation *taker, mlir::Value moved, llvm::ArrayRef sources) + { + if (!isCall(taker) || ownedParams(taker).empty()) + { + return nullptr; + } + + auto args = callArgs(taker); + auto kept = ownedParams(taker); + auto movedRoot = rootOf(moved); + // one kept position is the move itself; the value at a second one (`f(n, n)` with both + // kept) is a second owner, a use after the first move like any other operand + auto skipped = false; + for (auto &operand : taker->getOpOperands()) + { + // the moved argument itself, where the callee keeps it + auto number = operand.getOperandNumber(); + if (!skipped && !args.empty() && number >= args.getBeginOperandIndex() && + number < args.getBeginOperandIndex() + args.size() && + llvm::is_contained(kept, static_cast(number - args.getBeginOperandIndex())) && + rootOf(operand.get()) == movedRoot) + { + skipped = true; + continue; + } + + for (auto source : sources) + { + if (derivedFrom(source).contains(operand.get())) + { + return taker; + } + } + } + + return nullptr; + } + + bool OwnershipInferencePass::takerLoops(mlir::Operation *taker, mlir::Operation *definition) + { + auto &body = getFunction().getFunctionBody(); + auto *top = body.findAncestorOpInRegion(*taker); + if (!top) + { + return true; + } + + auto *kill = definition ? body.findAncestorOpInRegion(*definition) : nullptr; + llvm::SmallPtrSet seen; + llvm::SmallVector work(top->getBlock()->getSuccessors().begin(), + top->getBlock()->getSuccessors().end()); + while (!work.empty()) + { + auto *block = work.pop_back_val(); + if (!seen.insert(block).second) + { + continue; + } + + if (kill && kill->getBlock() == block && + (block != top->getBlock() || kill->isBeforeInBlock(top))) + { + continue; // the value is made again before the taker runs again + } + + if (block == top->getBlock()) + { + return true; + } + + work.append(block->getSuccessors().begin(), block->getSuccessors().end()); + } + + return false; + } + + bool OwnershipInferencePass::reachableAfter(mlir::Operation *from, mlir::Operation *to, mlir::Operation *kill) + { + return reachableAfter(from, to, kill ? llvm::ArrayRef(kill) : llvm::ArrayRef()); + } + + bool OwnershipInferencePass::orderedInBody(mlir::Operation *from, mlir::Operation *to) + { + auto &body = getFunction().getFunctionBody(); + from = body.findAncestorOpInRegion(*from); + to = body.findAncestorOpInRegion(*to); + return from && to && from != to; + } + + bool OwnershipInferencePass::reachableAfter(mlir::Operation *from, mlir::Operation *to, llvm::ArrayRef kills) + { + if (llvm::is_contained(kills, to)) + { + return false; + } + + auto &body = getFunction().getFunctionBody(); + from = body.findAncestorOpInRegion(*from); + to = body.findAncestorOpInRegion(*to); + llvm::SmallVector killAt; + for (auto *kill : kills) + { + if (auto *top = body.findAncestorOpInRegion(*kill)) + { + killAt.push_back(top); + } + } + + if (!from || !to) + { + return true; + } + + if (from == to) + { + // Two distinct ops that share one ancestor statement: the order inside it is not + // known here, and "reachable" can only turn a program into an error. + return true; + } + + // is there a kill in `block` after `after` (from its start when null) and before `before` + // (to its end when null)? + auto killsIn = [&](mlir::Block *block, mlir::Operation *after, mlir::Operation *before) { + return llvm::any_of(killAt, [&](mlir::Operation *kill) { + return kill->getBlock() == block && (!after || after->isBeforeInBlock(kill)) && + (!before || kill->isBeforeInBlock(before)); + }); + }; + + auto *fromBlock = from->getBlock(); + auto *toBlock = to->getBlock(); + if (fromBlock == toBlock && from->isBeforeInBlock(to)) + { + return !killsIn(fromBlock, from, to); + } + + // every way out of this block runs a kill first + if (killsIn(fromBlock, from, nullptr)) + { + return false; + } + + llvm::SmallPtrSet seen; + llvm::SmallVector work(fromBlock->getSuccessors().begin(), fromBlock->getSuccessors().end()); + while (!work.empty()) + { + auto *block = work.pop_back_val(); + if (!seen.insert(block).second) + { + continue; + } + + if (block == toBlock && !killsIn(block, nullptr, to)) + { + return true; + } + + if (killsIn(block, nullptr, nullptr)) + { + continue; + } + + work.append(block->getSuccessors().begin(), block->getSuccessors().end()); + } + + return false; + } + + mlir::Operation *OwnershipInferencePass::takerOf(mlir::Operation *retain, mlir::Value value) + { + if (auto retainSlotOp = mlir::dyn_cast(retain)) + { + return retainSlotOp.getSlot().getDefiningOp(); + } + + // a call that keeps the argument: rc's retain for it is in the callee + if (isCall(retain)) + { + return retain; + } + + mlir::Operation *taker = nullptr; + auto several = false; + forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { + if (mlir::isa(user) || isBorrow(user, used)) + { + return; + } + + several = several || taker; + taker = user; + }); + + // rc retained this read and hands the receiver the next read of the same place + if (!taker && !several) + { + if (auto twin = twinReadAfter(retain, value)) + { + return takerOf(retain, twin); + } + } + + return several ? nullptr : taker; // two takers of one read: not a move this phase proves + } + + mlir::Operation *OwnershipInferencePass::soleUser(mlir::Value value) + { + mlir::Operation *found = nullptr; + for (auto *user : value.getUsers()) + { + if (mlir::isa(user)) + { + continue; + } + + if (found) + { + return nullptr; + } + + found = user; + } + + return found; + } + + mlir::Value OwnershipInferencePass::twinReadAfter(mlir::Operation *retain, mlir::Value value) + { + auto first = value.getDefiningOp(); + if (!first || !mlir::isa(retain) || soleUser(value) != retain || retain->getBlock() != first->getBlock()) + { + return {}; + } + + auto place = first.getReference(); + for (auto *op = retain->getNextNode(); op; op = op->getNextNode()) + { + if (auto loadOp = mlir::dyn_cast(op); loadOp && loadOp.getReference() == place) + { + return loadOp.getResult(); + } + + // anything else given the place may write through it - a store, a push - and a call may + // write anything + if (llvm::is_contained(op->getOperands(), place) || isCall(op)) + { + return {}; + } + } + + return {}; + } + + bool OwnershipInferencePass::checkSlotMove(mlir::Operation *retain, mlir::Value value, mlir_ts::LoadOp load, + llvm::SetVector &toErase, bool placeReleases) + { + auto slot = load.getReference(); + auto varOp = slot.getDefiningOp(); + auto name = varName(varOp); + + auto *taker = takerOf(retain, value); + if (!taker) + { + reportSecondReference(retain); + return false; + } + + auto assignments = slotAssignments(slot); + llvm::SmallVector kills(assignments.begin(), assignments.end()); + if (varOp) + { + kills.push_back(varOp); + } + + // the move's own read, met again around a loop the slot was declared outside of + if (reachableAfter(taker, load, kills)) + { + reportMovedInLoop(taker, name); + return false; + } + + // the slot given another value on some path between the move's read and its taker - inside + // the call's own argument list, `f(n, (n = mk(7), 0))`, on one arm of it, `f(n, c ? (n = + // mk(7), 0) : 0)`: the assignment's release frees what the read holds before the callee takes + // it, so the assignment is a use of the slot after the move. The taker may be an assignment + // of the slot itself (`n = n`): it is the destination, not between. A read and a taker + // reachableAfter cannot order (one statement kept as a region, or outside the body) refuse. + { + llvm::SmallVector readAgain{load.getOperation()}; + if (varOp) + { + readAgain.push_back(varOp); + } + + for (auto *assignment : assignments) + { + if (assignment != taker && reachableAfter(load, assignment, varOp) && + reachableAfter(assignment, taker, readAgain)) + { + reportUseAfterMove(assignment, taker, name); + return false; + } + } + + llvm::SmallVector killsBetween(kills); + llvm::erase(killsBetween, taker); + if (!orderedInBody(load, taker) || !reachableAfter(load, taker, killsBetween)) + { + reportUseAfterMove(taker, taker, name); + return false; + } + } + + // the call that keeps it, given the slot's value again through another operand: a read of + // the slot that reaches the call without crossing an assignment of it (not one made before + // an assignment gave the slot its current value, `const arr = [n]; n = mk(5); f(n, arr)`); + // since the move's own read reaches the call so too, such a read holds the moved value + llvm::SmallVector slotReads; + for (auto *user : slot.getUsers()) + { + if (auto readOp = mlir::dyn_cast(user); readOp && reachableAfter(readOp, taker, kills)) + { + slotReads.push_back(readOp.getResult()); + } + } + + if (auto *again = callGivenAgain(taker, value, slotReads)) + { + reportUseAfterMove(again, taker, name); + return false; + } + + // Every read of the slot is a use of `a`, and so is every use of what a read returned: a + // `const b = a` is folded into its load, so `b.x` after the move reads the moved value + // through a load that came before it. A read through the new owner, while it holds the + // value, is not such a use (spec 29). + auto throughOwner = [&](mlir::Operation *user, mlir::Value used) { + return readsThroughOwner(taker, used, user, kills); + }; + + for (auto *use : slot.getUsers()) + { + if (mlir::isa(use)) + { + continue; + } + + if (use != load.getOperation() && reachableAfter(taker, use, kills)) + { + auto readOp = mlir::dyn_cast(use); + if (!readOp || !onlyReadThroughOwner(readOp.getResult(), throughOwner)) + { + reportUseAfterMove(use, taker, name); + return false; + } + + continue; + } + + if (auto readOp = mlir::dyn_cast(use)) + { + if (auto *late = readUsedAfter(readOp, retain, taker, varOp, throughOwner)) + { + reportUseAfterMove(late, taker, name); + return false; + } + } + } + + auto *acquire = mlir::isa(retain) ? taker : retain; + llvm::SmallVector moved; + for (auto *use : slot.getUsers()) + { + if (!mlir::isa(use) || !reachableAfter(acquire, use, kills)) + { + continue; + } + + // a release that one path reaches holding the moved value and another holding a new + // one an assignment after the move stored: there is no single answer for it + auto someMoved = !dominance->properlyDominates(acquire, use); + for (auto *assignment : assignments) + { + llvm::SmallVector movedAgain{load.getOperation()}; + if (varOp) + { + movedAgain.push_back(varOp); + } + + someMoved = someMoved || + (reachableAfter(acquire, assignment, varOp) && reachableAfter(assignment, use, movedAgain)); + } + + if (someMoved) + { + // ...but the paths that did not move it can release it before they join the + // others (spec 30) + if (placeReleases && placeOnUnmovedEdges(varOp, acquire, retain, taker, assignments, toErase)) + { + return true; + } + + reportMovedOnSomePaths(taker, use, name, placeReleases ? joinedUse : nullptr); + return false; + } + + moved.push_back(use); + } + + toErase.insert(moved.begin(), moved.end()); + return true; + } + +} // namespace own_inference diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.cpp b/tslang/lib/TypeScript/OwnershipInferencePass.cpp index 0271ab6e5..b08000cd3 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePass.cpp @@ -1,72 +1,14 @@ -#include "mlir/Pass/Pass.h" -#include "mlir/IR/Dominance.h" -#include "mlir/Dialect/LLVMIR/LLVMAttrs.h" -#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h" -#include "mlir/Interfaces/ControlFlowInterfaces.h" -#include "mlir/Interfaces/FunctionInterfaces.h" +// OwnershipInferencePass (see OwnershipInferencePass.h): the pass itself - each function analyzed, +// its retains decided and erased, string copies made. -#include "TypeScript/TypeScriptDialect.h" -#include "TypeScript/TypeScriptOps.h" -#include "TypeScript/TypeScriptFunctionPass.h" -#include "TypeScript/Passes.h" -#include "TypeScript/Defines.h" -#include "TypeScript/MLIRLogic/MLIRTypeHelper.h" - -#include "OwnershipFacts.h" - -#include "llvm/ADT/MapVector.h" -#include "llvm/ADT/ScopeExit.h" -#include "llvm/ADT/SetVector.h" -#include "llvm/ADT/SmallPtrSet.h" -#include "llvm/ADT/SmallVector.h" -#include "llvm/ADT/STLFunctionalExtras.h" -#include "llvm/ADT/StringSet.h" -#include "llvm/Support/raw_ostream.h" - -#include -#include +#include "OwnershipInferencePass.h" #define DEBUG_TYPE "own" -namespace mlir_ts = mlir::typescript; - -namespace -{ - -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" - -// 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. -// -// A fresh heap value may have at most one owner: one use that takes it - a local's -// declaration, a store into a field, element, global or return slot, an insertion into an -// array, a return - or, with none of those, the temporary itself, which rc gives back with a -// `ts.Release`. That owner's release is then its destroy, and every retain rc planted on the -// way is erased. Owners are counted from the uses themselves, not from rc's retains: rc also -// transfers ownership by *consuming* a value (`__owned_consumed`, a push, a field store), -// which leaves no retain behind to count. -// -// A move is decided by reachability on the affine CFG (spec 4.2 step 3): nothing may use the value -// after its taker, a taker that control comes back to without the value being made again is a -// move on every loop iteration (2.6), and each release the move reaches must be dominated by it -// and is erased. A release the move cannot reach stays - the owner on the paths that did not -// move. A local's release that paths which moved it and paths which did not both reach is placed -// instead on the edges where the latter join the former (spec 30). Anything else is an error at -// its location. -// -// It runs on every function: a `ts.Func`, and the `func.func` the async lowering outlines an -// `await`'s continuation or a `for await` body into, whose arguments are the values it captures. -class OwnershipInferencePass - : public mlir::PassWrapper> +namespace own_inference { - public: - MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(OwnershipInferencePass) - void runOnOperation() override + void OwnershipInferencePass::runOnOperation() { if (!getFunction().isExternal()) { @@ -74,12 +16,12 @@ class OwnershipInferencePass } } - mlir::FunctionOpInterface getFunction() + mlir::FunctionOpInterface OwnershipInferencePass::getFunction() { return getOperation(); } - void runOnFunction() + void OwnershipInferencePass::runOnFunction() { // the dry run (spec 22.3): does today's analysis pass as it is? { @@ -109,9 +51,7 @@ class OwnershipInferencePass analyze(toErase); } - // The analysis of one function; true when it passed. In a dry run it erases nothing and strips - // no facts. - bool analyze(llvm::SetVector &toErase) + bool OwnershipInferencePass::analyze(llvm::SetVector &toErase) { auto f = getFunction(); mlir::DominanceInfo dominanceInfo(f); @@ -119,31 +59,7 @@ class OwnershipInferencePass // it is this call's: nothing after it (the copies, the next run) may read it auto dominanceGone = llvm::make_scope_exit([&]() { dominance = nullptr; }); - // every value some ownership operation names, in the order the walk meets them - llvm::SetVector candidates; - llvm::SmallVector retains; - // reads out of fields and elements, and results that borrow an argument, and what may - // destroy what they read (spec 2.3, 2.4) - llvm::SmallVector placeReads; - // reads of what a mortal global holds (isGlobalRead) - llvm::SmallVector globalReads; - // calls whose callee keeps an argument: each such argument moves into the call - llvm::SmallVector keepingCalls; - drops.clear(); - edgeReleases.clear(); - derivedCache.clear(); - closures.clear(); - closureOf.clear(); - stateBoxes.clear(); - fieldBorrowRetains.clear(); - fieldBorrowReleases.clear(); - captureStores.clear(); - captureRetains.clear(); - returnedParams.clear(); - returnsBorrowOf = resultBorrows(f); - returnsRecordOf = isRecordResult(f) ? resultBorrowsAll(f) : llvm::SmallVector(); - records.clear(); - recordStoreUses.clear(); + resetFunctionState(); // first: whether a closure borrows what it captures changes what its box's stores are classifyClosures(); @@ -167,6 +83,73 @@ class OwnershipInferencePass exitReleases.clear(); }); + FunctionOps ops; + collectOwnershipOps(ops, toErase); + auto proven = checkOwnershipOps(ops, toErase); + + Receivers slotReceivers; + Receivers paramReceivers; + decideRetains(ops, proven, slotReceivers, paramReceivers, toErase); + giveKeptArguments(ops, slotReceivers, paramReceivers); + + for (auto &entry : slotReceivers) + { + decideSlot(entry.first, entry.second, toErase); + } + + for (auto &entry : paramReceivers) + { + decideParam(entry.first, entry.second, toErase); + } + + decideClosures(toErase); + decideCells(); + checkBorrowedReads(); + checkBoundedResults(); + checkBorrowingRecords(); + checkRecordResults(); + toErase.insert(fieldBorrowReleases.begin(), fieldBorrowReleases.end()); + + if (dryRun) + { + return failures == 0; + } + + // a handle's retains and releases are rc's, and stay (spec 23.2) + toErase.remove_if([](mlir::Operation *op) { return touchesHandle(op); }); + placeEdgeReleases(); + for (auto *op : toErase) + { + op->erase(); + } + + stripFacts(); + return true; + } + + void OwnershipInferencePass::resetFunctionState() + { + auto f = getFunction(); + drops.clear(); + edgeReleases.clear(); + derivedCache.clear(); + closures.clear(); + closureOf.clear(); + stateBoxes.clear(); + fieldBorrowRetains.clear(); + fieldBorrowReleases.clear(); + captureStores.clear(); + captureRetains.clear(); + returnedParams.clear(); + returnsBorrowOf = resultBorrows(f); + returnsRecordOf = isRecordResult(f) ? resultBorrowsAll(f) : llvm::SmallVector(); + records.clear(); + recordStoreUses.clear(); + } + + void OwnershipInferencePass::collectOwnershipOps(FunctionOps &ops, llvm::SetVector &toErase) + { + auto f = getFunction(); // A view of a block is that block: the candidate is always the root. f.walk([&](mlir::Operation *op) { // A handle put into a union with a member that owns a block of its own: counting the @@ -184,22 +167,22 @@ class OwnershipInferencePass if (auto retainOp = mlir::dyn_cast(op)) { - retains.push_back(op); - candidates.insert(rootOf(retainOp.getReference())); + ops.retains.push_back(op); + ops.candidates.insert(rootOf(retainOp.getReference())); } else if (auto retainSlotOp = mlir::dyn_cast(op)) { - retains.push_back(op); + ops.retains.push_back(op); } else if (auto releaseOp = mlir::dyn_cast(op)) { - candidates.insert(rootOf(releaseOp.getReference())); + ops.candidates.insert(rootOf(releaseOp.getReference())); } else if (auto varOp = mlir::dyn_cast(op)) { if (varOp.getInitializer() && isOwningVariable(varOp) && !isHandle(varOp.getInitializer())) { - candidates.insert(rootOf(varOp.getInitializer())); + ops.candidates.insert(rootOf(varOp.getInitializer())); } } else if (mlir::isa(op)) @@ -225,11 +208,11 @@ class OwnershipInferencePass // the value out: fresh, not a borrow of the state if (placeReadOf(loadOp.getResult()) && !loadOp->hasAttr(OWN_FRESH_RESULT_ATTR_NAME)) { - placeReads.push_back(loadOp); + ops.placeReads.push_back(loadOp); } else if (isGlobalRead(loadOp)) { - globalReads.push_back(loadOp); + ops.globalReads.push_back(loadOp); } } else if (auto releaseSlotOp = mlir::dyn_cast(op); releaseSlotOp && isYieldedField(releaseSlotOp.getSlot())) @@ -263,12 +246,12 @@ class OwnershipInferencePass drops.push_back(op); if (op->getNumResults() == 1 && placeReadOf(op->getResult(0)) == op) { - placeReads.push_back(op); + ops.placeReads.push_back(op); } if (!ownedParams(op).empty()) { - keepingCalls.push_back(op); + ops.keepingCalls.push_back(op); } } @@ -276,16 +259,19 @@ class OwnershipInferencePass { if (!isView(op) && isFresh(result) && ownsHeap(result) && !isHandle(result)) { - candidates.insert(result); + ops.candidates.insert(result); } } }); + } + llvm::DenseSet OwnershipInferencePass::checkOwnershipOps(const FunctionOps &ops, llvm::SetVector &toErase) + { // Every value gets the owner check, fresh or not: rc can hand a value it did not make to // two consuming declarations without a single retain (the result of an indirect call // taken by `let b = a; let c = a;`). Only a fresh value's retains can be erased. llvm::DenseSet proven; - for (auto value : candidates) + for (auto value : ops.candidates) { if (checkValueMoves(value, toErase) && isFresh(value)) { @@ -293,26 +279,30 @@ class OwnershipInferencePass } } - for (auto read : placeReads) + for (auto read : ops.placeReads) { checkPlaceRead(read, toErase); } - for (auto read : globalReads) + for (auto read : ops.globalReads) { checkGlobalRead(read); } checkArgumentsAssigned(); + return proven; + } + + void OwnershipInferencePass::decideRetains(const FunctionOps &ops, const llvm::DenseSet &proven, Receivers &slotReceivers, + Receivers ¶mReceivers, llvm::SetVector &toErase) + { // What is proven loses its retains; any other retain is a second reference nobody // proved safe - of a loaded value, a parameter, an unknown producer, or of a fresh value // whose own check was reported above. // Receivers of each owning local are decided together: one may move it, but where there // are several, each borrows it, since a borrow pins its owner. - llvm::MapVector> slotReceivers; - llvm::MapVector> paramReceivers; - for (auto *op : retains) + for (auto *op : ops.retains) { if (touchesHandle(op)) { @@ -421,12 +411,15 @@ class OwnershipInferencePass reportSecondReference(op); } } + } + void OwnershipInferencePass::giveKeptArguments(const FunctionOps &ops, Receivers &slotReceivers, Receivers ¶mReceivers) + { // An argument a callee keeps moves into the call (spec 2.4): out of an owning local or a // parameter this function keeps, like any other taker; a fresh value is decided with the // other takers of it, a read out of a container by its own check. Anything else is not // this function's to give. - for (auto *call : keepingCalls) + for (auto *call : ops.keepingCalls) { auto args = callArgs(call); for (auto index : ownedParams(call)) @@ -454,41 +447,12 @@ class OwnershipInferencePass } } } + } - for (auto &entry : slotReceivers) - { - decideSlot(entry.first, entry.second, toErase); - } - - for (auto &entry : paramReceivers) - { - decideParam(entry.first, entry.second, toErase); - } - - decideClosures(toErase); - decideCells(); - checkBorrowedReads(); - checkBoundedResults(); - checkBorrowingRecords(); - checkRecordResults(); - toErase.insert(fieldBorrowReleases.begin(), fieldBorrowReleases.end()); - - if (dryRun) - { - return failures == 0; - } - - // a handle's retains and releases are rc's, and stay (spec 23.2) - toErase.remove_if([](mlir::Operation *op) { return touchesHandle(op); }); - placeEdgeReleases(); - for (auto *op : toErase) - { - op->erase(); - } - - // the facts were for this pass only + void OwnershipInferencePass::stripFacts() + { returnedParams.clear(); - f.walk([](mlir::Operation *op) { + getFunction().walk([](mlir::Operation *op) { for (auto *name : {OWN_PARAMS_ATTR_NAME, OWN_PARAMS_RELEASED_ATTR_NAME, OWN_RESULT_BORROWS_ATTR_NAME, OWN_RESULT_RECORD_ATTR_NAME, OWN_NO_DROPS_ATTR_NAME, OWN_FACTS_LOST_ATTR_NAME, OWN_FRESH_RESULT_ATTR_NAME, @@ -499,34 +463,9 @@ class OwnershipInferencePass op->removeAttr(name); } }); - - return true; } - private: - mlir::DominanceInfo *dominance = nullptr; - - // What in this function may destroy a value held by a field or an element: an overwrite of a - // field or an element, an array op that removes elements, a call. - llvm::SmallVector drops; - - // derivedFrom's answer for each value it was asked about in this function; held by pointer, so - // an answer stays put while the next one is added - llvm::DenseMap>> derivedCache; - - // While non-zero, verdicts are tried without reporting: the next verdict gets its turn first. - unsigned quiet = 0; - - // The dry run (spec 22.3): the whole analysis, but nothing reported and nothing changed. A - // report it would make is counted in `failures`. - bool dryRun = false; - unsigned failures = 0; - // attributes the dry run set on ops that did not have them, taken off again after it - llvm::SmallVector> dryRunAttrs; - - // Whether a report is to be emitted: not under `quietly`'s trial, and not in the dry run, which - // only counts it. - bool reporting() + bool OwnershipInferencePass::reporting() { if (quiet) { @@ -542,8 +481,7 @@ class OwnershipInferencePass return true; } - // `op->setAttr(name, unit)`, undone after the dry run when the op did not have it - void setAttrTracked(mlir::Operation *op, llvm::StringRef name) + void OwnershipInferencePass::setAttrTracked(mlir::Operation *op, llvm::StringRef name) { auto attrName = mlir::StringAttr::get(&getContext(), name); if (dryRun && !op->hasAttr(attrName)) @@ -554,9 +492,7 @@ class OwnershipInferencePass op->setAttr(attrName, mlir::UnitAttr::get(&getContext())); } - // Strings as values (spec 22.3): each string retain the dry run did not erase is rewritten, where - // its one use can be found, into a copy for that use. - void makeStringCopies(const llvm::SetVector &erased) + void OwnershipInferencePass::makeStringCopies(const llvm::SetVector &erased) { llvm::SmallVector retains; getFunction()->walk([&](mlir::Operation *op) { @@ -573,20 +509,13 @@ class OwnershipInferencePass } } - // The value a parameter of this function is, when its callers were told this function keeps it - // or returns a borrow of it (spec 15): copying it would leak what the caller gave up. - bool borrowsKnownParam(mlir::Value value) + bool OwnershipInferencePass::borrowsKnownParam(mlir::Value value) { auto param = borrowedParam(value); return param >= 0 && (llvm::is_contained(ownedParams(getFunction()), param) || param == returnsBorrowOf); } - // Rewrites one string retain into a copy for the use it was made for (spec 22.3), and erases it; - // false, leaving it for the real run to report, where there is no single such use in its block, - // or the value is not a string or is a known parameter's (spec 22.4). Its use walk is takerOf's - // without the twinReadAfter fallback: a retain whose value has no use of its own is not copied, - // so its error stays. - bool copyForRetain(mlir::Operation *retain) + bool OwnershipInferencePass::copyForRetain(mlir::Operation *retain) { // `let x = v`: the local starts from a copy if (auto retainSlotOp = mlir::dyn_cast(retain)) @@ -666,6700 +595,11 @@ class OwnershipInferencePass return true; } - // The parameter this function's result borrows (`__own_result_borrows`), or -1; and the - // parameters (or views of them) whose rc reference for the caller was already decided. - int returnsBorrowOf = -1; - llvm::DenseSet returnedParams; - - // The parameters this function's result is a borrowing record of (OWN_RESULT_RECORD_ATTR_NAME, - // spec 32); empty for none. - llvm::SmallVector returnsRecordOf; - - // ---- Closures (spec 2.5, phase 5) ---- - // - // A closure is `ts.CreateBoundFunction(box, @f) {__owns_capture}`; its box holds a cell for each - // variable it captures by reference and a copy of each it captures by value. One that does not - // escape borrows all of it: rc's references for the box go, the box frees itself only - // (OWN_BORROWS_CAPTURES_ATTR_NAME), and nothing that may run the closure may come after anything - // it borrows is given back. One that escapes would own all of it, which is not done yet. - struct Closure - { - mlir_ts::CreateBoundFunctionOp op; - mlir::Value box; - // each store that fills the box, with the field it fills - llvm::SmallVector> fills; - bool hasCells = false; - // the first use that takes the closure out of this function's hands - mlir::Operation *escape = nullptr; - // what may run the closure: calls given it, called through it, or through its box - llvm::SmallVector uses; - // the calls through the box itself, made without the closure value (a folded `const f`) - llvm::SmallVector boxUses; - // where the closure value is given back: its releases, and those of the `let`s holding it - llvm::SmallVector ends; - // rc's references for the box: `ts.RetainCell` of each cell, `ts.Retain` of each copy - llvm::SmallVector retains; - // held by a local that owns nothing (`let f: () => number; f = () => ...`): an alias, so - // the closure's own releases stay its owner - bool aliased = false; - // a generator's box whose state object borrows from the generator's arguments - // (OWN_BORROWING_FIELDS_ATTR_NAME): a parameter's cell moves in, and the box frees the cell only - bool borrowsValues = false; - // copies only, and given to a generator's maker that borrows from it (`.map(f)`): the box - // borrows its copies, as a box with cells would - bool borrowsCopies = false; - }; - - llvm::SmallVector> closures; - // a copy stored into a borrowing box -> its closure - llvm::DenseMap captureStores; - // rc's references for boxes, which the closures decide - llvm::DenseSet captureRetains; - // each closure over a cell, by its `ts.CreateBoundFunction` - llvm::DenseMap closureOf; - // each generator's capture box, which escapes with its state object (classifyStateBoxes) - llvm::DenseMap stateBoxes; - // rc's retains for stores into borrowing fields, and those fields' releases (classifyBorrowingFields) - llvm::DenseSet fieldBorrowRetains; - llvm::SmallVector fieldBorrowReleases; - - // Finds the closures over cells and what they borrow, before anything else: whether a box's - // stores are reads or takers depends on it. A closure with copies only keeps what phase 4 does - // with it - its box owns them, and each copy moves in. - void classifyClosures() - { - getFunction().walk([&](mlir_ts::CreateBoundFunctionOp boundOp) { - if (!boundOp->hasAttr(OWNS_CAPTURE_ATTR_NAME) || closureOfBox(boundOp.getThisVal()) != boundOp) - { - return; - } - - auto closure = std::make_shared(); - closure->op = boundOp; - closure->box = boundOp.getThisVal(); - for (auto *user : closure->box.getUsers()) - { - if (user == boundOp.getOperation()) - { - continue; - } - - if (auto propertyRefOp = mlir::dyn_cast(user)) - { - for (auto *fieldUser : propertyRefOp->getUsers()) - { - auto storeOp = mlir::dyn_cast(fieldUser); - if (!storeOp || storeOp.getReference() != propertyRefOp.getResult()) - { - closure->escape = closure->escape ? closure->escape : fieldUser; - continue; - } - - closure->fills.push_back({propertyRefOp.getPosition(), storeOp}); - closure->hasCells = closure->hasCells || mlir::isa(storeOp.getValue().getType()); - } - - continue; - } - - if (isCall(user)) - { - closure->uses.push_back(user); - closure->boxUses.push_back(user); - continue; - } - - closure->escape = closure->escape ? closure->escape : user; - } - - if (auto *escape = followClosure(boundOp.getResult(), closure->uses, closure->ends, closure->aliased); - escape && !closure->escape) - { - closure->escape = escape; - } - - // copies only: the box owns them (phase 4); only an alias needs deciding - unless the box - // is given to a generator's maker whose result borrows from it - if (!closure->hasCells) - { - // ...and only when every copy has an owner of its own: the box frees itself alone, so a - // value made for it (a callback that captures, `a.filter((x) => x > n)`) would be - // nobody's - closure->borrowsCopies = - !closure->escape && - llvm::any_of(closure->boxUses, - [](mlir::Operation *use) { return use->hasAttr(OWN_RESULT_BOUNDED_ATTR_NAME); }) && - llvm::none_of(closure->fills, [&](auto &fill) { - auto value = fill.second.getValue(); - return !holdsNoBlock(value) && isFresh(rootOf(value)); - }); - if (!closure->borrowsCopies) - { - if (!closure->escape && closure->aliased) - { - closures.push_back(std::move(closure)); - } - - return; - } - } - - for (auto &fill : closure->fills) - { - auto storeOp = fill.second; - auto isCell = mlir::isa(storeOp.getValue().getType()); - // an escaping closure's copies move into its box as in phase 4, rc's retain with them - if (closure->escape && !isCell) - { - continue; - } - - if (auto *retain = retainBefore(storeOp.getValue(), storeOp)) - { - closure->retains.push_back(retain); - captureRetains.insert(retain); - } - - if (!closure->escape && !isCell) - { - setAttrTracked(storeOp, OWN_CAPTURE_BORROW_ATTR_NAME); - captureStores[storeOp] = closure.get(); - } - } - - if (!closure->escape) - { - setAttrTracked(boundOp, OWN_BORROWS_CAPTURES_ATTR_NAME); - } - - closureOf[boundOp.getOperation()] = closure.get(); - closures.push_back(std::move(closure)); - }); - } - - // The fields of a generator's state object of this type that borrow from its arguments - // (OWN_BORROWING_FIELDS_ATTR_NAME, the signature pass's), the `.captured` box's first. - llvm::SmallVector borrowingFieldsOf(mlir::Type recordType) - { - llvm::SmallVector fields; - if (auto objectType = mlir::dyn_cast(recordType)) - { - recordType = objectType.getStorageType(); - } - - if (auto storageType = mlir::dyn_cast(recordType)) - { - recordType = mlir_ts::TupleType::get(&getContext(), storageType.getFields()); - } - - auto module = getFunction()->getParentOfType(); - auto entries = module ? module->getAttrOfType(OWN_BORROWING_FIELDS_ATTR_NAME) : mlir::ArrayAttr(); - if (!entries) - { - return fields; - } - - for (auto entry : entries.getAsRange()) - { - if (mlir::cast(entry[0]).getValue() == recordType) - { - auto indices = mlir::cast(entry[1]).asArrayRef(); - fields.append(indices.begin(), indices.end()); - } - } - - return fields; - } - - // Is `ref` a field of a generator's state object that only yields (OWN_YIELDED_FIELDS_ATTR_NAME)? - bool isYieldedField(mlir::Value ref) - { - auto propertyRefOp = ref.getDefiningOp(); - if (!propertyRefOp) - { - return false; - } - - auto recordType = propertyRefOp->getOperand(0).getType(); - if (auto objectType = mlir::dyn_cast(recordType)) - { - recordType = objectType.getStorageType(); - } - - if (auto storageType = mlir::dyn_cast(recordType)) - { - recordType = mlir_ts::TupleType::get(&getContext(), storageType.getFields()); - } - - auto module = getFunction()->getParentOfType(); - auto entries = module ? module->getAttrOfType(OWN_YIELDED_FIELDS_ATTR_NAME) : mlir::ArrayAttr(); - if (!entries) - { - return false; - } - - for (auto entry : entries.getAsRange()) - { - if (mlir::cast(entry[0]).getValue() == recordType && - llvm::is_contained(mlir::cast(entry[1]).asArrayRef(), - static_cast(propertyRefOp.getPosition()))) - { - return true; - } - } - - return false; - } - - // Is `ref` a field of a generator's state object that borrows (other than the `.captured` box)? - bool isBorrowingField(mlir::Value ref) - { - auto propertyRefOp = ref.getDefiningOp(); - if (!propertyRefOp) - { - return false; - } - - auto fields = borrowingFieldsOf(propertyRefOp->getOperand(0).getType()); - return fields.size() > 1 && llvm::is_contained(llvm::ArrayRef(fields).drop_front(), - static_cast(propertyRefOp.getPosition())); - } - - // A store into a borrowing field holds what it stores without owning it, as a borrowing box's - // 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() - { - getFunction().walk([&](mlir::Operation *op) { - if (auto storeOp = mlir::dyn_cast(op); storeOp && isBorrowingField(storeOp.getReference())) - { - setAttrTracked(storeOp, OWN_CAPTURE_BORROW_ATTR_NAME); - if (auto *retain = retainBefore(storeOp.getValue(), storeOp)) - { - fieldBorrowRetains.insert(retain); - } - } - else if (auto releaseSlotOp = mlir::dyn_cast(op); - releaseSlotOp && isBorrowingField(releaseSlotOp.getSlot())) - { - fieldBorrowReleases.push_back(op); - } - }); - } - - // 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))`? - bool isBorrowingCell(mlir::Value ref) - { - auto cellLoad = ref.getDefiningOp(); - return cellLoad && isBorrowingBoxField(cellLoad.getReference()); - } - - // Is `ref` a field of the `.captured` box of a generator's state object that borrows: - // `ts.PropertyRef(ts.Load(ts.PropertyRef(state, captured)), i)`? A field copied in by value holds - // the caller's value itself, a cell field the caller's cell. - bool isBorrowingBoxField(mlir::Value ref) - { - auto cellRef = ref.getDefiningOp(); - auto boxLoad = cellRef ? cellRef->getOperand(0).getDefiningOp() : mlir_ts::LoadOp(); - auto boxRef = boxLoad ? boxLoad.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); - if (!boxRef) - { - return false; - } - - auto fields = borrowingFieldsOf(boxRef->getOperand(0).getType()); - return !fields.empty() && fields.front() == static_cast(boxRef.getPosition()); - } - - // What a generator reads out of a borrowing cell or field is the caller's: every use of it must - // be a borrow - a store into another borrowing field is one (classifyBorrowingFields). A yield of - // it, a push, a return, a store anywhere else would give the caller's value a second owner, and - // rc retains nothing for some of these, so nothing else would see it. - void checkBorrowedReads() - { - getFunction().walk([&](mlir_ts::LoadOp loadOp) { - auto ref = loadOp.getReference(); - if (!ownsHeap(loadOp.getResult()) || !(isBorrowingCell(ref) || isBorrowingBoxField(ref) || isBorrowingField(ref))) - { - return; - } - - auto reported = false; - forEachUse(loadOp.getResult(), [&](mlir::Operation *user, mlir::Value used) { - if (reported || isBorrow(user, used) || fieldBorrowRetains.contains(user)) - { - return; - } - - reportPlaceBorrowEscapes(user, ownerName(loadOp.getResult()), "the generator's argument"); - reported = true; - }); - }); - } - - // A call whose result borrows from its arguments (OWN_RESULT_BOUNDED_ATTR_NAME): a generator over - // a parameter's block. The result is owned as any fresh value is, but nothing may use it after - // one of those arguments is given back - released, assigned, or dropped from the place it was - // read from - and it may not be kept anywhere that could outlive them. Its releases are not - // uses: the state object's release gives back nothing it borrows. - void checkBoundedResults() - { - getFunction().walk([&](mlir::Operation *call) { - auto bounded = call->getAttrOfType(OWN_RESULT_BOUNDED_ATTR_NAME); - if (!bounded || !isCall(call) || call->getNumResults() != 1) - { - return; - } - - auto name = ownerName(call->getResult(0)); - llvm::SmallVector uses; - if (auto *kept = boundedUses(call->getResult(0), uses)) - { - reportPlaceBorrowEscapes(kept, name, "its generator's arguments"); - return; - } - - auto args = callArgs(call); - for (auto index : bounded.asArrayRef()) - { - if (static_cast(index) >= args.size()) - { - continue; - } - - // a closure's box (`.map(f)` calls its maker with it): it lives while the closures on - // it do, and what it holds while their owners do. A reference to storage, so first. - auto root = rootOf(args[index]); - if (auto boxOp = root.getDefiningOp(); boxOp && boxOp->hasAttr(CAPTURE_BOX_ATTR_NAME)) - { - if (!checkBoundedByBox(call, root, uses, name)) - { - return; - } - - continue; - } - - if (holdsNoBlock(args[index]) || isImmortalLiteral(args[index])) - { - continue; - } - - if (borrowedParam(root) >= 0) - { - continue; // the caller's caller's: alive for all of this function - } - - Chain chain; - auto *read = placeReadOf(root); - if (read) - { - chain = chainOf(read); - } - else if (auto slotLoad = slotLoadOf(root)) - { - chain.roots.push_back({slotLoad.getReference(), Chain::Slot}); - } - else if (isFresh(root)) - { - chain.roots.push_back({root, Chain::Owned}); - } - else - { - chain.roots.push_back({root, Chain::NotOwned}); - } - - // the root's own ends; and, for a read out of a place, whatever may destroy what the - // place holds (a local or a made value is not destroyed by a call it is given) - auto ends = rootEnds(chain); - for (auto *drop : read ? llvm::ArrayRef(drops) : llvm::ArrayRef()) - { - if (drop != call && dropsChain(drop, chain, read)) - { - ends.push_back(drop); - } - } - - auto owner = ownerName(root); - for (auto *end : ends) - { - if (end == call) - { - continue; - } - - for (auto *use : uses) - { - if (use != end && reachableAfter(end, use, call)) - { - reportBorrowOutlives(use, end, name, owner); - return; - } - } - } - } - }); - } - - // The ends of a capture box given to a call with a bounded result: each release of a closure on - // it (the box goes with its last), and each end of what a field of it was filled from. Reports - // and returns false when a use of the result comes after one. - bool checkBoundedByBox(mlir::Operation *call, mlir::Value box, llvm::ArrayRef uses, - llvm::StringRef name) - { - llvm::SmallVector ends; - for (auto *user : box.getUsers()) - { - if (auto boundOp = mlir::dyn_cast(user); boundOp && boundOp.getThisVal() == box) - { - llvm::SmallVector closureUses; - auto aliased = false; - if (followClosure(boundOp.getResult(), closureUses, ends, aliased)) - { - reportPlaceBorrowEscapes(call, name, "a closure that escapes"); // the box may go anywhere - return false; - } - - continue; - } - - auto propertyRefOp = mlir::dyn_cast(user); - if (!propertyRefOp) - { - continue; - } - - for (auto *fieldUser : propertyRefOp->getUsers()) - { - auto storeOp = mlir::dyn_cast(fieldUser); - if (!storeOp || storeOp.getReference() != propertyRefOp.getResult() || holdsNoBlock(storeOp.getValue())) - { - continue; - } - - auto root = rootOf(storeOp.getValue()); - Chain chain; - if (auto slotLoad = slotLoadOf(root)) - { - chain.roots.push_back({slotLoad.getReference(), Chain::Slot}); - } - else if (borrowedParam(root) >= 0 || isImmortalLiteral(root)) - { - continue; - } - else if (isFresh(root)) - { - chain.roots.push_back({root, Chain::Owned}); - } - else - { - reportPlaceBorrowEscapes(call, name, "a value this function cannot bound"); - return false; - } - - auto rootEndsOf = rootEnds(chain); - ends.append(rootEndsOf.begin(), rootEndsOf.end()); - } - } - - for (auto *end : ends) - { - for (auto *use : uses) - { - if (end != call && use != end && reachableAfter(end, use, call)) - { - reportBorrowOutlives(use, end, name, "the generator's source"); - return false; - } - } - } - - return true; - } - - // Every use of a bounded result, through its views, the locals that hold it, and the function - // and object a call through it is made with. Answers the first use that keeps it - anything - // that is not a read, a call, a local, or rc's bookkeeping - or null. - mlir::Operation *boundedUses(mlir::Value result, llvm::SmallVectorImpl &uses) - { - llvm::SmallVector values{result}; - llvm::SmallPtrSet seen; - mlir::Operation *kept = nullptr; - while (!values.empty() && !kept) - { - auto value = values.pop_back_val(); - if (!seen.insert(value).second) - { - continue; - } - - forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { - if (kept || mlir::isa(user)) - { - return; - } - - mlir::Value slot; - if (auto varOp = mlir::dyn_cast(user)) - { - slot = varOp.getResult(); - } - else if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getValue() == used) - { - slot = storeOp.getReference(); - } - - if (slot) - { - auto slotVar = slot.getDefiningOp(); - if (!slotVar || slotVar.getCaptured().value_or(false)) - { - kept = user; - return; - } - - for (auto *slotUser : slot.getUsers()) - { - if (auto loadOp = mlir::dyn_cast(slotUser)) - { - values.push_back(loadOp.getResult()); - } - } - - return; - } - - // returned by a function that says so (`@borrows`): its callers hold the result to - // the same bound, and the signature pass checked the arguments are its own - if (mlir::isa(user) && getFunction()->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME)) - { - return; - } - - uses.push_back(user); - if (isCall(user)) - { - if (isKeptArgument(user, used)) - { - kept = user; - } - - return; - } - - // `it.next()`: the field read, the bound function, its `this` - if (mlir::isa(user)) - { - for (auto partResult : user->getResults()) - { - values.push_back(partResult); - } - - return; - } - - if (!isBorrow(user, used)) - { - kept = user; - } - }); - } - - return kept; - } - - // A generator's maker puts what the generator captures - its parameters, an outer local - into - // a box, and the box into its state object's `.captured` field: the box of a closure that - // escapes, with the state object, at that store. There is no closure value, so no use runs it - // here and nothing calls through it. rc's `ts.RetainCell` of the box is the state object's - // reference, which moves in with it. A box used any other way is left to the RetainCell error. - void classifyStateBoxes() - { - getFunction().walk([&](mlir_ts::VariableOp boxOp) { - auto box = boxOp.getResult(); - if (!boxOp->hasAttr(CAPTURE_BOX_ATTR_NAME) || closureOfBox(box)) - { - return; - } - - auto closure = std::make_shared(); - closure->box = box; - llvm::SmallVector boxRetains; - for (auto *user : box.getUsers()) - { - if (auto propertyRefOp = mlir::dyn_cast(user)) - { - for (auto *fieldUser : propertyRefOp->getUsers()) - { - auto storeOp = mlir::dyn_cast(fieldUser); - if (!storeOp || storeOp.getReference() != propertyRefOp.getResult()) - { - return; - } - - closure->fills.push_back({propertyRefOp.getPosition(), storeOp}); - closure->hasCells = closure->hasCells || mlir::isa(storeOp.getValue().getType()); - } - - continue; - } - - if (mlir::isa(user)) - { - boxRetains.push_back(user); - continue; - } - - if (!closure->escape && isStoreIntoState(user, box)) - { - closure->escape = user; - continue; - } - - return; - } - - if (!closure->escape) - { - return; - } - - auto stateRef = mlir::cast(closure->escape).getReference().getDefiningOp(); - auto stateType = mlir::cast(stateRef->getOperand(0).getType()).getElementType(); - auto borrowing = borrowingFieldsOf(stateType); - closure->borrowsValues = !borrowing.empty() && borrowing.front() == stateRef.getPosition(); - // `.map`'s maker copies what its parameter box holds into the generator's box: a borrow - if (closure->borrowsValues) - { - for (auto &fill : closure->fills) - { - if (!mlir::isa(fill.second.getValue().getType())) - { - setAttrTracked(fill.second, OWN_CAPTURE_BORROW_ATTR_NAME); - } - } - } - - for (auto &fill : closure->fills) - { - // a parameter this function does not own, in a box that does not borrow: the box - // cannot take rc's reference for it, the caller's still holds it; left to the - // second-reference error, which says the callers would have to give it up - auto param = borrowedParam(fill.second.getValue()); - if (!closure->borrowsValues && param >= 0 && !llvm::is_contained(ownedParams(getFunction()), param)) - { - continue; - } - - if (auto *retain = retainBefore(fill.second.getValue(), fill.second)) - { - closure->retains.push_back(retain); - captureRetains.insert(retain); - } - } - - for (auto *retain : boxRetains) - { - closure->retains.push_back(retain); - captureRetains.insert(retain); - } - - stateBoxes[box] = closure.get(); - closures.push_back(std::move(closure)); - }); - } - - // `ts.Store(box, ts.PropertyRef(state, i))`, where `state` is a local a `ts.Constant` - // initializes whose every read fills a block `ts.New` made: a generator's initial state. - static bool isStoreIntoState(mlir::Operation *user, mlir::Value box) - { - auto storeOp = mlir::dyn_cast(user); - auto propertyRefOp = storeOp && storeOp.getValue() == box - ? storeOp.getReference().getDefiningOp() - : mlir_ts::PropertyRefOp(); - auto stateOp = propertyRefOp ? propertyRefOp->getOperand(0).getDefiningOp() : mlir_ts::VariableOp(); - if (!stateOp || !isConstantRecordLocal(stateOp)) - { - return false; - } - - auto reads = 0; - for (auto *stateUser : stateOp->getUsers()) - { - auto loadOp = mlir::dyn_cast(stateUser); - if (!loadOp) - { - continue; - } - - ++reads; - for (auto *readUser : loadOp->getUsers()) - { - // rc's retain of the initial state, for fields that own (holdsConstantsOrMoves) - if (mlir::isa(readUser)) - { - continue; - } - - auto fillOp = mlir::dyn_cast(readUser); - if (!fillOp || fillOp.getValue() != loadOp.getResult() || !fillOp.getReference().getDefiningOp()) - { - return false; - } - } - } - - return reads > 0; - } - - // rc's reference for the box's copy of `value`: the retain of it nearest before the store that - // fills the box, in the same block. - mlir::Operation *retainBefore(mlir::Value value, mlir::Operation *store) - { - // rc's retain of an earlier read of the same place (twinReadAfter) - if (auto loadOp = value.getDefiningOp()) - { - for (auto *user : loadOp.getReference().getUsers()) - { - auto earlier = mlir::dyn_cast(user); - auto *retain = earlier ? soleUser(earlier.getResult()) : nullptr; - if (retain && mlir::isa(retain) && !captureRetains.contains(retain) && - twinReadAfter(retain, earlier.getResult()) == value && retain->getBlock() == store->getBlock() && - retain->isBeforeInBlock(store)) - { - return retain; - } - } - } - - mlir::Operation *found = nullptr; - for (auto *user : value.getUsers()) - { - if (!mlir::isa(user) || user->getBlock() != store->getBlock() || - !user->isBeforeInBlock(store) || captureRetains.contains(user)) - { - continue; - } - - if (!found || found->isBeforeInBlock(user)) - { - found = user; - } - } - - return found; - } - - // Where a closure value goes: through its views and the owning `let`s that hold it, each use - // that may run it joins `uses`, and each release `ends`. Answers the first use that takes it - // anywhere else - returned, stored, kept by a callee, captured - or null. Anything this does - // not know is such a use. - mlir::Operation *followClosure(mlir::Value closure, llvm::SmallVectorImpl &uses, - llvm::SmallVectorImpl &ends, bool &aliased) - { - llvm::SmallVector values{closure}; - llvm::SmallPtrSet seen; - mlir::Operation *escape = nullptr; - auto followSlot = [&](mlir::Value slot) { - for (auto *slotUser : slot.getUsers()) - { - if (mlir::isa(slotUser)) - { - continue; - } - - if (mlir::isa(slotUser)) - { - ends.push_back(slotUser); - } - else if (auto loadOp = mlir::dyn_cast(slotUser)) - { - values.push_back(loadOp.getResult()); - } - else if (auto storeOp = mlir::dyn_cast(slotUser); !storeOp || storeOp.getReference() != slot) - { - escape = escape ? escape : slotUser; - } - } - }; - - while (!values.empty() && !escape) - { - auto value = values.pop_back_val(); - if (!seen.insert(value).second) - { - continue; - } - - forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { - if (escape || mlir::isa(user)) - { - return; - } - - if (mlir::isa(user)) - { - ends.push_back(user); - return; - } - - if (isCall(user)) - { - if (isKeptArgument(user, used)) - { - escape = user; - } - else - { - uses.push_back(user); - } - - return; - } - - // the function and the box a call through the closure is made with - if (mlir::isa(user)) - { - for (auto *partUser : user->getResult(0).getUsers()) - { - if (isCall(partUser) && !isKeptArgument(partUser, user->getResult(0))) - { - uses.push_back(partUser); - } - else - { - escape = escape ? escape : partUser; - } - } - - return; - } - - mlir::Value slot; - if (auto varOp = mlir::dyn_cast(user)) - { - slot = varOp.getResult(); - } - else if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getValue() == used) - { - slot = storeOp.getReference(); - } - - // a local, owning or not (`let f: () => number;` owns nothing): its reads are the - // closure; a function's result slot is a local too, and its read is returned - auto slotVar = slot ? slot.getDefiningOp() : mlir_ts::VariableOp(); - if (slotVar && !slotVar.getCaptured().value_or(false)) - { - aliased = aliased || !isOwningVariable(slotVar); - followSlot(slot); - return; - } - - escape = user; - }); - } - - return escape; - } - - // Does a call given this value give it a closure, which may run and touch what its box holds? - static bool holdsCaptures(mlir::Value value) - { - if (mlir::isa(value.getType())) - { - return true; - } - - auto root = rootOf(value); - if (auto varOp = root.getDefiningOp()) - { - return varOp->hasAttr(CAPTURE_BOX_ATTR_NAME); - } - - // a closure's `this` is its box, a reference; a bound method's is the object - auto getThisOp = root.getDefiningOp(); - return getThisOp && mlir::isa(getThisOp.getType()); - } - - // The verdicts on the closures over cells: an escape is an error; a borrow must not be run after - // anything it borrows is given back - a cell this function owns at its releases, a copy at its - // owner's ends. A cell an enclosing closure holds and a parameter end nowhere here; a copy of a - // read out of a container is bounded by the read's own check, which sees the closure's uses - // (walkBorrowed). - void decideClosures(llvm::SetVector &toErase) - { - for (auto &closure : closures) - { - if (!closure->op) - { - decideEscaping(*closure, toErase); // a generator's box - continue; - } - - auto *boundOp = closure->op.getOperation(); - auto name = ownerName(closure->op.getResult()); - auto ok = true; - // Held by a local that owns nothing: rc's birth reference reads to the move logic as the - // store's, which would leave the box with no owner. The closure keeps its releases, and - // nothing may run it after them. - auto keepAliasedReleases = [&]() { - for (auto *end : closure->ends) - { - if (!mlir::isa(end)) - { - continue; - } - - toErase.remove(end); - for (auto *use : closure->uses) - { - if (ok && reachableAfter(end, use, boundOp)) - { - reportClosureOutlives(use, end, name, name); - ok = false; - } - } - } - }; - - if (!closure->hasCells && !closure->borrowsCopies) - { - keepAliasedReleases(); - continue; - } - - if (closure->escape) - { - decideEscaping(*closure, toErase); - continue; - } - - for (auto &fill : closure->fills) - { - auto value = fill.second.getValue(); - llvm::SmallVector ends; - llvm::StringRef captured = "this value"; - if (mlir::isa(value.getType())) - { - if (isCellVariable(value)) - { - captured = varName(value.getDefiningOp()); - for (auto *user : value.getUsers()) - { - if (mlir::isa(user) || movesCellAway(user)) - { - ends.push_back(user); - } - } - } - else if (!isLoadedCell(value)) - { - reportCaptureNotOwned(fill.second, captured); - ok = false; - continue; - } - } - else - { - auto root = rootOf(value); - captured = ownerName(root); - Chain chain; - if (holdsNoBlock(value) || placeReadOf(root) || borrowedParam(root) >= 0 || isHandle(root)) - { - // nothing to own, a read the read's check bounds, a parameter, or a handle, - // whose copy in the box is counted (spec 23.2) - } - else if (auto slotLoad = slotLoadOf(root)) - { - chain.roots.push_back({slotLoad.getReference(), Chain::Slot}); - } - else if (isFresh(root)) - { - chain.roots.push_back({root, Chain::Owned}); - } - else - { - reportCaptureNotOwned(fill.second, captured); - ok = false; - continue; - } - - ends = rootEnds(chain); - } - - for (auto *end : ends) - { - if (captureRetains.contains(end) || captureStores.count(end)) - { - continue; // a box's own copy, this one's or another borrowing closure's - } - - for (auto *use : closure->uses) - { - if (ok && reachableAfter(end, use, boundOp)) - { - reportClosureOutlives(use, end, name, captured); - ok = false; - } - } - } - } - - if (closure->aliased) - { - keepAliasedReleases(); - } - - // a call through the box, after the closure value that owns the box is given back - for (auto *end : closure->ends) - { - for (auto *use : closure->boxUses) - { - if (ok && reachableAfter(end, use, boundOp)) - { - reportClosureOutlives(use, end, name, name); - ok = false; - } - } - } - - toErase.insert(closure->retains.begin(), closure->retains.end()); - } - } - - // Is this the store that moves a cell into the box of a closure that escapes? - bool movesCellAway(mlir::Operation *op) - { - auto storeOp = mlir::dyn_cast(op); - auto propertyRefOp = storeOp ? storeOp.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); - if (!propertyRefOp || !mlir::isa(storeOp.getValue().getType())) - { - return false; - } - - if (stateBoxes.count(propertyRefOp.getObjectRef())) - { - return true; - } - - auto boundOp = closureOfBox(propertyRefOp.getObjectRef()); - auto found = boundOp ? closureOf.find(boundOp.getOperation()) : closureOf.end(); - return found != closureOf.end() && found->second->escape; - } - - // A closure that escapes owns what its box holds (spec 2.5, phase 5b). Its copies moved in as - // in phase 4. Each cell moves in where the box takes it: that store is the move, rc's - // `ts.RetainCell` for it goes, and so does each of the frame's `ts.ReleaseCell`s the move reaches, - // which it must dominate. Nothing in the frame may use the variable after the move - read it, - // assign it, capture it again - and the move may not come round a loop to a cell made outside - // it. A cell that borrows its value (a parameter's) or one an enclosing closure holds has - // nothing to move. And nothing may call through the box once the closure has gone. - void decideEscaping(Closure &closure, llvm::SetVector &toErase) - { - auto ok = true; - for (auto &fill : closure.fills) - { - auto value = fill.second.getValue(); - mlir::Operation *move = fill.second; - if (!mlir::isa(value.getType())) - { - continue; - } - - if (isLoadedCell(value)) - { - reportEscapingCellNotOwned(move, closure.escape, "it is captured from an enclosing function"); - ok = false; - continue; - } - - auto varOp = isCellVariable(value) ? value.getDefiningOp() : mlir_ts::VariableOp(); - if (!varOp) - { - reportCaptureNotOwned(move, "this value"); - ok = false; - continue; - } - - auto name = varName(varOp); - MLIRTypeHelper mth(&getContext(), CompileOptions{}); - auto elementType = mlir::cast(value.getType()).getElementType(); - // a handle's cell holds a count of its own, whoever's the value was (spec 23.2) - if (!isOwningVariable(varOp) && !closure.borrowsValues && mth.ownsHeapMemory(varOp.getLoc(), elementType) && - !MLIRTypeHelper::isSharedHandleType(elementType)) - { - reportEscapingCellNotOwned(move, closure.escape, "its value is the caller's"); - ok = false; - continue; - } - - if (takerLoops(move, varOp)) - { - reportMovedInLoop(move, name); - ok = false; - continue; - } - - llvm::SmallVector moved; - for (auto *user : value.getUsers()) - { - if (user == move || llvm::is_contained(closure.retains, user) || !reachableAfter(move, user, varOp)) - { - continue; - } - - if (!mlir::isa(user)) - { - reportUsedAfterEscapingCapture(user, move, name); - ok = false; - break; - } - - if (!dominance->properlyDominates(move, user)) - { - reportMovedOnSomePaths(move, user, name); - ok = false; - break; - } - - moved.push_back(user); - } - - toErase.insert(moved.begin(), moved.end()); - } - - // A call through the box itself after the closure was given away or given back. followClosure - // stops at the escape, so `ends` may miss releases past it; that is enough while the only - // calls through a box are those of a folded `const f`, whose release ends its block. A - // generator's box has no closure value and no such call. - if (!closure.op) - { - toErase.insert(closure.retains.begin(), closure.retains.end()); - return; - } - - auto name = ownerName(closure.op.getResult()); - llvm::SmallVector gone(closure.ends.begin(), closure.ends.end()); - gone.push_back(closure.escape); - for (auto *end : gone) - { - for (auto *use : closure.boxUses) - { - if (ok && use != end && reachableAfter(end, use, closure.op.getOperation())) - { - reportUseAfterMove(use, end, name); - ok = false; - } - } - } - - toErase.insert(closure.retains.begin(), closure.retains.end()); - } - - // A captured variable whose value this function does not own - a parameter, `this` - has a cell - // that borrows the value: the cell's release frees the cell only (OWN_CELL_BORROWS_ATTR_NAME), - // and nothing may assign the variable, here or in a closure over it, since the old value is the - // caller's. A variable whose value nobody can be said to own is an error. - void decideCells() - { - getFunction().walk([&](mlir_ts::VariableOp varOp) { - auto cell = varOp.getResult(); - if (!isCellVariable(cell) || isOwningVariable(varOp)) - { - return; - } - - MLIRTypeHelper mth(&getContext(), CompileOptions{}); - auto elementType = mlir::cast(cell.getType()).getElementType(); - if (!mth.ownsHeapMemory(varOp.getLoc(), elementType)) - { - return; - } - - // A handle's cell holds a count of its own, taken where it is made (spec 23.2): its - // release gives that back, as under rc, and an assignment is a counted copy. - if (MLIRTypeHelper::isSharedHandleType(elementType)) - { - return; - } - - auto name = varName(varOp); - auto init = varOp.getInitializer(); - auto argument = init ? mlir::dyn_cast(init) : mlir::BlockArgument(); - if (!argument || !argument.getOwner()->isEntryBlock() || - !mlir::isa(argument.getOwner()->getParentOp())) - { - reportCaptureNotOwned(varOp, name); - return; - } - - for (auto *user : cell.getUsers()) - { - if (mlir::isa(user)) - { - setAttrTracked(user, OWN_CELL_BORROWS_ATTR_NAME); - continue; - } - - auto storeOp = mlir::dyn_cast(user); - if (mlir::isa(user) || (storeOp && storeOp.getReference() == cell)) - { - reportCapturedParamAssigned(user, name); - return; - } - - // captured: the closure's body must not assign it either - auto propertyRefOp = storeOp ? storeOp.getReference().getDefiningOp() - : mlir_ts::PropertyRefOp(); - auto boundOp = propertyRefOp ? closureOfBox(propertyRefOp.getObjectRef()) : mlir_ts::CreateBoundFunctionOp(); - if (!boundOp) - { - continue; - } - - auto assigned = boundOp->getAttrOfType(OWN_ASSIGNS_CAPTURES_ATTR_NAME); - if (assigned && llvm::is_contained(assigned.asArrayRef(), static_cast(propertyRefOp.getPosition()))) - { - reportCapturedParamAssigned(boundOp, name); - return; - } - } - }); - } - - // An argument read out of a local is lent to the call, but the arguments are read in order: a - // later one may give the local a new value before the call takes the first - `g(n, (n = mk(), 0))`, - // in a conditional argument too - or call a closure that assigns it, when it is captured. The - // assignment drops the block the argument is, and the callee reads freed memory. rc holds such an - // argument across the call instead (OwnedReturnConsumptionPass, holdGlobalArguments). A read of a - // global is checkGlobalRead's, and a store into a place in the local the place borrows' (a field - // read `g(n.v, (n.v = mk(), 0))`); a store into the local's slot ends `n.v` too. - void checkArgumentsAssigned() - { - getFunction().walk([&](mlir::Operation *call) { - if (!isCall(call)) - { - return mlir::WalkResult::advance(); - } - - for (auto argument : callArgs(call)) - { - // a method's object: `this` of the method bound to it - if (auto getThisOp = argument.getDefiningOp()) - { - auto *boundOp = getThisOp->getOperand(0).getDefiningOp(); - if (mlir::isa_and_nonnull(boundOp)) - { - argument = boundOp->getOperand(0); - } - } - - auto *read = argument.getDefiningOp(); - auto local = mlir::dyn_cast_or_null(slotOf(argument)); - if (!read || !local || !ownsHeap(argument) || isHandle(argument)) - { - continue; - } - - auto assignedByClosure = closureAssigns(local); - auto *write = findBetween(read, call, [&](mlir::Operation *op) { - if (assignedByClosure && isCall(op)) - { - return true; - } - - auto storeOp = mlir::dyn_cast(op); - auto releaseSlotOp = mlir::dyn_cast(op); - return (storeOp && storeOp.getReference() == local.getResult()) || - (releaseSlotOp && releaseSlotOp.getSlot() == local.getResult()); - }); - if (!write) - { - continue; - } - - if (reporting()) - { - auto name = varName(local); - auto callee = calleeName(call); - auto diag = call->emitError("'") - << name << "' is lent to '" << (callee.empty() ? "a function" : callee) - << "', but is given a new value before the call takes it; -mm=own cannot prove " - "it outlives the call"; - diag.attachNote(write->getLoc()) << "'" << name << "' is given a new value here"; - signalPassFailure(); - } - - return mlir::WalkResult::interrupt(); - } - - return mlir::WalkResult::advance(); - }); - } - - // Does a closure built over this captured local assign it (OWN_ASSIGNS_CAPTURES_ATTR_NAME)? Yes too - // when its cell goes where this does not follow. - bool closureAssigns(mlir_ts::VariableOp local) - { - if (!local.getCaptured().value_or(false)) - { - return false; - } - - for (auto *user : local->getUsers()) - { - auto storeOp = mlir::dyn_cast(user); - if (mlir::isa(user) || - (storeOp && storeOp.getReference() == local.getResult())) - { - continue; - } - - // the cell put into a closure's capture box - auto propertyRefOp = storeOp ? storeOp.getReference().getDefiningOp() - : mlir_ts::PropertyRefOp(); - auto boundOp = propertyRefOp ? closureOfBox(propertyRefOp.getObjectRef()) : mlir_ts::CreateBoundFunctionOp(); - if (!boundOp) - { - return true; - } - - auto assigned = boundOp->getAttrOfType(OWN_ASSIGNS_CAPTURES_ATTR_NAME); - if (assigned && llvm::is_contained(assigned.asArrayRef(), static_cast(propertyRefOp.getPosition()))) - { - return true; - } - } - - return false; - } - - struct SlotReceiver - { - mlir::Operation *retain; - mlir::Value value; - mlir_ts::LoadOp load; - }; - - template bool quietly(F check) - { - ++quiet; - auto result = check(); - --quiet; - return result; - } - - // Move, else borrow, else error (spec 2.2) for the receivers of one owning local. - void decideSlot(mlir::Value slot, llvm::ArrayRef receivers, llvm::SetVector &toErase) - { - auto owner = varName(slot.getDefiningOp()); - if (receivers.size() == 1 || takeValuesOfTheirOwn(slot, receivers)) - { - // a move on some paths only is placed when it is the local's one receiver (spec 30) - for (auto &receiver : receivers) - { - decideSlotReceiver(slot, receiver, owner, toErase, receivers.size() == 1); - } - - return; - } - - decideSlotReceivers(slot, receivers, owner, toErase); - } - - void decideSlotReceiver(mlir::Value slot, const SlotReceiver &receiver, llvm::StringRef owner, - llvm::SetVector &toErase, bool sole) - { - if (quietly([&] { return checkSlotMove(receiver.retain, receiver.value, receiver.load, toErase, sole); })) - { - // rc's retain goes with the move; a call that keeps the argument is the move itself - if (!isCall(receiver.retain)) - { - toErase.insert(receiver.retain); - } - - return; - } - - if (auto borrower = borrowerOf(receiver.retain)) - { - borrowWith(borrower, receiver.retain, slotEnds(slot), owner, toErase); - return; - } - - checkSlotMove(receiver.retain, receiver.value, receiver.load, toErase, sole); // reports - } - - // Receivers of one owning local that each read a value of its own (#528): `h.c = a; a = new C(); - // g.c = a`. An assignment comes between every two of their reads on every path from one to the - // other, so each is decided alone, as the one receiver of the value it reads. So are receivers - // no path joins at all, assigned or not: `if (c) return a; return a;` moves `a` once on each - // way out. - bool takeValuesOfTheirOwn(mlir::Value slot, llvm::ArrayRef receivers) - { - auto assignments = slotAssignments(slot); - llvm::SmallVector kills(assignments.begin(), assignments.end()); - if (auto varOp = slot.getDefiningOp()) - { - kills.push_back(varOp); - } - - for (auto &from : receivers) - { - for (auto &to : receivers) - { - if (&from != &to && reachableAfter(from.load, to.load, kills)) - { - return false; - } - } - } - - return true; - } - - // ---- A kept parameter released where it is not moved (spec 33) ---- - // - // `return v.x > 2 ? acc : V.plus(acc, v)`: `acc` is kept (OwnershipSignaturePass), so the caller - // moved it in; the path that returns it moves it out, and the others must release it. So must a - // callback that only borrows the accumulator its family gives it to keep. MLIRGen made no owner - // of a parameter, so its releases are put in here, before each way out of the body - a return, - // a throw that leaves the function - as MLIRGen puts an owning local's. The move's own check then - // erases those it reaches, and places the release on the edges into the join that did not move - // it (spec 30), statically: no run-time flag. Unwinding out of the function (a call that throws - // outside a `try`, a `finally` passed on the way out) gives nothing back: it leaks, as an owning - // local does there. - - // the releases put in for this run - llvm::SmallVector exitReleases; - - // The slot of parameter `index` (isParameterSlot), or null. - mlir_ts::VariableOp parameterSlot(int32_t index) - { - auto &body = getFunction().getFunctionBody(); - if (body.empty() || static_cast(index) >= body.front().getNumArguments()) - { - return {}; - } - - for (auto *user : body.front().getArgument(index).getUsers()) - { - auto varOp = mlir::dyn_cast(user); - auto position = -1; - if (varOp && isParameterSlot(varOp, position) && position == index) - { - return varOp; - } - } - - return {}; - } - - // Is `slot` a parameter this function keeps and releases where it does not move it? - bool isReleasedParam(mlir::Value slot) - { - auto f = getFunction(); - auto attr = f->getAttrOfType(OWN_PARAMS_RELEASED_ATTR_NAME); - auto varOp = slot.getDefiningOp(); - auto index = -1; - return attr && varOp && isParameterSlot(varOp, index) && llvm::is_contained(attr.asArrayRef(), index) && - llvm::is_contained(ownedParams(f), index); - } - - // Does `value` come from a read of `slot`: computed from one, through any op? - static bool readsSlot(mlir::Value value, mlir::Value slot, int depth = 0) - { - auto *def = value.getDefiningOp(); - if (!def) - { - return false; - } - - // too deep to tell: as if it did - if (depth > 16) - { - return true; - } - - if (auto loadOp = mlir::dyn_cast(def); loadOp && loadOp.getReference() == slot) - { - return true; - } - - return llvm::any_of(def->getOperands(), [&](mlir::Value operand) { return readsSlot(operand, slot, depth + 1); }); - } - - void releaseKeptParamsAtExits() - { - exitReleases.clear(); - auto f = getFunction(); - auto attr = f->getAttrOfType(OWN_PARAMS_RELEASED_ATTR_NAME); - if (!attr) - { - return; - } - - for (auto index : attr.asArrayRef()) - { - auto varOp = parameterSlot(index); - if (!varOp || !llvm::is_contained(ownedParams(f), index)) - { - continue; - } - - for (auto &block : f.getFunctionBody()) - { - auto *exit = block.empty() ? nullptr : &block.back(); - // a thrown value made of the parameter takes it with it: no release there (a leak, - // if it does not) - auto leaves = mlir::isa_and_nonnull(exit) || - (mlir::isa_and_nonnull(exit) && - !readsSlot(exit->getOperand(0), varOp.getResult())); - if (!leaves) - { - continue; - } - - mlir::OpBuilder builder(exit); - exitReleases.push_back(builder.create(exit->getLoc(), varOp.getResult())); - } - } - } - - // A parameter this function keeps (`__own_params`): its caller moved the argument in, and this - // function has no release for it, so exactly one taker must move it on, on every way out of - // the function, and nothing may read it after that. - void decideParam(mlir::Value slot, llvm::ArrayRef receivers, llvm::SetVector &toErase) - { - auto name = varName(slot.getDefiningOp()); - auto &receiver = receivers.front(); - auto *taker = takerOf(receiver.retain, receiver.value); - if (receivers.size() > 1) - { - auto &second = receivers[1]; - auto *secondTaker = takerOf(second.retain, second.value); - reportUseAfterMove(secondTaker ? secondTaker : second.retain, taker ? taker : receiver.retain, name); - return; - } - - // one the body releases on the paths that do not move it (spec 33): the move's own check - // erases the releases at the exits it reaches, and places one on each edge into where the - // paths join that did not move it (spec 30) - auto released = isReleasedParam(slot); - if (!checkSlotMove(receiver.retain, receiver.value, receiver.load, toErase, released)) - { - return; - } - - if (released) - { - if (!isCall(receiver.retain)) - { - toErase.insert(receiver.retain); - } - - return; - } - - auto &body = getFunction().getFunctionBody(); - auto *moved = body.findAncestorOpInRegion(*taker); - auto everyPath = true; - getFunction().walk([&](mlir_ts::ReturnInternalOp returnOp) { - auto *exit = body.findAncestorOpInRegion(*returnOp); - if (everyPath && (!moved || !exit || !dominance->dominates(moved, exit))) - { - reportKeptOnSomePaths(taker, returnOp, name); - everyPath = false; - } - }); - - // rc's retain goes with the move - a `ts.RetainSlot` too, for an owning local the parameter - // moves into (`let r = init`, spec 33); a call that keeps it is the move itself - if (everyPath && !isCall(receiver.retain)) - { - toErase.insert(receiver.retain); - } - } - - // The thrown value, read out of the exception object by a catch's copy thunk (`.eh.copy.*`, - // which the C++ runtime calls to copy the exception into the catch). rc retains it for the - // copy; own moves it: the exception object never gives its reference back (a throw hands the - // thrower's reference to it, and nothing releases it - rc leaks it the same way), and each - // exception is copied into one catch, since a TypeScript rethrow is a new throw. - bool isReadOutOfException(mlir::Value value) - { - auto loadOp = rootOf(value).getDefiningOp(); - auto argument = loadOp ? mlir::dyn_cast(loadOp.getReference()) : mlir::BlockArgument(); - return argument && argument.getOwner()->isEntryBlock() && getFunction().getName().starts_with(".eh.copy."); - } - - // The read of a parameter this function keeps, through its views. None for anything else. - mlir_ts::LoadOp keptParamLoadOf(mlir::Value value) - { - auto loadOp = rootOf(value).getDefiningOp(); - auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); - auto index = -1; - if (!varOp || !isParameterSlot(varOp, index) || !llvm::is_contained(ownedParams(getFunction()), index)) - { - return {}; - } - - return loadOp; - } - - // More than one receiver of one owning local. Each has to borrow: a borrow pins the owner, so - // none may move it. A receiver that is not a `let` reports its move error - another - // receiver's reads come after it - or, if its move alone would pass, that a borrow pins it. - void decideSlotReceivers(mlir::Value slot, llvm::ArrayRef receivers, llvm::StringRef owner, - llvm::SetVector &toErase) - { - auto ends = slotEnds(slot); - mlir_ts::VariableOp someBorrower; - for (auto &receiver : receivers) - { - if (auto borrower = borrowerOf(receiver.retain)) - { - someBorrower = borrower; - borrowWith(borrower, receiver.retain, ends, owner, toErase); - } - } - - for (auto &receiver : receivers) - { - if (borrowerOf(receiver.retain)) - { - continue; - } - - llvm::SetVector unused; - if (checkSlotMove(receiver.retain, receiver.value, receiver.load, unused)) - { - reportMovedWhileBorrowed(receiver.retain, owner, someBorrower ? varName(someBorrower) : "another variable"); - } - } - } - - // A `let` that could borrow: owning, declared from a value, holding a reference it took - // itself (a `ts.RetainSlot`, not the value's own consumed one), and not captured. With - // `consumed`, also one that took over the reference rc thought the value carried: a call's - // result that turned out to borrow an argument carries none. - static mlir_ts::VariableOp borrowerOf(mlir::Operation *acquisition, bool consumed = false) - { - mlir_ts::VariableOp varOp; - if (auto retainSlotOp = mlir::dyn_cast(acquisition)) - { - varOp = retainSlotOp.getSlot().getDefiningOp(); - } - else if (auto retainOp = mlir::dyn_cast(acquisition)) - { - // rc's reference for a `let` that takes the value over: `let ri = raw` retains the - // interface and declares `ri` consuming it - varOp = consumingLetOf(retainOp.getReference()); - consumed = true; - } - else - { - varOp = mlir::dyn_cast(acquisition); - } - - if (!varOp || !varOp.getInitializer() || !isOwningVariable(varOp) || varOp.getCaptured().value_or(false)) - { - return {}; - } - - if (varOp->hasAttr(OWNED_LOCAL_CONSUMED_ATTR_NAME)) - { - return consumed ? varOp : mlir_ts::VariableOp(); - } - - auto retains = llvm::any_of(varOp.getResult().getUsers(), - [](mlir::Operation *user) { return mlir::isa(user); }); - return retains ? varOp : mlir_ts::VariableOp(); - } - - // tryBorrow for the `let` an acquisition gives a reference: a `ts.Retain` of the value a - // consuming `let` takes over is that `let`'s, and goes with the borrow; a `ts.RetainSlot` is the - // `let`'s own bookkeeping already. - bool borrowWith(mlir_ts::VariableOp borrower, mlir::Operation *acquisition, llvm::ArrayRef ends, - llvm::StringRef owner, llvm::SetVector &toErase) - { - if (!tryBorrow(borrower, ends, owner, toErase)) - { - return false; - } - - if (mlir::isa(acquisition)) - { - toErase.insert(acquisition); - } - - return true; - } - - // The owning `let` declared from `value` that takes over its reference, or none. - static mlir_ts::VariableOp consumingLetOf(mlir::Value value) - { - for (auto *user : value.getUsers()) - { - auto varOp = mlir::dyn_cast(user); - if (varOp && varOp.getInitializer() == value && varOp->hasAttr(OWNED_LOCAL_CONSUMED_ATTR_NAME)) - { - return varOp; - } - } - - return {}; - } - - // Where an owning local stops holding its value: each release of its slot and each store - // into it. - static llvm::SmallVector slotEnds(mlir::Value slot) - { - llvm::SmallVector ends; - for (auto *user : slot.getUsers()) - { - if (mlir::isa(user)) - { - ends.push_back(user); - } - } - - return ends; - } - - // A borrow (spec 2.2, verdict 2): `borrower` takes no reference and gives none back, so its - // retain and every release of its slot go, and the owner keeps its own. Sound while no use of - // the borrower can run after any of `ends` - the owner's releases and assignments - and while - // nothing it holds is kept: not stored, returned, retained or assigned. A path back to a use - // through the borrower's own declaration is a new borrow, not this one. Reports and returns - // false otherwise. - bool tryBorrow(mlir_ts::VariableOp borrower, llvm::ArrayRef ends, llvm::StringRef owner, - llvm::SetVector &toErase) - { - auto name = varName(borrower); - llvm::SmallVector uses; - llvm::SmallVector bookkeeping; - mlir::Operation *declaration = borrower; - if (auto *kept = collectBorrowerUses(borrower, declaration, uses, bookkeeping)) - { - if (isAssignmentOf(kept, borrower)) - { - reportBorrowerAssigned(kept, name, owner); - } - else - { - reportBorrowEscapes(kept, name, owner); - } - - return false; - } - - for (auto *end : ends) - { - for (auto &use : uses) - { - if (reachableAfter(end, use.op, use.kills)) - { - reportBorrowOutlives(use.op, end, name, owner); - return false; - } - } - } - - toErase.insert(bookkeeping.begin(), bookkeeping.end()); - return true; - } - - // A use of a borrowed value, and where the value it sees is made again: a path from a release - // to the use through one of `kills` reaches a new value, not this one. For the borrow's own - // uses that is the read or the borrowing `let`; for a use through a merge it is the merge's - // block, and through a local that owns nothing, that local's declaration and assignments. - struct BorrowUse - { - mlir::Operation *op; - llvm::SmallVector kills; - // the function value an indirect call goes through: read when the call starts, not while - // the callee runs - bool callee = false; - }; - - // A borrowing `let`'s slot: each read, and everything it leads to (walkBorrowed), joins - // `uses`, made again at `kills`; its `ts.RetainSlot` and `ts.ReleaseSlot`s are `bookkeeping`, - // which a borrow erases. Answers the first use that would keep what the slot holds - an - // assignment, or anything else that is not a read - or null. - mlir::Operation *collectBorrowerUses(mlir_ts::VariableOp borrower, llvm::ArrayRef kills, - llvm::SmallVectorImpl &uses, - llvm::SmallVectorImpl &bookkeeping) - { - for (auto *user : borrower.getResult().getUsers()) - { - if (mlir::isa(user)) - { - bookkeeping.push_back(user); - continue; - } - - auto readOp = mlir::dyn_cast(user); - if (!readOp) - { - return user; - } - - uses.push_back({user, {kills.begin(), kills.end()}}); - if (auto *kept = walkBorrowed(readOp.getResult(), kills, uses)) - { - return kept; - } - } - - return nullptr; - } - - static bool isAssignmentOf(mlir::Operation *op, mlir_ts::VariableOp varOp) - { - auto storeOp = mlir::dyn_cast(op); - return storeOp && storeOp.getReference() == varOp.getResult(); - } - - // What `use` hands the very value it uses on to, as that value: a cast or a view of it, the - // argument of the block a branch passes it to (a merge: `a ?? b`), or the reads of a local - // that owns nothing and is assigned it (`for (x of arr)` into an outer `let x`). Answers - // whether the use is one of those, even where nothing comes out of it. Where the value lands - // somewhere that is replaced - a block argument, a local - `renewedAt` gets what replaces it: - // the block's entry, the local's declaration (when it has an initializer: `let l!: V` stores - // nothing, and in a loop the slot keeps the last round's value) and assignments. - static bool passesOn(mlir::OpOperand &use, llvm::SmallVectorImpl &values, - llvm::SmallVectorImpl *renewedAt = nullptr) - { - auto *user = use.getOwner(); - if (mlir::isa(user) || isView(user)) - { - values.push_back(user->getResult(0)); - return true; - } - - if (auto branchOp = mlir::dyn_cast(user)) - { - if (auto argument = branchOp.getSuccessorBlockArgument(use.getOperandNumber())) - { - values.push_back(*argument); - if (renewedAt) - { - renewedAt->push_back(&argument->getOwner()->front()); - } - - return true; - } - - return false; - } - - if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getValue() == use.get()) - { - if (auto varOp = storeOp.getReference().getDefiningOp(); - varOp && !isOwningVariable(varOp) && !varOp.getCaptured().value_or(false)) - { - if (renewedAt && varOp.getInitializer()) - { - renewedAt->push_back(varOp); - } - - for (auto *varUser : varOp.getResult().getUsers()) - { - if (auto loadOp = mlir::dyn_cast(varUser)) - { - values.push_back(loadOp.getResult()); - } - else if (renewedAt && isAssignmentOf(varUser, varOp)) - { - renewedAt->push_back(varUser); - } - } - - return true; - } - } - - return false; - } - - // Where a borrowed value goes. Every use of it, and of what it produces that may still point - // into the block, joins `uses`, made again at `kills`. The value itself - through what passesOn - // hands it on to - must not be kept anywhere; what it produces - a field reference, a value - // loaded through one, a bound method, a catch clause's non-owning local - still points into - // the borrowed block, so each of its uses is a use of the borrow too. A value that cannot point - // anywhere (a number, a boolean) ends the walk. Where passesOn hands the value to something - // that is replaced, the uses beyond are made again where it is replaced instead; the op that - // hands it on is itself a use, bounded by `kills`. `isBorrower` accepts a use of the value - // itself that the caller follows on its own (a borrowing `let`), made again at the kills it is - // given. Answers the first use that would keep the value. - // - // `keeping` names what rc planted around a borrow that it took for an owner and that goes - // with it: the temporary release of a result that borrows an argument (the start's, or that of - // a call it is passed to, which is a borrow of it too), and, in a function whose result - // borrows this very value's parameter, the retain rc made for the caller. Those join - // `bookkeeping`, not `uses`, and the return itself is a use, not a keep. - struct Keeping - { - // no default member initializers: GCC rejects them in a default argument of the class - // that encloses the struct - llvm::SmallVectorImpl *bookkeeping; - bool returns; - }; - - mlir::Operation *walkBorrowed( - mlir::Value start, llvm::ArrayRef kills, llvm::SmallVectorImpl &uses, - llvm::function_ref)> isBorrower = nullptr, - Keeping keeping = Keeping{nullptr, false}, bool countedHandle = false, bool startBorrowed = true) - { - struct Pending - { - mlir::Value value; - bool borrowed; - llvm::SmallVector kills; - }; - - llvm::SmallVector values; - // `startBorrowed` false: `start` is what a read produced (a field's reference), whose uses - // are uses of the borrow but not the borrowed value itself - values.push_back({start, startBorrowed, {kills.begin(), kills.end()}}); - llvm::SmallPtrSet seen{start}; - while (!values.empty()) - { - auto pending = values.pop_back_val(); - for (auto &use : pending.value.getUses()) - { - auto *user = use.getOwner(); - if (pending.borrowed && keeping.bookkeeping && - ((mlir::isa(user) && borrowingCall(rootOf(pending.value))) || - (keeping.returns && mlir::isa(user)))) - { - keeping.bookkeeping->push_back(user); - continue; - } - - // rc's reference for a borrowing box's copy goes with the closure (decideClosures), and - // one for a borrowing field's with the field (classifyBorrowingFields) - if (captureRetains.contains(user) || fieldBorrowRetains.contains(user)) - { - continue; - } - - uses.push_back({user, pending.kills, isCall(user) && isCalleeOperand(use)}); - // copied into a borrowing box: whatever runs the closure reads it - if (auto found = captureStores.find(user); found != captureStores.end()) - { - for (auto *closureUse : found->second->uses) - { - uses.push_back({closureUse, pending.kills}); - } - } - - // stored into a field a borrowing record borrows (spec 32): whatever uses the record - // reads it - if (auto found = recordStoreUses.find(user); found != recordStoreUses.end()) - { - for (auto &recordUse : found->second->walk.uses) - { - uses.push_back({recordUse.op, pending.kills, recordUse.callee}); - } - } - - if (pending.borrowed && keeping.returns && mlir::isa(user)) - { - continue; - } - - if (pending.borrowed) - { - // a handle read that rc counts (spec 23.3): a return or an `any` box rc - // retained for is a counted copy of its own; passesOn would hand them on - if (countedHandle && isCountedHandOff(user, pending.value)) - { - continue; - } - - llvm::SmallVector same; - llvm::SmallVector renewedAt; - if (passesOn(use, same, &renewedAt)) - { - for (auto value : same) - { - if (mayPointInto(value) && seen.insert(value).second) - { - values.push_back({value, true, renewedAt.empty() ? pending.kills : renewedAt}); - } - } - - continue; - } - - // a handle read that rc counts (spec 23.3): its retain, and what takes the value - // it retains for, are a counted copy of their own, as a string copy is; the - // read's other uses are the borrow's (Ruling 7) - if (countedHandle && isCountedHandleReceiver(user, pending.value)) - { - continue; - } - - if (isBorrower && isBorrower(user, pending.kills)) - { - continue; - } - - if (mlir::isa(user) || !isBorrow(user, pending.value)) - { - return user; - } - } - - // a copy (spec 22.2) reads the borrow into a block of its own, which points into - // nothing, and so does a concatenation: `s + t` is a new string whatever s and t are - // (a `+` of two strings lowers to ts.StringConcat, which always allocates) - if (mlir::isa(user) || - (mlir::isa(user) && mlir::isa(user->getResult(0).getType()))) - { - continue; - } - - // a call whose result is the caller's own (callResultIsOwn) ends the walk at the call - if (callResultIsOwn(user)) - { - continue; - } - - // A result that borrows what it was given borrows this too, and is walked as it. - for (auto result : user->getResults()) - { - if (mayPointInto(result) && seen.insert(result).second) - { - values.push_back({result, !!borrowingCall(result), pending.kills}); - } - } - } - } - - return nullptr; - } - - // Whether a call hands back a block of the caller's own, which points into nothing it was given: - // walkBorrowed then ends a borrow's walk at the call (the call itself is still a use for as long - // as it runs). Never a generator's state that borrows its arguments (`__own_result_bounded`). - // - A call known to make its result (`__own_fresh_result`: an iterator's `next()`, a field that - // only ever holds one function, another own module's `..new`) - the `{value, done}` record of - // `it.next()` points nowhere into `it`, and what it does after its next call cannot be a use of - // the method read out of `it`. - // - A direct call of a function this module defines (isFresh): its result points into nothing it - // was given but what its facts name - a borrow of an argument (`__own_result_borrows`, never - // fresh), a borrowing record (isFresh, but bounded by what it borrows), a generator's state - // that borrows its arguments - and a body whose callers cannot all know such a fact is an - // error. `getPoint(x, scene.camera)` makes a new vector: a call after it that may destroy - // `scene.camera` does not end that vector (spec 34). - static bool callResultIsOwn(mlir::Operation *call) - { - if (!isCall(call) || call->hasAttr(OWN_RESULT_BOUNDED_ATTR_NAME)) - { - return false; - } - - return call->hasAttr(OWN_FRESH_RESULT_ATTR_NAME) || - (call->getNumResults() == 1 && !isRecordResult(call) && isFresh(call->getResult(0))); - } - - // Is this what an indirect call goes through: its callee value, or, for a method read out of - // a field of an object (`o.m()`, `m` a field holding a function), the object it is bound to, - // which is the container of what was read, not what was borrowed. A method of a class bound - // to a borrowed instance (`h.c.m()`) is not one: its `this` is the borrow. - static bool isCalleeOperand(mlir::OpOperand &use) - { - auto *user = use.getOwner(); - auto callee = isCall(user) ? calleeValue(user) : mlir::Value(); - if (!callee) - { - return false; - } - - if (use.getOperandNumber() == 0) - { - return true; - } - - auto getThisOp = use.get().getDefiningOp(); - auto getMethodOp = callee.getDefiningOp(); - auto methodField = getThisOp ? getThisOp.getOperand().getDefiningOp() : mlir_ts::LoadOp(); - return use.getOperandNumber() == 1 && getMethodOp && methodField && - getThisOp.getOperand() == getMethodOp.getBoundFunc() && - mlir::isa(methodField.getReference().getType()); - } - - // ---- Borrowing records (spec 32) ---- - // - // An object literal boxed as an interface that holds values it does not own - `this`, a parameter, - // a field read, an owning `let`'s value, a captured variable's - beside fresh values, which move - // in. The signature pass marked its box (OWN_RECORD_BORROWED_FIELDS_ATTR_NAME): its release frees - // the box and the fields it owns, and leaves the others to their owners. So the box has one owner - // like any fresh block - the box's own check - and what it borrows bounds every use of it: a read - // out of a place is checked with the record's uses as its own (walkBorrowed), a local's value - // against the local's ends, a captured variable's against whatever may assign it. Returned, the - // record is the caller's to own and to bound (checkRecordResults), which the function's facts - // must say. Stored anywhere else that may outlive what it borrows, it is an error. - struct RecordWalk - { - llvm::SmallVector uses; - // the first use that would keep the record itself: a store into a place, a callee that keeps - // it, a capture - mlir::Operation *kept = nullptr; - llvm::SmallVector returns; - }; - - struct BorrowedField - { - mlir_ts::StoreOp store; - RecordFieldSource source; - }; - - struct BorrowingRecord - { - mlir_ts::NewOp box; - mlir_ts::VariableOp local; - llvm::SmallVector interfaces; - llvm::SmallVector borrowed; - RecordWalk walk; - }; - - llvm::SmallVector> records; - // a store of a place read into a borrowing record's field -> the record's uses, which are the - // read's uses too (walkBorrowed) - llvm::DenseMap recordStoreUses; - - // Can a value of this type point into a heap block - a reference into one, a block of its own, - // or a method bound to one? A number or a boolean read out of a borrowed block cannot. - bool mayPointInto(mlir::Value value) - { - auto type = value.getType(); - return mlir::isa(type) || - ownsHeap(value); - } - - // The borrowing record built in `varOp` (classifyRecords), or null. - BorrowingRecord *borrowingRecordOf(mlir_ts::VariableOp varOp) - { - for (auto &record : records) - { - if (record->local == varOp) - { - return record.get(); - } - } - - return nullptr; - } - - // The boxes the signature pass marked as borrowing records, where every field still holds what it - // was marked for: a borrowed field a value this pass bounds (recordFieldSource), any other one a - // fresh value or nothing. A store into a borrowed field takes nothing, and whatever uses the record - // reads what it stored (walkBorrowed, through recordStoreUses). A box that is not what it was - // marked for is left to the record's retain, which is then an error. - void classifyRecords() - { - getFunction().walk([&](mlir_ts::NewOp newOp) { - auto fields = recordBorrowedFields(newOp); - auto varOp = fields.empty() ? mlir_ts::VariableOp() : recordLocalOf(newOp.getResult()); - if (!varOp) - { - return; - } - - auto record = std::make_shared(); - record->box = newOp; - record->local = varOp; - for (auto *user : varOp->getUsers()) - { - auto propertyRefOp = mlir::dyn_cast(user); - if (!propertyRefOp) - { - continue; - } - - auto borrows = llvm::is_contained(fields, static_cast(propertyRefOp.getPosition())); - for (auto *fieldUser : propertyRefOp->getUsers()) - { - auto storeOp = mlir::dyn_cast(fieldUser); - if (!storeOp || storeOp.getReference() != propertyRefOp.getResult()) - { - continue; - } - - auto source = recordFieldSource(storeOp.getValue()); - if (source == RecordFieldSource::NoBlock) - { - continue; - } - - if (source == RecordFieldSource::Unknown || borrows != (source != RecordFieldSource::Fresh)) - { - return; - } - - if (borrows) - { - record->borrowed.push_back({storeOp, source}); - } - } - } - - for (auto *user : newOp->getUsers()) - { - if (auto castOp = mlir::dyn_cast(user)) - { - for (auto *castUser : castOp->getUsers()) - { - if (auto newInterfaceOp = mlir::dyn_cast(castUser)) - { - record->interfaces.push_back(newInterfaceOp.getResult()); - } - } - } - } - - // a path back to a use through the record's making is another record - llvm::SmallVector kills{newOp.getOperation(), varOp.getOperation()}; - walkRecord(record->interfaces, kills, record->walk); - for (auto &field : record->borrowed) - { - setAttrTracked(field.store, OWN_CAPTURE_BORROW_ATTR_NAME); - recordStoreUses[field.store.getOperation()] = record.get(); - } - - records.push_back(std::move(record)); - }); - } - - // Every use of a borrowing record (spec 32), and of what is read out of it - each may read what - // the record borrows - through its views, merges, and the locals that hold it, made again at - // `kills`. Its own retains and releases are the box's, which its own check decides. The record - // itself is `kept` when it is stored anywhere but a local, given to a callee that keeps it, or - // captured; returned, it is in `returns`. A call given it hands back its own block, unless that - // is a borrowing record that holds it, whose uses are then this one's as well. - void walkRecord(llvm::ArrayRef starts, llvm::ArrayRef kills, RecordWalk &walk) - { - struct Pending - { - mlir::Value value; - bool record; - }; - - llvm::SmallVector work; - llvm::SmallPtrSet seen; - auto push = [&](mlir::Value value, bool record) { - if ((record || mayPointInto(value)) && seen.insert(value).second) - { - work.push_back({value, record}); - } - }; - - auto followSlot = [&](mlir_ts::VariableOp varOp, bool record) { - for (auto *slotUser : varOp->getUsers()) - { - // its releases and its other values are the slot's own - if (auto loadOp = mlir::dyn_cast(slotUser)) - { - push(loadOp.getResult(), record); - } - } - }; - - auto keep = [&](mlir::Operation *user) { walk.kept = walk.kept ? walk.kept : user; }; - - for (auto start : starts) - { - push(start, true); - } - - while (!work.empty()) - { - auto pending = work.pop_back_val(); - auto value = pending.value; - for (auto &use : value.getUses()) - { - auto *user = use.getOwner(); - if (mlir::isa(user) || - (pending.record && mlir::isa(user))) - { - continue; - } - - // the same value: a view, a cast, a merge - if ((isView(user) && use.getOperandNumber() == 0) || mlir::isa(user)) - { - push(user->getResult(0), pending.record); - continue; - } - - if (auto branchOp = mlir::dyn_cast(user)) - { - if (auto argument = branchOp.getSuccessorBlockArgument(use.getOperandNumber())) - { - push(*argument, pending.record); - } - - continue; - } - - // a local that holds it: its reads - auto varOp = mlir::dyn_cast(user); - auto storeOp = mlir::dyn_cast(user); - if (varOp || (storeOp && storeOp.getValue() == value)) - { - auto slotOp = varOp ? varOp : storeOp.getReference().getDefiningOp(); - if (slotOp && !slotOp.getCaptured().value_or(false)) - { - followSlot(slotOp, pending.record); - continue; - } - - // a place, a global, a captured variable: what is read out of the record is checked - // where it is read (checkPlaceRead) - walk.uses.push_back({user, {kills.begin(), kills.end()}}); - if (pending.record) - { - keep(user); - } - - continue; - } - - walk.uses.push_back({user, {kills.begin(), kills.end()}, isCall(user) && isCalleeOperand(use)}); - if (mlir::isa(user)) - { - if (pending.record) - { - walk.returns.push_back(user); - } - - continue; - } - - if (isCall(user)) - { - if (pending.record && isKeptArgument(user, value)) - { - keep(user); - continue; - } - - // a result that borrows it - a plain borrow (`id(r)`), or a borrowing record that - // holds it - points where it points - auto args = callArgs(user); - auto given = [&](llvm::ArrayRef indices) { - return llvm::any_of(indices, [&](int index) { - return static_cast(index) < args.size() && args[index] == value; - }); - }; - - if (user->getNumResults() == 1 && given(resultBorrowsAll(user))) - { - push(user->getResult(0), pending.record || isRecordResult(user)); - } - - // ...and a generator's state that borrows it (OWN_RESULT_BOUNDED_ATTR_NAME) - if (auto bounded = user->getAttrOfType(OWN_RESULT_BOUNDED_ATTR_NAME); - bounded && user->getNumResults() == 1 && - given(llvm::SmallVector(bounded.asArrayRef().begin(), bounded.asArrayRef().end()))) - { - push(user->getResult(0), false); - } - - continue; - } - - if (pending.record && !isBorrow(user, value)) - { - keep(user); - continue; - } - - // a field written through the record: its old value may be one the record borrows, - // which the overwrite would release, and the field may be one its release leaves - if (pending.record && mlir::isa(user)) - { - for (auto *fieldUser : user->getResult(0).getUsers()) - { - auto fieldStore = mlir::dyn_cast(fieldUser); - if (mlir::isa(fieldUser) || - (fieldStore && fieldStore.getReference() == user->getResult(0))) - { - keep(fieldUser); - } - } - } - - // what it produces - a field's reference, a value read out of it, a bound method - may - // still point into what the record borrows - for (auto result : user->getResults()) - { - push(result, false); - } - } - } - } - - // The name the record goes by: the local that holds it, or a folded `const`'s. - llvm::StringRef recordName(mlir::Value value) - { - llvm::StringRef name = ownerName(value); - forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { - if (auto varOp = mlir::dyn_cast(user); varOp && varOp.getInitializer() == used) - { - name = varName(varOp); - } - }); - - return name; - } - - // What a borrowed field holds, for an error: `'ray'`, `'this.pos'`, `'livec'`, ... - std::string describeRecordSource(BorrowedField field) - { - auto value = field.store.getValue(); - auto quoted = [](llvm::StringRef name) { return "'" + name.str() + "'"; }; - switch (field.source) - { - case RecordFieldSource::Param: - return quoted(paramName(borrowedParam(value))); - case RecordFieldSource::Place: - if (auto loadOp = rootOf(value).getDefiningOp(); loadOp && isPlace(loadOp.getReference())) - { - return describePlace(loadOp.getReference()); - } - - return describeCall(rootOf(value).getDefiningOp()); - case RecordFieldSource::Slot: - return quoted(varName(slotLoadOf(value).getReference().getDefiningOp())); - case RecordFieldSource::Cell: - return "a captured variable"; - default: - return "a value it does not own"; - } - } - - // A record this function makes (spec 32): kept, it is an error; returned, each field it borrows - // must be a parameter's that this function's facts lend (`__own_result_record`), so that its - // callers bound it; and no use of it may come after what a field borrows may be released or - // overwritten - an owning local's ends, and whatever may assign a captured variable. A parameter - // and `this` are the caller's for the whole call; a read out of a place has the record's uses as - // its own (checkPlaceRead). - void checkBorrowingRecords() - { - for (auto &record : records) - { - auto name = recordName(record->interfaces.empty() ? record->box.getResult() : record->interfaces.front()); - if (record->walk.kept) - { - reportPlaceBorrowEscapes(record->walk.kept, name, - record->borrowed.empty() ? "a value it does not own" - : describeRecordSource(record->borrowed.front())); - continue; - } - - auto ok = true; - for (auto *returned : record->walk.returns) - { - for (auto &field : record->borrowed) - { - if (!ok || field.source == RecordFieldSource::Immortal) - { - continue; - } - - auto param = field.source == RecordFieldSource::Param || field.source == RecordFieldSource::Place - ? borrowedParam(field.store.getValue()) - : -1; - if (param < 0 || !llvm::is_contained(returnsRecordOf, param)) - { - reportPlaceBorrowEscapes(returned, name, describeRecordSource(field)); - ok = false; - } - } - } - - for (auto &field : record->borrowed) - { - if (!ok) - { - break; - } - - auto value = field.store.getValue(); - Chain chain; - if (field.source == RecordFieldSource::Slot) - { - chain.roots.push_back({slotLoadOf(value).getReference(), Chain::Slot}); - } - else if (auto load = field.source == RecordFieldSource::Param ? keptParamLoadOf(value) : mlir_ts::LoadOp()) - { - // a parameter this function keeps is its own to move away, as a local's value is - chain.roots.push_back({load.getReference(), Chain::Slot}); - } - else if (field.source == RecordFieldSource::Cell) - { - chain.roots.push_back({rootOf(value), Chain::NotOwned}); - } - else - { - continue; - } - - auto ends = rootEnds(chain); - if (field.source == RecordFieldSource::Cell) - { - for (auto *drop : drops) - { - if (dropsChain(drop, chain, nullptr, value)) - { - ends.push_back(drop); - } - } - } - - for (auto *end : ends) - { - for (auto &use : record->walk.uses) - { - if (ok && (use.op != end || !use.callee) && reachableAfter(end, use.op, use.kills)) - { - if (field.source == RecordFieldSource::Slot) - { - reportBorrowOutlives(use.op, end, name, varName(chain.roots.front().first.getDefiningOp())); - } - else - { - reportPlaceBorrowOutlives(use.op, end, name, describeRecordSource(field)); - } - - ok = false; - } - } - } - } - } - } - - // A call whose result is a borrowing record (OWN_RESULT_RECORD_ATTR_NAME, spec 32): the box is - // this function's to own - it is fresh, and its own check decides its moves and releases - but - // it may hold a borrow of each argument the facts name, as a call's result that borrows them - // does (checkPlaceRead): somewhere under each. So no use of it, through the locals it moves - // into, may come after one of those arguments may be released or overwritten, and it may not be - // kept. Returned, the arguments must be parameters this function's own facts lend. Its releases - // are not uses: they free only what the record owns. - void checkRecordResults() - { - getFunction().walk([&](mlir::Operation *call) { - if (!isCall(call) || !isRecordResult(call) || call->getNumResults() != 1) - { - return; - } - - auto result = call->getResult(0); - auto place = describeCall(call); - auto name = recordName(result); - RecordWalk walk; - llvm::SmallVector kills{call}; - walkRecord({result}, kills, walk); - if (walk.kept) - { - reportPlaceBorrowEscapes(walk.kept, name, place); - return; - } - - auto args = callArgs(call); - for (auto *returned : walk.returns) - { - for (auto index : resultBorrowsAll(call)) - { - if (static_cast(index) >= args.size() || argumentHoldsNoBlock(args[index]) || - isImmortalLiteral(args[index])) - { - continue; - } - - auto param = borrowedParam(args[index]); - if (param < 0 || !llvm::is_contained(returnsRecordOf, param)) - { - reportPlaceBorrowEscapes(returned, name, place); - return; - } - } - } - - auto chain = chainOf(call); - // rc's retain of a fresh argument right before the call is no end: the box's own check - // decides it, and the use that takes it is an end of its own - llvm::SmallVector ends; - for (auto *end : rootEnds(chain)) - { - auto ownedRetain = mlir::isa(end) && llvm::any_of(chain.roots, [&](auto &root) { - return root.second == Chain::Owned && rootOf(end->getOperand(0)) == root.first; - }); - if (!ownedRetain) - { - ends.push_back(end); - } - } - - for (auto *drop : drops) - { - if (drop != call && dropsChain(drop, chain, call)) - { - ends.push_back(drop); - } - } - - for (auto *end : ends) - { - for (auto &use : walk.uses) - { - if ((use.op != end || !use.callee) && reachableAfter(end, use.op, use.kills)) - { - reportPlaceBorrowOutlives(use.op, end, name, place); - return; - } - } - } - }); - } - - // ---- Reads out of containers (spec 2.3, phase 3) ---- - // - // `h.c` and `arr[i]` read a value the container owns. Such a read is a borrow: it may not be - // kept, and none of its uses - nor any use of what it produces - may run after something that - // may destroy it (spec 2.2's dropping mutations): an overwrite of a place on the way from the - // read up to its root, an array op that removes elements, an end of the root itself, or a call - // that may reach the root. A path back to a use through the read itself is a new borrow. - - // A read of a heap value out of a field or an element, or a call whose result borrows an - // argument (spec 2.4, phase 4), seen through its views. Null for anything else. - mlir::Operation *placeReadOf(mlir::Value value) - { - auto root = rootOf(value); - auto *def = root.getDefiningOp(); - // a handle read out of a place that rc gives an owner of its own is a counted copy, not a - // borrow (spec 23.3); any other is a borrow of the place, as a field read is - if (!def || !ownsHeap(root) || (isHandle(root) && isCountedHandleRead(root))) - { - return nullptr; - } - - if (auto loadOp = mlir::dyn_cast(def); loadOp && isPlace(loadOp.getReference())) - { - // a field of a generator's borrowing box: only its maker fills it, and nothing a call - // can reach overwrites it; it is the caller's, as a parameter is (checkBorrowedReads) - if (isBorrowingBoxField(loadOp.getReference())) - { - return nullptr; - } - - return def; - } - - return borrowingCall(root); - } - - // How a handle read out of a place is used (spec 23.3), seen through casts, merges and locals - // that own nothing. Every use is classified, and nothing falls through (Ruling 9): - // - `counted`: a receiver that takes a count rc made for that very use - // (isCountedHandleReceiver) - a store, a push or an unshift, `new Shared`, an array - // literal's element, a return, an `any` box, or an owning local's initializer; - // - passed on: a cast or a view that is still a handle, a merge, a local that owns nothing - - // whose own uses are classified in turn; - // - `borrowed`: everything else. A use that reads through the place (`f(p, p.child)`, a - // `.value` read, `Shared.count`), and every use that is neither of the above: a tuple's - // element, an object literal's field, a capture, a `yield`, a receiver rc made no retain - // for, any op not named here. The read is then a borrow, and tenure-checked - // (checkPlaceRead). - // rc's retains count for nothing on their own: each is the count of the receiver right after - // it. `cur = cur.value.next` is counted only. - struct HandleReadUses - { - bool counted; - bool borrowed; - }; - - static HandleReadUses handleReadUses(mlir::Value value) - { - HandleReadUses result{false, false}; - llvm::SmallVector values{value}; - llvm::SmallPtrSet seen{value}; - while (!values.empty()) - { - auto current = values.pop_back_val(); - for (auto &use : current.getUses()) - { - auto *user = use.getOwner(); - if (mlir::isa(user)) - { - continue; - } - - // a return and an `any` box take the value, though passesOn would hand them on - if (isHandOff(user, current)) - { - (isCountedHandleReceiver(user, current) ? result.counted : result.borrowed) = true; - continue; - } - - llvm::SmallVector same; - if (passesOn(use, same)) - { - for (auto next : same) - { - // what is no longer a handle is no receiver: a test (`if (p.child)`) only - // reads it - if (!isHandle(next)) - { - result.borrowed = true; - } - else if (seen.insert(next).second) - { - values.push_back(next); - } - } - - continue; - } - - (isCountedHandleReceiver(user, current) ? result.counted : result.borrowed) = true; - } - } - - return result; - } - - // A handle read out of a place that rc gives a count of its own is a counted copy, not a borrow - // (spec 23.3) - but only when every use of it is one rc counts (Ruling 7), and a use that is - // not known to be one is not (Ruling 9). A read with one use that reads through the place as - // well (`f(p, q, q.child = p.child)`), or that keeps it with no count (`[(q.child = p.child), - // 1]`), is a borrow, and that use is tenure-checked; the counted uses are copies - // (checkPlaceRead). - static bool isCountedHandleRead(mlir::Value value) - { - auto uses = handleReadUses(value); - return uses.counted && !uses.borrowed; - } - - // A use that gives a handle an owner rc counts, with rc's retain for that very use: rc's - // retain itself, a store into a field, an element, a global, an owning local or the result - // slot (a return) that rc retained for (countedStoreRetain); a push or an unshift, `new - // Shared`, an array literal's element, an `any` box, a `ts.ReturnVal`, each right after rc's - // retain of what it takes (retainedRightBefore); or the initializer of an owning local, which - // takes a count of its own (`ts.RetainSlot`). Any of them without its retain is not one: it - // keeps the read without a count (`{ s: (q.child = p.child) }`'s field). - static bool isCountedHandleReceiver(mlir::Operation *user, mlir::Value value) - { - if (mlir::isa(user)) - { - return true; - } - - if (auto varOp = mlir::dyn_cast(user)) - { - return isOwningVariable(varOp) && llvm::any_of(varOp->getUsers(), [](mlir::Operation *varUser) { - return mlir::isa(varUser); - }); - } - - if (auto storeOp = mlir::dyn_cast(user)) - { - return storeOp.getValue() == value && countedStoreRetain(storeOp); - } - - if (auto pushOp = mlir::dyn_cast(user)) - { - return retainedRightBefore(user, pushOp.getItems(), value); - } - - if (auto unshiftOp = mlir::dyn_cast(user)) - { - return retainedRightBefore(user, unshiftOp.getItems(), value); - } - - if (auto castOp = mlir::dyn_cast(user)) - { - return mlir::isa(castOp.getType()) && retainedRightBefore(user, user->getOperands(), value); - } - - // `ts.ReturnVal` is the return before the affine lowering, which makes it a store into - // the result slot - if (mlir::isa(user)) - { - return retainedRightBefore(user, user->getOperands(), value); - } - - return false; - } - - // A use that passesOn hands on but that takes the value: a return's store into the function's - // result slot (isResultSlot), and a cast that boxes it into `any`. - static bool isHandOff(mlir::Operation *user, mlir::Value value) - { - if (auto storeOp = mlir::dyn_cast(user)) - { - auto slot = storeOp.getReference().getDefiningOp(); - return storeOp.getValue() == value && slot && isResultSlot(slot); - } - - auto castOp = mlir::dyn_cast(user); - return castOp && castOp.getIn() == value && mlir::isa(castOp.getType()); - } - - // A return or an `any` box of a handle that rc counts (walkBorrowed). - static bool isCountedHandOff(mlir::Operation *user, mlir::Value value) - { - return isHandOff(user, value) && isCountedHandleReceiver(user, value); - } - - // rc's retains for what `op` takes: MLIRGen puts them in a row right before it - // (mlirGenRetainCaptured, retainInsertedElements), one for each operand it does not take - // over. Answers whether that row retains `value` once for each time `operands` hold it. A - // retain is right before one op only, so no other receiver can count it as well. - static bool retainedRightBefore(mlir::Operation *op, mlir::ValueRange operands, mlir::Value value) - { - auto needed = llvm::count(operands, value); - if (needed == 0) - { - return false; - } - - for (auto *previous = op->getPrevNode(); needed > 0 && previous; previous = previous->getPrevNode()) - { - auto retainOp = mlir::dyn_cast(previous); - if (!retainOp) - { - break; - } - - if (retainOp.getReference() == value) - { - --needed; - } - } - - return needed == 0; - } - - // The releases of the old value that rc's counted stores of a handle read make: `ts.Retain` of - // the value, `ts.ReleaseSlot` of the slot, `ts.Store` of the value into it, in a row. Seen - // through the read's casts, merges and locals that own nothing, as handleReadUses sees it. - static void collectCountedStoreReleases(mlir::Value value, llvm::SmallPtrSetImpl &releases) - { - llvm::SmallVector values{value}; - llvm::SmallPtrSet seen{value}; - while (!values.empty()) - { - auto current = values.pop_back_val(); - for (auto &use : current.getUses()) - { - llvm::SmallVector same; - if (passesOn(use, same)) - { - for (auto next : same) - { - if (isHandle(next) && seen.insert(next).second) - { - values.push_back(next); - } - } - - continue; - } - - auto storeOp = mlir::dyn_cast(use.getOwner()); - if (!storeOp || storeOp.getValue() != current || !countedStoreRetain(storeOp)) - { - continue; - } - - if (auto releaseSlotOp = mlir::dyn_cast_or_null(storeOp->getPrevNode())) - { - releases.insert(releaseSlotOp); - } - } - } - } - - // rc's retain for a store of a value, when it made one: `ts.Retain` of the value right before - // the store, or before the `ts.ReleaseSlot` of the old value right before it - - // `Retain(v)`, [`ReleaseSlot(slot)`], `Store(v, slot)`. A return's store into the result - // slot comes after the scope exit's releases of the frame's locals and their slots: - // `Retain(v)`, [`Release(..)`, `ReleaseSlot(..)`...], `Store(v, result)`. Null for a store rc - // counts nothing for. - static mlir::Operation *countedStoreRetain(mlir_ts::StoreOp storeOp) - { - auto *previous = storeOp->getPrevNode(); - if (auto slot = storeOp.getReference().getDefiningOp(); slot && isResultSlot(slot)) - { - // the scope exit's releases, of locals and of their slots, in any order - while (mlir::isa_and_nonnull(previous) && - previous->getOperand(0) != storeOp.getValue()) - { - previous = previous->getPrevNode(); - } - } - else if (auto releaseSlotOp = mlir::dyn_cast_or_null(previous)) - { - previous = releaseSlotOp.getSlot() == storeOp.getReference() ? releaseSlotOp->getPrevNode() : nullptr; - } - - auto retainOp = mlir::dyn_cast_or_null(previous); - return retainOp && retainOp.getReference() == storeOp.getValue() ? retainOp.getOperation() : nullptr; - } - - // The call a value is the result of, when that result is a plain borrow of an argument. A borrowing - // record (spec 32) is not one: the caller owns the box (checkRecordResults). - static mlir::Operation *borrowingCall(mlir::Value value) - { - auto *def = value.getDefiningOp(); - if (!def || !isCall(def) || resultBorrows(def) < 0) - { - return nullptr; - } - - auto args = callArgs(def); - auto indices = resultBorrowsAll(def); - return llvm::all_of(indices, [&](int index) { return static_cast(index) < args.size(); }) ? def : nullptr; - } - - // The places from a read up to its roots, and the roots: the values the function reached the - // first container through. A container reached through a merge (`(c ? a : b).x`) has a root - // on each way in. - struct Chain - { - enum Kind - { - Slot, // an owning local, or a value-type local reached by reference - Owned, // a value this function made - NotOwned // a parameter, `this`, a global, anything else - }; - - // a field's position and reference type; an element is position -1, a handle's payload - // (`s.value`) is SharedValue, a field reached through an interface is InterfaceField: its - // position in the object is not known here (spec 28) - llvm::SmallVector> places; - static constexpr int64_t SharedValue = -2; - static constexpr int64_t InterfaceField = -3; - llvm::SmallVector> roots; - // The roots that are handles - what a `s.value` was read through - each also NotOwned - // among `roots` (spec 23.3). Known from the op, not the root's type: a view may hide it. - llvm::SmallVector handles; - // Somewhere under a root, but where is not known: a result that borrows an argument may - // be any field or element under it. Every overwrite and every removal drops it. - bool anyPlace = false; - - bool owned() const - { - return llvm::none_of(roots, [](auto &root) { return root.second == NotOwned; }); - } - }; - - // With `place`, the chain of that place itself: where a store into it moves a value (spec 29). - Chain chainOf(mlir::Operation *read, mlir::Value place = {}) - { - Chain chain; - llvm::SmallVector refs; - llvm::SmallVector first; - if (place) - { - refs.push_back(place); - } - else if (auto loadOp = mlir::dyn_cast(read)) - { - refs.push_back(loadOp.getReference()); - } - else - { - // a call's result that borrows its arguments - a plain borrow, or a borrowing record's box - // (spec 32), which may hold a borrow of each - is somewhere under each of them - chain.anyPlace = true; - auto args = callArgs(read); - for (auto index : resultBorrowsAll(read)) - { - if (static_cast(index) < args.size() && !argumentHoldsNoBlock(args[index]) && - !isImmortalLiteral(args[index])) - { - first.push_back(rootOf(args[index])); - } - } - } - - llvm::SmallPtrSet seen; - // the bases a `s.value` was read through, and what merges into them - llvm::SmallPtrSet handleBases; - while (!refs.empty() || !first.empty()) - { - llvm::SmallVector bases; - if (!first.empty()) - { - bases.swap(first); - } - else - { - auto ref = refs.pop_back_val(); - mlir::Value base; - if (auto propertyRefOp = ref.getDefiningOp()) - { - chain.places.push_back({propertyRefOp.getPosition(), propertyRefOp.getType()}); - base = propertyRefOp.getObjectRef(); - } - else if (auto sharedValueRefOp = ref.getDefiningOp()) - { - chain.places.push_back({Chain::SharedValue, sharedValueRefOp.getType()}); - base = sharedValueRefOp.getShared(); - handleBases.insert(rootOf(base)); - } - else if (auto interfaceRefOp = ref.getDefiningOp()) - { - chain.places.push_back({Chain::InterfaceField, interfaceRefOp.getType()}); - base = interfaceRefOp.getInterfaceVal(); - } - else - { - auto elementRefOp = ref.getDefiningOp(); - chain.places.push_back({-1, elementRefOp.getType()}); - base = elementRefOp.getArray(); - } - - bases.push_back(rootOf(base)); - } - - while (!bases.empty()) - { - auto base = bases.pop_back_val(); - if (!seen.insert(base).second) - { - continue; - } - - // A handle (spec 23.3): other handles may reach its block, so this function owns - // nothing under it. A handle read out of a place still has that place above it, and - // one merged from several has each of them. - if (handleBases.contains(base) || isHandle(base)) - { - llvm::SmallVector merged; - if (auto argument = mlir::dyn_cast(base); argument && mergedInto(argument, merged)) - { - handleBases.insert(merged.begin(), merged.end()); - bases.append(merged.begin(), merged.end()); - continue; - } - - chain.roots.push_back({base, Chain::NotOwned}); - chain.handles.push_back(base); - if (auto loadOp = base.getDefiningOp(); loadOp && isPlace(loadOp.getReference())) - { - refs.push_back(loadOp.getReference()); - } - - continue; - } - - if (isPlace(base)) - { - refs.push_back(base); // a field of a value-type field: the same object - continue; - } - - if (auto loadOp = base.getDefiningOp(); loadOp && isPlace(loadOp.getReference())) - { - refs.push_back(loadOp.getReference()); - continue; - } - - // the object a method is called on - if (auto object = boundThis(base)) - { - bases.push_back(rootOf(object)); - continue; - } - - // a result that borrows an argument is somewhere under the argument - if (auto *call = borrowingCall(base)) - { - chain.anyPlace = true; - auto args = callArgs(call); - for (auto index : resultBorrowsAll(call)) - { - if (static_cast(index) < args.size()) - { - bases.push_back(rootOf(args[index])); - } - } - - continue; - } - - if (auto argument = mlir::dyn_cast(base); argument && mergedInto(argument, bases)) - { - continue; - } - - if (base.getDefiningOp()) - { - chain.roots.push_back({base, Chain::Slot}); - } - else if (auto slotLoad = slotLoadOf(base)) - { - chain.roots.push_back({slotLoad.getReference(), Chain::Slot}); - } - else - { - chain.roots.push_back({base, isFresh(base) ? Chain::Owned : Chain::NotOwned}); - } - } - } - - return chain; - } - - // Where the roots stop holding what the chain reads: a local's releases and assignments and - // every declaration or retain that may move it away; a made value's releases and every use - // that takes it; a global's assignments. - llvm::SmallVector rootEnds(const Chain &chain) - { - llvm::SmallVector ends; - for (auto [root, kind] : chain.roots) - { - // A handle (spec 23.3), never owned: where this one is given back or overwritten - a - // local's release and assignments, a temporary's release; a global's assignments are - // below. A copy of it is counted, and moves nothing away. - if (llvm::is_contained(chain.handles, root)) - { - auto loadOp = root.getDefiningOp(); - if (auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp()) - { - for (auto *user : varOp->getUsers()) - { - if (mlir::isa(user)) - { - ends.push_back(user); - } - } - } - else if (!loadOp) - { - forEachUse(root, [&](mlir::Operation *use, mlir::Value) { - if (mlir::isa(use)) - { - ends.push_back(use); - } - }); - } - } - - if (kind == Chain::Slot) - { - for (auto *user : root.getUsers()) - { - if (mlir::isa(user)) - { - ends.push_back(user); - } - else if (auto loadOp = mlir::dyn_cast(user)) - { - forEachUse(loadOp.getResult(), [&](mlir::Operation *use, mlir::Value used) { - auto varOp = mlir::dyn_cast(use); - if (mlir::isa(use) || (varOp && isOwningVariable(varOp)) || - (isCall(use) && isKeptArgument(use, used))) - { - ends.push_back(use); - } - }); - } - } - } - else if (kind == Chain::Owned) - { - forEachUse(root, [&](mlir::Operation *use, mlir::Value used) { - if (mlir::isa(use) || !isBorrow(use, used)) - { - ends.push_back(use); - } - }); - } - else if (auto loadOp = root.getDefiningOp()) - { - if (auto global = loadOp.getReference().getDefiningOp()) - { - getFunction().walk([&](mlir::Operation *op) { - mlir::Value slot; - if (auto releaseSlotOp = mlir::dyn_cast(op)) - { - slot = releaseSlotOp.getSlot(); - } - else if (auto storeOp = mlir::dyn_cast(op)) - { - slot = storeOp.getReference(); - } - - auto other = slot ? slot.getDefiningOp() : mlir_ts::AddressOfOp(); - if (other && other.getGlobalName() == global.getGlobalName()) - { - ends.push_back(op); - } - }); - } - } - } - - return ends; - } - - // What a call has to be given to reach a root: the root, and where the root is a local - // declared from another (a borrow, or a move that left the other dead), that one too. - static llvm::SmallVector rootSources(const Chain &chain) - { - llvm::SmallVector sources; - for (auto &root : chain.roots) - { - auto value = root.first; - sources.push_back(value); - for (auto steps = 0; steps < 8; ++steps) - { - auto varOp = value.getDefiningOp(); - if (!varOp || !varOp.getInitializer()) - { - break; - } - - auto initializer = rootOf(varOp.getInitializer()); - auto slotLoad = slotLoadOf(initializer); - value = slotLoad ? slotLoad.getReference() : initializer; - sources.push_back(value); - } - } - - return sources; - } - - // Everything that may point into what `start` points to: the forward closure over users' - // results that may point anywhere. A call's result is not followed: the call hands back a - // value it owns. - const llvm::DenseSet &derivedFrom(mlir::Value start) - { - if (auto found = derivedCache.find(start); found != derivedCache.end()) - { - return *found->second; - } - - auto &slot = derivedCache[start]; - slot = std::make_shared>(); - auto &derived = *slot; - llvm::SmallVector work{start}; - while (!work.empty()) - { - auto value = work.pop_back_val(); - if (!derived.insert(value).second) - { - continue; - } - - for (auto &use : value.getUses()) - { - auto *user = use.getOwner(); - if (isCall(user)) - { - // ...but a borrowing record given this value holds it (spec 32) - auto args = callArgs(user); - if (isRecordResult(user) && llvm::any_of(resultBorrowsAll(user), [&](int index) { - return static_cast(index) < args.size() && args[index] == value; - })) - { - work.push_back(user->getResult(0)); - } - - continue; - } - - llvm::SmallVector next(user->getResults().begin(), user->getResults().end()); - passesOn(use, next); - for (auto result : next) - { - if (mayPointInto(result)) - { - work.push_back(result); - } - } - } - } - - return derived; - } - - // May this op of the function's drops destroy what the chain reads? An overwrite of a place - // on it, matched by position and type (two references to one field of one object always - // agree on both; an unrelated field that happens to agree only adds an error); an array op - // that removes elements of the type of an element on it; a call whose callee may destroy - // anything (`__own_no_drops`, phase 4) - any such call, for a root this function does not - // own, since the owner may be a global the callee assigns; else one given something derived - // from the root that the read did not produce. - // `borrowed`, when given, is the value the borrow holds in place of the read's result (a value - // moved into the place, spec 29), and `read` may then be null. - bool dropsChain(mlir::Operation *drop, const Chain &chain, mlir::Operation *read, mlir::Value borrowed = {}) - { - if (read && drop == read) - { - return false; // a call that returns a borrow has not destroyed it by returning it - } - - if (chain.anyPlace && !isCall(drop)) - { - return true; - } - - if (auto releaseSlotOp = mlir::dyn_cast(drop)) - { - // An assignment to a captured variable destroys its old value, which is reached only - // through a read of the variable's cell: a root this function does not own. - auto slot = releaseSlotOp.getSlot(); - if (isCellVariable(slot) || isLoadedCell(slot)) - { - return !chain.owned(); - } - - // An assignment to a global destroys its old value and all that value owns. A root this - // function does not own - `this`, a parameter, a global's or a field's value - may be - // that very block, or under it (spec 29). A root this function made cannot. An immortal - // global's one store is its initializer, whose old value is the empty one (spec 25). - if (auto addressOfOp = slot.getDefiningOp()) - { - return !addressOfOp->hasAttr(OWN_IMMORTAL_GLOBAL_ATTR_NAME) && !chain.owned(); - } - - // `x.value = ...` through any handle, of any type, may replace the payload another - // handle reads (spec 23.3): a handle converts to one of a base class's type - if (slot.getDefiningOp()) - { - return llvm::any_of(chain.places, [&](auto &place) { return place.first == Chain::SharedValue; }); - } - - auto isField = [](int64_t position) { return position >= 0 || position == Chain::InterfaceField; }; - int64_t position = -1; - if (auto propertyRefOp = slot.getDefiningOp()) - { - position = propertyRefOp.getPosition(); - } - else if (isInterfaceField(slot)) - { - position = Chain::InterfaceField; - } - - // A field reached through an interface may be any field of the object under it, of any - // type (a class may declare the field narrower than the interface): an overwrite through - // an interface drops every field on the chain, and any field's overwrite drops a field - // read through an interface (spec 28). - return llvm::any_of(chain.places, [&](auto &place) { - if (isField(position) && isField(place.first) && - (position == Chain::InterfaceField || place.first == Chain::InterfaceField)) - { - return true; - } - - return place.first == position && place.second == slot.getType(); - }); - } - - if (isCall(drop)) - { - if (callNoDrops(drop)) - { - return false; - } - - // a call given a closure may run it, and its box may hold anything - if (!chain.owned() || llvm::any_of(drop->getOperands(), holdsCaptures)) - { - return true; - } - - auto &fromRead = derivedFrom(borrowed ? borrowed : read->getResult(0)); - auto sources = rootSources(chain); - return llvm::any_of(drop->getOperands(), [&](mlir::Value operand) { - return !fromRead.contains(operand) && llvm::any_of(sources, [&](mlir::Value source) { - return derivedFrom(source).contains(operand); - }); - }); - } - - // pop, shift, splice, `length =`; push and unshift on an array reached through a handle - auto refType = mlir::dyn_cast(drop->getOperand(0).getType()); - auto arrayType = refType ? mlir::dyn_cast(refType.getElementType()) : mlir_ts::ArrayType(); - if (!arrayType) - { - return true; - } - - return llvm::any_of(chain.places, [&](auto &place) { - auto placeType = mlir::dyn_cast(place.second); - return place.first == -1 && placeType && placeType.getElementType() == arrayType.getElementType(); - }); - } - - // A read out of a container is a borrow of it (spec 2.3), and so is a `let` declared from one: - // its retain and releases go when the check passes, as for any borrowing `let` (phase 2). - // 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) - { - auto loadOp = mlir::dyn_cast(read); - auto place = loadOp ? describePlace(loadOp.getReference()) : describeCall(read); - auto result = read->getResult(0); - // A handle read rc counts (spec 23.3): its counted uses are copies (walkBorrowed), and the - // release of the old value a counted store makes, right before it stores the retained - // read, cannot free the block read: it holds the retain's count by then - // (`Shared.count(q.child = p.child)`). - auto countedHandle = isHandle(result) && handleReadUses(result).counted; - llvm::SmallPtrSet countedStoreReleases; - if (countedHandle) - { - collectCountedStoreReleases(result, countedStoreReleases); - } - - llvm::StringRef name = ownerName(result); - 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; - } - - name = varName(borrower); - if (auto *keeps = collectBorrowerUses(borrower, kills, uses, bookkeeping); keeps && !letKept) - { - letKept = keeps; - letKeeps = borrower; - } - - return true; - }, keeping, countedHandle); - - if (kept || letKept) - { - if (!kept && isAssignmentOf(letKept, letKeeps)) - { - reportPlaceBorrowerAssigned(letKept, varName(letKeeps), place); - } - else - { - reportPlaceBorrowEscapes(kept ? kept : letKept, name, place); - } - - return false; - } - - auto chain = chainOf(read); - // A call given the borrow is a use of it for as long as the callee runs, so a call that - // may destroy it is a use after that, whatever the order of its arguments. The function - // value it goes through is only read as it starts. - auto outlives = [&](mlir::Operation *drop) { - for (auto &use : uses) - { - if ((use.op != drop || !use.callee) && reachableAfter(drop, use.op, use.kills)) - { - reportPlaceBorrowOutlives(use.op, drop, name, place); - return true; - } - } - - return false; - }; - - for (auto *end : rootEnds(chain)) - { - if (outlives(end)) - { - return false; - } - } - - if (keptParamOutlives(chain, outlives)) - { - return false; - } - - for (auto *drop : drops) - { - if (countedStoreReleases.contains(drop)) - { - continue; - } - - if (dropsChain(drop, chain, read) && outlives(drop)) - { - return false; - } - } - - toErase.insert(bookkeeping.begin(), bookkeeping.end()); - return true; - } - - // A local that holds a borrow through `user`, and the field path under it that holds it (empty: - // the local's value itself): a store of it into the local, or into a field of the local's value - // tuple at any depth (an object literal built in place), or the local declared from it. - struct BorrowHolder - { - mlir_ts::VariableOp local; - llvm::SmallVector path; - }; - - static std::optional borrowHolderOf(mlir::Operation *user) - { - BorrowHolder holder; - mlir::Value ref; - if (auto storeOp = mlir::dyn_cast(user)) - { - ref = storeOp.getReference(); - while (auto propertyRefOp = ref.getDefiningOp()) - { - holder.path.insert(holder.path.begin(), propertyRefOp.getPosition()); - ref = propertyRefOp.getObjectRef(); - } - } - else if (auto varOp = mlir::dyn_cast(user)) - { - ref = varOp.getResult(); - } - - auto varOp = ref ? ref.getDefiningOp() : mlir_ts::VariableOp(); - if (!varOp || varOp.getCaptured().value_or(false) || isCellVariable(varOp.getResult())) - { - return std::nullopt; - } - - holder.local = varOp; - return holder; - } - - // Where the holder is given a new value: the local's declaration when it has an initializer - // (`let l!: V` stores nothing, and in a loop the slot keeps the last round's value: fix-up - // re-review N4), and every store into the field path that holds the borrow or into a prefix of - // it (the whole local, `o.p` for `o.p.v`). A store into a sibling field, or below the path, - // gives the path no new value. - static llvm::SmallVector holderRenewals(BorrowHolder holder) - { - llvm::SmallVector renewals; - if (holder.local.getInitializer()) - { - renewals.push_back(holder.local); - } - - llvm::SmallVector> refs{{holder.local.getResult(), 0}}; - while (!refs.empty()) - { - auto [ref, depth] = refs.pop_back_val(); - for (auto *refUser : ref.getUsers()) - { - if (auto propertyRefOp = mlir::dyn_cast(refUser)) - { - if (depth < holder.path.size() && propertyRefOp.getPosition() == holder.path[depth]) - { - refs.push_back({propertyRefOp.getResult(), depth + 1}); - } - } - else if (auto storeOp = mlir::dyn_cast(refUser); storeOp && storeOp.getReference() == ref) - { - renewals.push_back(storeOp); - } - } - } - - return renewals; - } - - // Whether one field path is on the way to the other: a read of either may reach the other's value. - static bool pathsMeet(llvm::ArrayRef a, llvm::ArrayRef b) - { - auto common = std::min(a.size(), b.size()); - return a.take_front(common) == b.take_front(common); - } - - // A read of what a mortal global holds (spec 25: an immortal one's one store is its - // initializer): a heap value it owns, not a handle (spec 23.3 counts those). - bool isGlobalRead(mlir_ts::LoadOp loadOp) - { - auto addressOfOp = loadOp.getReference().getDefiningOp(); - auto result = loadOp.getResult(); - return addressOfOp && !addressOfOp->hasAttr(OWN_IMMORTAL_GLOBAL_ATTR_NAME) && ownsHeap(result) && !isHandle(result); - } - - // Such a read is a borrow of the value the global owns - `const c = K.w` names the read itself, - // with no retain - and an assignment of the global releases it. So no use of the read, nor of - // what it produces (walkBorrowed), may run after one in this function. A path back to a use - // through the read itself is a new read. A call that may assign the global, itself or through - // its callees, is such an assignment too (follow-up 11: `const c = G; reset(); print(c.x)`); the - // read given to that call as an argument is #639's. (C1 of the fix-up review: `const c = K.w; K.w = new V(i + 1); print(c.x)` in a loop - // read the block the assignment freed.) - // - // Where the walk meets a keep, the read is either followed into what holds it or rejected - - // never let go unchecked (fix-up re-review N2: a kept read is not always another check's). A - // local holds it (borrowHolderOf): an object literal built in place (`const o = { v: c }`, - // `{ p: { v: K.w } }`), a `let` or a destructured local declared or assigned from it - // (`let l = o.v`, `let { v } = o`). Every use of that local under the path that holds the borrow - // - its reads, a copy, a spread (`ts.DeconstructTuple`), what those lead to - is a use of the - // read, made again only where the local is given a new value (holderRenewals): never at the - // read, which renews `c` and not `o.v` - a holder declared before a loop keeps the old borrow - // across the back edge until it is stored again (fix-up re-review N3). The local's own retain - // and releases are its bookkeeping, which the owner checks decide. A sibling field never given - // the borrow is not followed. A value made from the borrow and stored into another local makes - // that one a holder too. Any other keep - a field of an object, an array, a global, a callee - // that keeps it, a capture, a return - cannot be bounded here and is an error. A string read is - // copied where it is kept (spec 22), so its keeps are not this check's. - void checkGlobalRead(mlir_ts::LoadOp read) - { - // each access of a global is a `ts.AddressOf` of its own - auto global = read.getReference().getDefiningOp().getGlobalName(); - llvm::SmallVector uses; - llvm::StringSet<> followed; - mlir::Operation *keptAt = nullptr; - // rc's temporary release of a call's result that borrows an argument goes with that result - // (walkBorrowed's `keeping`); the owner checks decide it - llvm::SmallVector bookkeeping; - Keeping keeping{&bookkeeping, false}; - std::function)> intoLocal; - - // the uses of what `holder` holds, made again at its renewals: those depend on the holder - // alone, not on the way the walk reached it, so a holder is followed once. A value loaded - // out of it is made again only by a path back through that load (the load itself is a use, - // bounded by the renewals): a store into the path after the load gives the loaded value - // nothing new. - auto followHolder = [&](BorrowHolder holder) { - std::string key; - llvm::raw_string_ostream keyOut(key); - keyOut << holder.local.getOperation(); - for (auto position : holder.path) - { - keyOut << "." << position; - } - - if (!followed.insert(keyOut.str()).second) - { - return true; - } - - auto localKills = holderRenewals(holder); - llvm::SmallVector>> refs{{holder.local.getResult(), {}}}; - while (!refs.empty()) - { - auto [ref, path] = refs.pop_back_val(); - for (auto *refUser : ref.getUsers()) - { - if (auto propertyRefOp = mlir::dyn_cast(refUser)) - { - auto fieldPath = path; - fieldPath.push_back(propertyRefOp.getPosition()); - if (pathsMeet(fieldPath, holder.path)) - { - refs.push_back({propertyRefOp.getResult(), fieldPath}); - } - - continue; - } - - if (ref == holder.local.getResult() && - mlir::isa(refUser)) - { - continue; - } - - uses.push_back({refUser, {localKills.begin(), localKills.end()}}); - if (auto loadOp = mlir::dyn_cast(refUser)) - { - if (auto *kept = walkBorrowed(loadOp.getResult(), loadOp.getOperation(), uses, intoLocal, keeping)) - { - keptAt = keptAt ? keptAt : kept; - return false; - } - } - } - } - - return true; - }; - - intoLocal = [&](mlir::Operation *user, llvm::ArrayRef kills) { - // a tuple taken apart (`{ ...b }`, a destructuring): each field is the borrow again - if (mlir::isa(user)) - { - for (auto result : user->getResults()) - { - if (!mayPointInto(result)) - { - continue; - } - - if (auto *kept = walkBorrowed(result, kills, uses, intoLocal, keeping)) - { - keptAt = keptAt ? keptAt : kept; - return false; - } - } - - return true; - } - - auto holder = borrowHolderOf(user); - return holder && followHolder(*holder); - }; - - auto *kept = walkBorrowed(read.getResult(), read.getOperation(), uses, intoLocal, keeping); - - // a value made from the borrow (a field of a tuple read out of a holder, `{ ...b }`) stored - // into another local, or declaring one: that local holds the borrow too - for (size_t index = 0; !kept && !keptAt && index < uses.size(); index++) - { - auto *op = uses[index].op; - auto storeOp = mlir::dyn_cast(op); - auto varOp = mlir::dyn_cast(op); - auto value = storeOp ? storeOp.getValue() : varOp ? varOp.getInitializer() : mlir::Value(); - if (!value || !mayPointInto(value)) - { - continue; - } - - if (auto holder = borrowHolderOf(op)) - { - followHolder(*holder); - } - } - - if (kept || keptAt) - { - if (mlir::isa(read.getType())) - { - return; - } - - if (reporting()) - { - auto *at = keptAt ? keptAt : kept; - at->emitError("the value read from '") - << global << "' is kept where its end at an assignment of '" << global - << "' cannot be checked; -mm=own cannot prove it outlives the global's value"; - signalPassFailure(); - } - - return; - } - - llvm::SmallVector ends; - getFunction().walk([&](mlir::Operation *op) { - mlir::Value slot; - if (auto releaseSlotOp = mlir::dyn_cast(op)) - { - slot = releaseSlotOp.getSlot(); - } - else if (auto storeOp = mlir::dyn_cast(op)) - { - slot = storeOp.getReference(); - } - - auto other = slot ? slot.getDefiningOp() : mlir_ts::AddressOfOp(); - if (other && other.getGlobalName() == global) - { - ends.push_back(op); - } - - // a call that may give the global a new value (OwnershipSignaturePass, pinAssignedGlobals): - // `const c = G; reset(); print(c.x)` - if (isCall(op)) - { - auto assigned = op->getAttrOfType(OWN_ASSIGNS_GLOBALS_ATTR_NAME); - if (op->hasAttr(OWN_ASSIGNS_ANY_GLOBAL_ATTR_NAME) || - (assigned && llvm::any_of(assigned.getAsRange(), - [&](mlir::StringAttr name) { return name.getValue() == global; }))) - { - ends.push_back(op); - } - } - }); - - for (auto *end : ends) - { - for (auto &use : uses) - { - if (use.op != end && reachableAfter(end, use.op, use.kills)) - { - reportBorrowOutlives(use.op, end, ownerName(read.getResult()), global); - return; - } - } - } - } - - // A borrow read out of a parameter this function keeps, taken before the parameter moves into a - // new owner (spec 33, review fix): from the move on it is bounded by that owner, as a read through - // the owner would be (spec 29) - by the owner's ends and the drops of its place after the move. - // `const inner = p.v; let r = p; r = new N(...); inner.x` reads what the assignment freed. A move - // into anything that is not such a place (a call that keeps it, a return, a push) ends the borrow - // there. So do the releases of the parameter's slot (spec 33's exit releases). True when some use - // of the borrow comes after one of these (`outlives` reported it). - bool keptParamOutlives(const Chain &chain, llvm::function_ref outlives) - { - for (auto &root : chain.roots) - { - auto load = keptParamLoadOf(root.first); - if (!load) - { - continue; - } - - // the moves of the parameter: what takes a read of its slot - llvm::SmallVector> takers; - for (auto *user : load.getReference().getUsers()) - { - if (mlir::isa(user)) - { - if (outlives(user)) - { - return true; - } - - continue; - } - - auto readOp = mlir::dyn_cast(user); - if (!readOp) - { - continue; - } - - forEachUse(readOp.getResult(), [&](mlir::Operation *use, mlir::Value used) { - auto varOp = mlir::dyn_cast(use); - if (mlir::isa(use)) - { - auto *taker = takerOf(use, used); - takers.push_back({taker ? taker : use, used}); - } - else if ((varOp && isOwningVariable(varOp)) || (isCall(use) && isKeptArgument(use, used))) - { - takers.push_back({use, used}); - } - }); - } - - for (auto [taker, moved] : takers) - { - Chain owner; - if (!destinationChain(taker, owner)) - { - if (outlives(taker)) - { - return true; - } - - continue; - } - - auto afterMove = [&](mlir::Operation *end) { - return end != taker && reachableAfter(taker, end, llvm::ArrayRef{}) && outlives(end); - }; - - for (auto *end : rootEnds(owner)) - { - if (afterMove(end)) - { - return true; - } - } - - for (auto *drop : drops) - { - if (dropsChain(drop, owner, nullptr, moved) && afterMove(drop)) - { - return true; - } - } - } - } - - return false; - } - - // A parameter, or a view of one (`u`, the payload `ts.Unbox` reads out of an `any`), that - // this function returns as a borrow (`__own_result_borrows`). rc retained it for the caller, - // who now takes no reference: the retain goes, and nothing may keep it. The caller's own - // borrow of the argument bounds it; nothing here can end that but a call, and a call that may - // is the caller's use of what it passed (spec 2.4, phase 4). - void checkReturnedParam(mlir::Value value, llvm::SetVector &toErase) - { - llvm::SmallVector uses; - llvm::SmallVector bookkeeping; - if (auto *kept = walkBorrowed(value, {}, uses, nullptr, Keeping{&bookkeeping, true})) - { - reportBorrowEscapes(kept, ownerName(value), "a parameter"); - return; - } - - toErase.insert(bookkeeping.begin(), bookkeeping.end()); - } - - // `the result of 'first'`, for a call whose result borrows an argument. - static std::string describeCall(mlir::Operation *call) - { - auto callee = calleeName(call); - return callee.empty() ? "a call's result" : "the result of '" + callee + "'"; - } - - // The function a call names - directly, or the method it calls through a vtable - or empty. - static std::string calleeName(mlir::Operation *call) - { - mlir::StringAttr callee; - auto value = calleeValue(call); - if (auto symbol = directCallee(call)) - { - callee = symbol.getAttr(); - } - else if (auto calleeOp = value ? value.getDefiningOp() : nullptr) - { - if (auto getMethodOp = mlir::dyn_cast(calleeOp)) - { - calleeOp = getMethodOp.getBoundFunc().getDefiningOp(); - } - - if (auto refOp = mlir::dyn_cast_or_null(calleeOp)) - { - callee = refOp.getIdentifierAttr().getAttr(); - } - else if (auto refOp = mlir::dyn_cast_or_null(calleeOp)) - { - callee = refOp.getIdentifierAttr().getAttr(); - } - else if (auto refOp = mlir::dyn_cast_or_null(calleeOp)) - { - callee = refOp.getIdentifierAttr().getAttr(); - } - else if (auto refOp = mlir::dyn_cast_or_null(calleeOp)) - { - // a method reached through an interface (spec 31): `.` - auto method = mlir::dyn_cast(refOp.getIdentifier()); - auto interfaceType = mlir::dyn_cast(refOp.getInterfaceVal().getType()); - if (method && interfaceType) - { - return (interfaceType.getName().getValue() + "." + method.getValue()).str(); - } - } - } - - return callee ? callee.getValue().str() : std::string(); - } - - // `'h.c'`, `'arr[]'`, `'h.c.v'` - or `a field`, `an element` when the root has no name (a - // temporary, or no --di). - std::string describePlace(mlir::Value ref) - { - auto path = pathOf(ref); - if (path.empty()) - { - return ref.getDefiningOp() ? "an element" : "a field"; - } - - return "'" + path + "'"; - } - - std::string pathOf(mlir::Value ref) - { - mlir::Value base; - std::string step; - if (auto propertyRefOp = ref.getDefiningOp()) - { - auto field = fieldName(propertyRefOp); - if (field.empty()) - { - return {}; - } - - base = propertyRefOp.getObjectRef(); - step = "." + field; - } - else if (auto elementRefOp = ref.getDefiningOp()) - { - base = elementRefOp.getArray(); - step = "[]"; - } - else if (auto sharedValueRefOp = ref.getDefiningOp()) - { - base = sharedValueRefOp.getShared(); - step = ".value"; - } - else if (auto interfaceRefOp = ref.getDefiningOp()) - { - auto field = mlir::dyn_cast(interfaceRefOp.getIdentifier()); - if (!field) - { - return {}; - } - - base = interfaceRefOp.getInterfaceVal(); - step = "." + field.getValue().str(); - } - else - { - return {}; - } - - base = rootOf(base); - std::string prefix; - if (isPlace(base)) - { - prefix = pathOf(base); - } - else if (auto loadOp = base.getDefiningOp()) - { - auto from = loadOp.getReference(); - if (isPlace(from)) - { - prefix = pathOf(from); - } - else if (auto varOp = from.getDefiningOp()) - { - prefix = varName(varOp).str(); - } - else if (auto global = from.getDefiningOp()) - { - prefix = global.getGlobalName().str(); - } - } - else if (auto varOp = base.getDefiningOp()) - { - prefix = varName(varOp).str(); - } - else - { - prefix = ownerName(base).str(); - } - - // a name MLIRGen made up (`.a`, the array a `for...of` walks) means nothing to the reader - if (prefix.empty() || prefix == "this value" || prefix.front() == '.') - { - return {}; - } - - return prefix + step; - } - - static std::string fieldName(mlir_ts::PropertyRefOp propertyRefOp) - { - auto type = propertyRefOp.getObjectRef().getType(); - if (auto refType = mlir::dyn_cast(type)) - { - type = refType.getElementType(); - } - - if (auto classType = mlir::dyn_cast(type)) - { - type = classType.getStorageType(); - } - - auto position = propertyRefOp.getPosition(); - mlir::Attribute id; - if (auto storageType = mlir::dyn_cast(type); storageType && position < storageType.size()) - { - id = storageType.getId(position); - } - else if (auto tupleType = mlir::dyn_cast(type); tupleType && position < tupleType.size()) - { - id = tupleType.getId(position); - } - - if (auto name = mlir::dyn_cast_or_null(id)) - { - return name.str(); - } - - if (auto name = mlir::dyn_cast_or_null(id)) - { - return name.getValue().str(); - } - - return {}; - } - - // The name of a temporary that owns: a folded `const` has no ts.Variable, but under --di it - // has a ts.DebugVariable, whose location carries the name. - static llvm::StringRef ownerName(mlir::Value value) - { - llvm::StringRef name = "this value"; - forEachUse(value, [&](mlir::Operation *user, mlir::Value) { - if (mlir::isa(user)) - { - name = nameAt(user->getLoc()); - } - }); - - return name; - } - - // A `ts.RetainSlot`'s slot holds its initializer where the retain is. MLIRGen puts the retain at - // the declaration (takeOwnershipOfLocal), and nothing between the two may write the slot: in - // their block, no op is given the slot, no call runs, and no op has regions (a store could hide - // in them). What is stored into the slot later is decided at its own store; this retain takes - // only the initializer. No closure captures it. - static bool slotHoldsInitializerAt(mlir::Operation *op) - { - auto slot = mlir::cast(op).getSlot(); - auto varOp = slot.getDefiningOp(); - if (!varOp || !varOp.getInitializer() || isCellVariable(slot) || varOp->getBlock() != op->getBlock()) - { - return false; - } - - for (auto *between = varOp->getNextNode(); between; between = between->getNextNode()) - { - if (between == op) - { - return true; - } - - if (llvm::is_contained(between->getOperands(), slot) || isCall(between) || between->getNumRegions() > 0) - { - return false; - } - } - - return false; - } - - // The value a retain acquires: a Retain's operand, or a RetainSlot's variable's initializer, - // 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) - { - if (auto retainOp = mlir::dyn_cast(op)) - { - return rootOf(retainOp.getReference()); - } - - auto varOp = mlir::cast(op).getSlot().getDefiningOp(); - return varOp && varOp.getInitializer() ? rootOf(varOp.getInitializer()) : mlir::Value(); - } - - // Does a value of this type own a block that a release would destroy? - bool ownsHeap(mlir::Value value) - { - // a `value_ref` points at storage it does not own, but one `ts.New` made and seen as its - // object is the object's block: the root of a generator's state object or an object literal - if (value.getDefiningOp() && mlir::isa(value.getType()) && - llvm::any_of(value.getUsers(), [](mlir::Operation *user) { return mlir::isa(user) && isView(user); })) - { - return true; - } - - return ownsHeap(value.getLoc(), value.getType()); - } - - bool ownsHeap(mlir::Location location, mlir::Type type) - { - MLIRTypeHelper mth(&getContext(), CompileOptions{}); - return mth.ownsHeapMemory(location, type); - } - - // A tuple read out of a local that a `ts.Constant` initializes, whose owning fields hold what - // that constant put there - null, a number, an immortal literal - or a fresh value moved in: - // the initial state a generator's maker copies into its state object, or a record built to be - // returned (`return {value: new C(), done: false}`, a `yield`'s result). rc's retain of it is - // its reference for the values moved in, which own moves instead (recordRetainOf). - bool holdsConstantsOrMoves(mlir::Value value) - { - auto loadOp = value.getDefiningOp(); - auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); - // a borrowing record (spec 32) takes nothing for what it borrows, and its fresh values move in - // as an owning record's do - if (!varOp || !isConstantRecordLocal(varOp) || !(isOwningRecord(value) || borrowingRecordOf(varOp))) - { - return false; - } - - // a fresh value moved in, its move decided by its own check against this retain - auto movesIn = llvm::any_of(varOp->getUsers(), [&](mlir::Operation *user) { - auto propertyRefOp = mlir::dyn_cast(user); - if (!propertyRefOp) - { - return false; - } - - // a method's field is a `!ts.bound_ref`, which holds a function, not a block - auto refType = mlir::dyn_cast(propertyRefOp.getType()); - if (refType && !ownsHeap(propertyRefOp.getLoc(), refType.getElementType())) - { - return false; - } - - return llvm::any_of(propertyRefOp->getUsers(), - [](mlir::Operation *fieldUser) { return mlir::isa(fieldUser); }); - }); - - return !movesIn || recordRetainOf(varOp) != nullptr; - } - - // rc's one `ts.Retain` of a record local's one read, where the read is only returned: stored - // into a local that owns nothing and is read only by returns (the result slot), or copied into - // the box it is given in as an object or an interface (spec 27). It is the reference rc gives - // each fresh value stored into the record, so it is where they move in. Null for any other - // record. - static mlir::Operation *recordRetainOf(mlir_ts::VariableOp varOp) - { - if (!isConstantRecordLocal(varOp)) - { - return nullptr; - } - - mlir_ts::LoadOp read; - for (auto *user : varOp->getUsers()) - { - if (auto loadOp = mlir::dyn_cast(user)) - { - if (read) - { - return nullptr; - } - - read = loadOp; - } - } - - if (!read) - { - return nullptr; - } - - mlir::Operation *retain = nullptr; - auto boxed = false; - auto other = false; - for (auto *user : read->getUsers()) - { - if (mlir::isa(user)) - { - if (retain) - { - return nullptr; - } - - retain = user; - continue; - } - - // copied into the block it is given in as an object or an interface (spec 27): the box - // owns what the record held, and its own check decides where the box goes - auto storeOp = mlir::dyn_cast(user); - auto box = storeOp && storeOp.getValue() == read.getResult() ? recordBoxOf(storeOp.getReference()) - : mlir_ts::NewOp(); - if (box && box.getResult() == storeOp.getReference()) - { - if (boxed) - { - return nullptr; - } - - boxed = true; - continue; - } - - other = true; - if (mlir::isa(user)) - { - continue; - } - - // `let a = { item: new C() }`: the read is what an owning local starts with, and rc's - // `ts.RetainSlot` of that local is the record's retain - if (auto ownerOp = mlir::dyn_cast(user)) - { - auto *slotRetain = ownerOp.getInitializer() == read.getResult() && isOwningVariable(ownerOp) - ? onlyRetainSlotOf(ownerOp) - : nullptr; - if (!slotRetain || retain) - { - return nullptr; - } - - retain = slotRetain; - continue; - } - - // stored into the result slot, or into an owning local (`a = { item: new C() }`) - auto slot = storeOp && storeOp.getValue() == read.getResult() - ? storeOp.getReference().getDefiningOp() - : mlir_ts::VariableOp(); - if (!slot || !(isResultSlot(slot) || isOwningVariable(slot))) - { - return nullptr; - } - } - - // a boxed record goes nowhere else: the box is its one owner - if (boxed && other) - { - return nullptr; - } - - return retain; - } - - // `const b = [...a, ...a]`: MLIRGen builds the array in a local of its own that owns nothing, - // starting from a fresh one and pushing into it, and reads it once, when it is done, into the - // owning local it is made for. That local's `ts.RetainSlot` (`retain`) is the reference: the - // fresh array moves into the builder and then into the owner, and nothing else holds it. - bool isBuiltArrayRead(mlir::Operation *retain, mlir::Value value) - { - auto read = value.getDefiningOp(); - auto builder = read ? read.getReference().getDefiningOp() : mlir_ts::VariableOp(); - auto owner = mlir::cast(retain).getSlot().getDefiningOp(); - 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) - { - return false; - } - - // only filled, and only before it is read - return llvm::all_of(builder->getUses(), [&](mlir::OpOperand &use) { - auto *user = use.getOwner(); - if (user == read) - { - return true; - } - - return mlir::isa(user) && use.getOperandNumber() == 0 && - !reachableAfter(read, user, builder); - }); - } - - // `return [...a]`: the builder of isBuiltArrayRead, read once, when it is done, and returned. - // The return's `ts.Retain` (`retain`) is the reference the caller gets: the fresh array moves - // into the builder and then out of the function, and nothing else holds it. The builder's - // other reads only bind its `push` (`ts.ThisSymbolRef @__array_push`) or are not used. A store - // into a field is the same move, into a container: `h.c = [...a]`, and a generator's local - // (`const groups = [...result]`), which is a field of its state object. - bool isBuiltArrayReturned(mlir::Operation *retain, mlir::Value value) - { - auto read = value.getDefiningOp(); - auto builder = read ? read.getReference().getDefiningOp() : mlir_ts::VariableOp(); - if (!builder || isOwningVariable(builder) || builder.getCaptured().value_or(false) || !builder.getInitializer() || - !mlir::isa_and_nonnull(builder.getInitializer().getDefiningOp())) - { - return false; - } - - // the read is retained and returned - stored into the function's result slot - and nothing else - auto returned = false; - for (auto *user : read->getUsers()) - { - if (user == retain) - { - continue; - } - - auto storeOp = mlir::dyn_cast(user); - auto resultSlot = storeOp && storeOp.getValue() == read.getResult() - ? storeOp.getReference().getDefiningOp() - : mlir_ts::VariableOp(); - auto intoField = storeOp && storeOp.getValue() == read.getResult() && - storeOp.getReference().getDefiningOp(); - if (!((resultSlot && isResultSlot(resultSlot)) || intoField) || returned) - { - return false; - } - - returned = true; - } - - if (!returned) - { - return false; - } - - // only filled, and only before it is read - return llvm::all_of(builder->getUses(), [&](mlir::OpOperand &use) { - auto *user = use.getOwner(); - if (user == read) - { - return true; - } - - if (reachableAfter(read, user, builder)) - { - return false; - } - - if (auto loadOp = mlir::dyn_cast(user)) - { - return llvm::all_of(loadOp->getUsers(), [](mlir::Operation *loadUser) { - return mlir::isa(loadUser) && loadUser->use_empty(); - }); - } - - return mlir::isa(user) && use.getOperandNumber() == 0; - }); - } - - // The one `ts.RetainSlot` of a local; null when there are none or several. - static mlir::Operation *onlyRetainSlotOf(mlir_ts::VariableOp varOp) - { - mlir::Operation *found = nullptr; - for (auto *user : varOp->getUsers()) - { - if (mlir::isa(user)) - { - if (found) - { - return nullptr; - } - - found = user; - } - } - - return found; - } - - // A local that owns nothing, is never captured, and is read only to be returned. - static bool isResultSlot(mlir_ts::VariableOp varOp) - { - if (isOwningVariable(varOp) || varOp.getCaptured().value_or(false)) - { - return false; - } - - return llvm::all_of(varOp->getUsers(), [&](mlir::Operation *user) { - if (auto storeOp = mlir::dyn_cast(user)) - { - return storeOp.getReference() == varOp.getResult(); - } - - auto loadOp = mlir::dyn_cast(user); - return loadOp && llvm::all_of(loadOp->getUsers(), [](mlir::Operation *loadUser) { - return mlir::isa(loadUser); - }); - }); - } - - // The record whose field `taker` stores into, and rc's retain that moves the stored value in; - // null when `taker` is not such a store. - static mlir::Operation *recordRetainOfStore(mlir::Operation *taker) - { - auto storeOp = mlir::dyn_cast(taker); - auto propertyRefOp = storeOp ? storeOp.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); - auto varOp = propertyRefOp ? propertyRefOp->getOperand(0).getDefiningOp() : mlir_ts::VariableOp(); - auto *retain = varOp ? recordRetainOf(varOp) : nullptr; - if (!retain || retain->getBlock() != taker->getBlock() || !taker->isBeforeInBlock(retain)) - { - return nullptr; - } - - return retain; - } - - // A string constant cast to `string` is the global itself, which a release skips; so is a read - // of an immortal global (spec 25). - static bool isImmortalLiteral(mlir::Value value) - { - if (isImmortalGlobalRead(value)) - { - return true; - } - - auto castOp = rootOf(value).getDefiningOp(); - return castOp && mlir::isa(castOp.getType()) && - castOp.getIn().getDefiningOp(); - } - - // A use that reads the value without keeping it. Kept deliberately short: anything not listed - // is treated as taking the value, which can only turn a program into an error. - static bool isBorrow(mlir::Operation *user, mlir::Value value) - { - if (mlir::isa(user)) - { - return true; - } - - // `assert(c, message)` prints the message when it fails, and keeps it either way - if (mlir::isa(user)) - { - return true; - } - - // `s.value`'s place and `Shared.count(s)` read the handle and keep nothing (spec 23.1) - if (mlir::isa(user)) - { - return true; - } - - // `--di`'s record of a folded const's value for the debugger: names it, keeps nothing - if (mlir::isa(user)) - { - return true; - } - - // a test - `if (d)`, `d?.x`, `typeof u` - asks about the value, and keeps nothing - if (mlir::isa(user)) - { - return true; - } - - // ...and so does a cast to `null` or `undefined`, which holds no block: `--di` records the - // `r` that `if (r == null)` narrows as one - if (auto castOp = mlir::dyn_cast(user)) - { - return mlir::isa(castOp.getType()) || - castOp.getType().isInteger(1) || isBoundForCall(castOp); - } - - if (mlir::isa(user)) - { - return isBoundForCall(user); - } - - // arguments are borrowed, but one the callee keeps (`__own_params`) is taken; a callee - // with no facts that keeps one retains it, and that retain is its own error - if (isCall(user)) - { - return !isKeptArgument(user, value); - } - - // the array being pushed onto is written, not taken; what is inserted is taken - if (auto pushOp = mlir::dyn_cast(user)) - { - return pushOp.getOp() == value && !llvm::is_contained(pushOp.getItems(), value); - } - - if (auto unshiftOp = mlir::dyn_cast(user)) - { - return unshiftOp.getOp() == value && !llvm::is_contained(unshiftOp.getItems(), value); - } - - if (auto spliceOp = mlir::dyn_cast(user)) - { - return spliceOp.getOp() == value && !llvm::is_contained(spliceOp.getItems(), value); - } - - // a declaration that does not own what it holds only reads it - if (auto varOp = mlir::dyn_cast(user)) - { - return !isOwningVariable(varOp); - } - - // the box of a closure that borrows what it captures holds a copy that owns nothing; the - // closure's own uses are the borrow's (walkBorrowed, decideClosures) - if (mlir::isa(user) && user->hasAttr(OWN_CAPTURE_BORROW_ATTR_NAME)) - { - return true; - } - - // a store through a `ts.New`'d `value_ref` fills its block and keeps nothing of it - if (auto storeOp = mlir::dyn_cast(user)) - { - return mlir::isa(value.getType()) && storeOp.getReference() == value && - storeOp.getValue() != value; - } - - return false; - } - - // `ts.CreateBoundFunction(ts.Cast(object), method)` split straight back into `ts.GetThis` and - // `ts.GetMethod` for a call: a method of another module's class - its constructor, when it is - // built with `new` - called on an object here. It reads the object as `ts.ThisSymbolRef` does - // for a method of this module; nothing else may use the cast or the bound function, and the - // object comes out again only as a call's argument, which is a borrow of its own. An - // interface's `this` (`ts.ExtractInterfaceThis`) is bound so for a call of a function-typed - // field (`i.toString()` where `toString: () => string`). - static bool isBoundForCall(mlir::Operation *thisOp) - { - if (thisOp->getNumResults() != 1 || !mlir::isa(thisOp->getResult(0).getType()) || - thisOp->use_empty()) - { - return false; - } - - return llvm::all_of(thisOp->getUses(), [](mlir::OpOperand &use) { - auto boundOp = mlir::dyn_cast(use.getOwner()); - if (!boundOp || boundOp.getThisVal() != use.get()) - { - return false; - } - - return llvm::all_of(boundOp->getUsers(), [](mlir::Operation *boundUser) { - if (mlir::isa(boundUser)) - { - return true; - } - - return mlir::isa(boundUser) && - llvm::all_of(boundUser->getUsers(), [](mlir::Operation *thisUser) { return isCall(thisUser); }); - }); - }); - } - - // Is `value` an argument the call's callee keeps? - static bool isKeptArgument(mlir::Operation *call, mlir::Value value) - { - auto args = callArgs(call); - return llvm::any_of(ownedParams(call), [&](int32_t index) { - return static_cast(index) < args.size() && args[index] == value; - }); - } - - // The moves out of a fresh or unknown SSA value. Each taking use (spec 11.1) is a move: - // - nothing else may use the value after it - another taker included - and a taker that - // control comes back to without the value being made again is a move on every iteration - // of a loop (spec 2.6); - // - where the temporary is also released (`__owned_result` rc gives back at block end), the - // taker must be one rc gave its own reference (takerAcquires), and each release the move - // reaches must be dominated by it - that release then has nothing to give back and is - // erased. A release the move cannot reach stays: it is the owner on the other paths. - // Reports and returns false otherwise. - bool checkValueMoves(mlir::Value value, llvm::SetVector &toErase) - { - // Nothing to move: a number owns no block, and a string literal is the immortal global, - // which any number of places may hold. Under --opt, CSE merges identical literals before - // this pass, so one such value is routinely stored into several places. A read out of a - // container owns nothing either: checkPlaceRead decides it. Nor does other data nothing - // owns: an empty optional, a function, a tuple of literals (isConstantData). - if (!ownsHeap(value) || isImmortalLiteral(value) || isConstantData(value) || placeReadOf(value)) - { - return true; - } - - // A temporary's releases are one owner, one release on each way out. Two on one path are - // two owners: each view rc gave a reference of its own (`use(o); use(o)`, with `o` a - // folded object literal and `use` taking an interface: two `ts.NewInterface`s of one block). - if (!checkReleasedOnce(value)) - { - return false; - } - - if (quietly([&] { return movesOf(value, toErase); })) - { - return true; - } - - // Not a move. Every taker a `let` that can borrow, and the temporary's own release the - // owner that bounds them (spec 2.2, verdict 2) - else the move's error stands. - llvm::SmallVector takers; - llvm::SmallVector releases; - forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { - if (mlir::isa(user)) - { - return; - } - - if (mlir::isa(user)) - { - releases.push_back(user); - } - else if (!isBorrow(user, used)) - { - takers.push_back(user); - } - }); - - if (!releases.empty() && !takers.empty() && - llvm::all_of(takers, [](mlir::Operation *taker) { return !!borrowerOf(taker); })) - { - auto owner = ownerName(value); - auto ok = true; - for (auto *taker : takers) - { - ok = tryBorrow(borrowerOf(taker), releases, owner, toErase) && ok; - } - - return ok; - } - - return movesOf(value, toErase); - } - - // No release of `value` comes after another on any path. Reports and returns false otherwise. - bool checkReleasedOnce(mlir::Value value) - { - llvm::SmallVector releases; - forEachUse(value, [&](mlir::Operation *user, mlir::Value) { - if (mlir::isa(user)) - { - releases.push_back(user); - } - }); - - auto *definition = value.getDefiningOp(); - for (auto *first : releases) - { - for (auto *second : releases) - { - if (first != second && reachableAfter(first, second, definition)) - { - // the view the second release gives back, where rc took its reference - auto *view = second->getOperand(0).getDefiningOp(); - reportSecondReference(view ? view : second, value); - return false; - } - } - } - - return true; - } - - // The move verdict for an SSA value (phase 1). Reports and returns false when it is not one. - bool movesOf(mlir::Value value, llvm::SetVector &toErase) - { - llvm::SmallVector takers; - llvm::SmallVector releases; - llvm::SmallVector reads; - llvm::DenseMap readOf; - forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { - if (mlir::isa(user)) - { - return; - } - - if (mlir::isa(user)) - { - releases.push_back(user); - } - else if (isBorrow(user, used)) - { - reads.push_back(user); - readOf.try_emplace(user, used); - } - else - { - takers.push_back(user); - } - }); - - auto *definition = value.getDefiningOp(); - auto name = describe(value); - llvm::SmallVector moved; - for (auto *taker : takers) - { - if (takerLoops(taker, definition)) - { - reportMovedInLoop(taker, name); - return false; - } - - llvm::SmallVector made; - if (definition) - { - made.push_back(definition); - } - - // the call that keeps it, given it again through another operand - if (auto *again = callGivenAgain(taker, value, {value})) - { - reportUseAfterMove(again, taker, name); - return false; - } - - for (auto *use : llvm::concat(takers, reads)) - { - if (use != taker && reachableAfter(taker, use, definition)) - { - // a read through the new owner, while it holds the value (spec 29) - if (auto found = readOf.find(use); - found != readOf.end() && readsThroughOwner(taker, found->second, use, made)) - { - continue; - } - - reportUseAfterMove(use, taker, name); - return false; - } - } - - if (releases.empty()) - { - continue; // rc handed the taker the value's own reference: nothing to cancel - } - - if (!takerAcquires(taker, value)) - { - reportSecondReference(taker, value); - return false; - } - - auto *acquire = acquirePoint(taker, value); - for (auto *release : releases) - { - if (!reachableAfter(acquire, release, definition)) - { - continue; - } - - if (!dominance->properlyDominates(acquire, release)) - { - reportMovedOnSomePaths(taker, release, name); - return false; - } - - moved.push_back(release); - } - } - - toErase.insert(moved.begin(), moved.end()); - return true; - } - - // A call that keeps an argument and is given the same value again through another operand - the - // value itself, a view of it, something read out of it, another read of its slot (`f(n, n)`, - // `f(h, n, n.v)` with only the first `n` kept): the callee takes the value while it borrows it - // through the other operand, and may free it under that borrow, so the other operand is a use - // after the move (spec 33, review). `sources` are what the moved value is: the value, or every - // read of the owning slot it came from. The call itself, or null. - mlir::Operation *callGivenAgain(mlir::Operation *taker, mlir::Value moved, llvm::ArrayRef sources) - { - if (!isCall(taker) || ownedParams(taker).empty()) - { - return nullptr; - } - - auto args = callArgs(taker); - auto kept = ownedParams(taker); - auto movedRoot = rootOf(moved); - // one kept position is the move itself; the value at a second one (`f(n, n)` with both - // kept) is a second owner, a use after the first move like any other operand - auto skipped = false; - for (auto &operand : taker->getOpOperands()) - { - // the moved argument itself, where the callee keeps it - auto number = operand.getOperandNumber(); - if (!skipped && !args.empty() && number >= args.getBeginOperandIndex() && - number < args.getBeginOperandIndex() + args.size() && - llvm::is_contained(kept, static_cast(number - args.getBeginOperandIndex())) && - rootOf(operand.get()) == movedRoot) - { - skipped = true; - continue; - } - - for (auto source : sources) - { - if (derivedFrom(source).contains(operand.get())) - { - return taker; - } - } - } - - return nullptr; - } - - // Does control come back round to `taker` without making the value again? Only then is it a - // move on every iteration of a loop. - bool takerLoops(mlir::Operation *taker, mlir::Operation *definition) - { - auto &body = getFunction().getFunctionBody(); - auto *top = body.findAncestorOpInRegion(*taker); - if (!top) - { - return true; - } - - auto *kill = definition ? body.findAncestorOpInRegion(*definition) : nullptr; - llvm::SmallPtrSet seen; - llvm::SmallVector work(top->getBlock()->getSuccessors().begin(), - top->getBlock()->getSuccessors().end()); - while (!work.empty()) - { - auto *block = work.pop_back_val(); - if (!seen.insert(block).second) - { - continue; - } - - if (kill && kill->getBlock() == block && - (block != top->getBlock() || kill->isBeforeInBlock(top))) - { - continue; // the value is made again before the taker runs again - } - - if (block == top->getBlock()) - { - return true; - } - - work.append(block->getSuccessors().begin(), block->getSuccessors().end()); - } - - return false; - } - - // Can control reach `to` after `from` has run, without passing `kill` on the way? `kill` is - // the source's definition - the producer of an SSA value, or a local's `ts.Variable` - and a - // path through it is a new value, not this one. Straight-line order within a block, then the - // CFG from the block's successors; a path back into `from`'s own block reaches every op in it. - // - // Ops nested in a region that is not the function body are compared by their ancestors in the - // body. That answers "reachable" whenever the two share an ancestor, which can only turn a - // program into an error. (By this pass async bodies and lambdas are functions of their own; - // after the affine lowering no op of a body holds a region, so the case is kept for safety.) - bool reachableAfter(mlir::Operation *from, mlir::Operation *to, mlir::Operation *kill) - { - return reachableAfter(from, to, kill ? llvm::ArrayRef(kill) : llvm::ArrayRef()); - } - - // Whether reachableAfter can order `from` and `to` at all: both inside the function body and not - // one op of it (nested in the same statement). A check whose safe answer is "not reachable" must - // refuse what it cannot order, where reachableAfter would answer "reachable". - bool orderedInBody(mlir::Operation *from, mlir::Operation *to) - { - auto &body = getFunction().getFunctionBody(); - from = body.findAncestorOpInRegion(*from); - to = body.findAncestorOpInRegion(*to); - return from && to && from != to; - } - - // The same, where a path through any of `kills` is a new value. A use that is itself a kill - // runs after the value is made again. - bool reachableAfter(mlir::Operation *from, mlir::Operation *to, llvm::ArrayRef kills) - { - if (llvm::is_contained(kills, to)) - { - return false; - } - - auto &body = getFunction().getFunctionBody(); - from = body.findAncestorOpInRegion(*from); - to = body.findAncestorOpInRegion(*to); - llvm::SmallVector killAt; - for (auto *kill : kills) - { - if (auto *top = body.findAncestorOpInRegion(*kill)) - { - killAt.push_back(top); - } - } - - if (!from || !to) - { - return true; - } - - if (from == to) - { - // Two distinct ops that share one ancestor statement: the order inside it is not - // known here, and "reachable" can only turn a program into an error. - return true; - } - - // is there a kill in `block` after `after` (from its start when null) and before `before` - // (to its end when null)? - auto killsIn = [&](mlir::Block *block, mlir::Operation *after, mlir::Operation *before) { - return llvm::any_of(killAt, [&](mlir::Operation *kill) { - return kill->getBlock() == block && (!after || after->isBeforeInBlock(kill)) && - (!before || kill->isBeforeInBlock(before)); - }); - }; - - auto *fromBlock = from->getBlock(); - auto *toBlock = to->getBlock(); - if (fromBlock == toBlock && from->isBeforeInBlock(to)) - { - return !killsIn(fromBlock, from, to); - } - - // every way out of this block runs a kill first - if (killsIn(fromBlock, from, nullptr)) - { - return false; - } - - llvm::SmallPtrSet seen; - llvm::SmallVector work(fromBlock->getSuccessors().begin(), fromBlock->getSuccessors().end()); - while (!work.empty()) - { - auto *block = work.pop_back_val(); - if (!seen.insert(block).second) - { - continue; - } - - if (block == toBlock && !killsIn(block, nullptr, to)) - { - return true; - } - - if (killsIn(block, nullptr, nullptr)) - { - continue; - } - - work.append(block->getSuccessors().begin(), block->getSuccessors().end()); - } - - return false; - } - - // The use that takes `value` for this retain: the declaration a `ts.RetainSlot` belongs to, - // or the one use of the value, or of a view of it, after a `ts.Retain` that is not a read. - static mlir::Operation *takerOf(mlir::Operation *retain, mlir::Value value) - { - if (auto retainSlotOp = mlir::dyn_cast(retain)) - { - return retainSlotOp.getSlot().getDefiningOp(); - } - - // a call that keeps the argument: rc's retain for it is in the callee - if (isCall(retain)) - { - return retain; - } - - mlir::Operation *taker = nullptr; - auto several = false; - forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { - if (mlir::isa(user) || isBorrow(user, used)) - { - return; - } - - several = several || taker; - taker = user; - }); - - // rc retained this read and hands the receiver the next read of the same place - if (!taker && !several) - { - if (auto twin = twinReadAfter(retain, value)) - { - return takerOf(retain, twin); - } - } - - return several ? nullptr : taker; // two takers of one read: not a move this phase proves - } - - // rc reads a place twice for one copy - `.map(f)` boxes its array so - and retains the first - // read, whose only use is that retain: the read of the same place that follows it in its block, - // with nothing stored into the place between, is the same value. Null when there is none. - // The one op that uses `value`, but `--di`'s ts.DebugVariable, which only names it; or null. - static mlir::Operation *soleUser(mlir::Value value) - { - mlir::Operation *found = nullptr; - for (auto *user : value.getUsers()) - { - if (mlir::isa(user)) - { - continue; - } - - if (found) - { - return nullptr; - } - - found = user; - } - - return found; - } - - static mlir::Value twinReadAfter(mlir::Operation *retain, mlir::Value value) - { - auto first = value.getDefiningOp(); - if (!first || !mlir::isa(retain) || soleUser(value) != retain || retain->getBlock() != first->getBlock()) - { - return {}; - } - - auto place = first.getReference(); - for (auto *op = retain->getNextNode(); op; op = op->getNextNode()) - { - if (auto loadOp = mlir::dyn_cast(op); loadOp && loadOp.getReference() == place) - { - return loadOp.getResult(); - } - - // anything else given the place may write through it - a store, a push - and a call may - // write anything - if (llvm::is_contained(op->getOperands(), place) || isCall(op)) - { - return {}; - } - } - - return {}; - } - - // A move out of an owning local: `let b = a`, `h.c = a`, `return a`. rc read the slot and - // retained what it read for the receiver; the move erases that retain and every release of - // the slot the move reaches, which must be dominated by it - or, with `placeReleases`, is - // placed on the paths that did not move it (spec 30). Any other use of the slot the - // move reaches - a read, an assignment's own read of it - is a use after the move. Reports and - // returns false when it is not a move. - // - // An assignment the move reaches gives the slot a new value to own (#528): a path through it - // is a new value, as a path through the declaration is. Its release before the store goes - // with the move - what the slot held is the receiver's - and the releases only a path through - // an assignment reaches stay, to destroy the new value. - bool checkSlotMove(mlir::Operation *retain, mlir::Value value, mlir_ts::LoadOp load, - llvm::SetVector &toErase, bool placeReleases = false) - { - auto slot = load.getReference(); - auto varOp = slot.getDefiningOp(); - auto name = varName(varOp); - - auto *taker = takerOf(retain, value); - if (!taker) - { - reportSecondReference(retain); - return false; - } - - auto assignments = slotAssignments(slot); - llvm::SmallVector kills(assignments.begin(), assignments.end()); - if (varOp) - { - kills.push_back(varOp); - } - - // the move's own read, met again around a loop the slot was declared outside of - if (reachableAfter(taker, load, kills)) - { - reportMovedInLoop(taker, name); - return false; - } - - // the slot given another value on some path between the move's read and its taker - inside - // the call's own argument list, `f(n, (n = mk(7), 0))`, on one arm of it, `f(n, c ? (n = - // mk(7), 0) : 0)`: the assignment's release frees what the read holds before the callee takes - // it, so the assignment is a use of the slot after the move. The taker may be an assignment - // of the slot itself (`n = n`): it is the destination, not between. A read and a taker - // reachableAfter cannot order (one statement kept as a region, or outside the body) refuse. - { - llvm::SmallVector readAgain{load.getOperation()}; - if (varOp) - { - readAgain.push_back(varOp); - } - - for (auto *assignment : assignments) - { - if (assignment != taker && reachableAfter(load, assignment, varOp) && - reachableAfter(assignment, taker, readAgain)) - { - reportUseAfterMove(assignment, taker, name); - return false; - } - } - - llvm::SmallVector killsBetween(kills); - llvm::erase(killsBetween, taker); - if (!orderedInBody(load, taker) || !reachableAfter(load, taker, killsBetween)) - { - reportUseAfterMove(taker, taker, name); - return false; - } - } - - // the call that keeps it, given the slot's value again through another operand: a read of - // the slot that reaches the call without crossing an assignment of it (not one made before - // an assignment gave the slot its current value, `const arr = [n]; n = mk(5); f(n, arr)`); - // since the move's own read reaches the call so too, such a read holds the moved value - llvm::SmallVector slotReads; - for (auto *user : slot.getUsers()) - { - if (auto readOp = mlir::dyn_cast(user); readOp && reachableAfter(readOp, taker, kills)) - { - slotReads.push_back(readOp.getResult()); - } - } - - if (auto *again = callGivenAgain(taker, value, slotReads)) - { - reportUseAfterMove(again, taker, name); - return false; - } - - // Every read of the slot is a use of `a`, and so is every use of what a read returned: a - // `const b = a` is folded into its load, so `b.x` after the move reads the moved value - // through a load that came before it. A read through the new owner, while it holds the - // value, is not such a use (spec 29). - auto throughOwner = [&](mlir::Operation *user, mlir::Value used) { - return readsThroughOwner(taker, used, user, kills); - }; - - for (auto *use : slot.getUsers()) - { - if (mlir::isa(use)) - { - continue; - } - - if (use != load.getOperation() && reachableAfter(taker, use, kills)) - { - auto readOp = mlir::dyn_cast(use); - if (!readOp || !onlyReadThroughOwner(readOp.getResult(), throughOwner)) - { - reportUseAfterMove(use, taker, name); - return false; - } - - continue; - } - - if (auto readOp = mlir::dyn_cast(use)) - { - if (auto *late = readUsedAfter(readOp, retain, taker, varOp, throughOwner)) - { - reportUseAfterMove(late, taker, name); - return false; - } - } - } - - auto *acquire = mlir::isa(retain) ? taker : retain; - llvm::SmallVector moved; - for (auto *use : slot.getUsers()) - { - if (!mlir::isa(use) || !reachableAfter(acquire, use, kills)) - { - continue; - } - - // a release that one path reaches holding the moved value and another holding a new - // one an assignment after the move stored: there is no single answer for it - auto someMoved = !dominance->properlyDominates(acquire, use); - for (auto *assignment : assignments) - { - llvm::SmallVector movedAgain{load.getOperation()}; - if (varOp) - { - movedAgain.push_back(varOp); - } - - someMoved = someMoved || - (reachableAfter(acquire, assignment, varOp) && reachableAfter(assignment, use, movedAgain)); - } - - if (someMoved) - { - // ...but the paths that did not move it can release it before they join the - // others (spec 30) - if (placeReleases && placeOnUnmovedEdges(varOp, acquire, retain, taker, assignments, toErase)) - { - return true; - } - - reportMovedOnSomePaths(taker, use, name, placeReleases ? joinedUse : nullptr); - return false; - } - - moved.push_back(use); - } - - toErase.insert(moved.begin(), moved.end()); - return true; - } - - // ---- A move on some paths only (spec 30) ---- - // - // `if (c) { best = v; }`: after the paths join, `v` holds its value on one and a moved one on - // the other, and its one release there has no single answer. No run-time record of the move is - // kept (spec 2.2); instead, where nothing uses the value after the join, the release is placed - // on each edge into the join that did not move it - freeing the value earlier, where it is - // already dead - and the releases after the join go. Each path frees the value once: through - // its new owner where it moved, through the edge where it did not. - - // What the slot of an owning local holds at a point: nothing yet or any more (declared later, - // released), its value, or a value moved out of it. A point some paths reach holding the value - // and others having moved it is where the paths join. - enum SlotState : unsigned - { - SlotDead = 1, - SlotLive = 2, - SlotMoved = 4 - }; - - static bool isJoined(unsigned state) - { - return (state & SlotLive) && (state & SlotMoved); - } - - // A value of this type can point into a block: anything but a number, a boolean, a literal or - // an empty value. - static bool carriesPointer(mlir::Type type) - { - return !mlir::isa(type); - } - - // A release on an edge into the join that did not move the local's value (spec 30), put in after - // the analysis. - struct EdgeRelease - { - mlir::Value slot; - mlir::Block *from; - unsigned successor; - mlir::Location loc; - }; - - llvm::SmallVector edgeReleases; - - // The use that kept placeOnUnmovedEdges from placing the releases, for the error's note. - mlir::Operation *joinedUse = nullptr; - - // Can control reach `to` from the start of `start`, without passing `kills` on the way? - bool reachableFrom(mlir::Block *start, mlir::Operation *to, llvm::ArrayRef kills) - { - auto &body = getFunction().getFunctionBody(); - to = body.findAncestorOpInRegion(*to); - if (!to) - { - return true; - } - - llvm::SmallVector killAt; - for (auto *kill : kills) - { - if (auto *top = body.findAncestorOpInRegion(*kill)) - { - killAt.push_back(top); - } - } - - llvm::SmallPtrSet seen; - llvm::SmallVector work{start}; - while (!work.empty()) - { - auto *block = work.pop_back_val(); - if (!seen.insert(block).second) - { - continue; - } - - auto killed = [&](mlir::Operation *before) { - return llvm::any_of(killAt, [&](mlir::Operation *kill) { - return kill->getBlock() == block && (!before || kill->isBeforeInBlock(before) || kill == before); - }); - }; - - if (to->getBlock() == block && !killed(to)) - { - return true; - } - - if (!killed(nullptr)) - { - work.append(block->getSuccessors().begin(), block->getSuccessors().end()); - } - } - - return false; - } - - // The blocks of an exception handler: from a landing pad to the end of its catch. A release there - // would be a call inside an exception funclet. - llvm::SmallPtrSet handlerBlocks() - { - llvm::SmallPtrSet handler; - llvm::SmallVector work; - for (auto &block : getFunction().getFunctionBody()) - { - if (!block.empty() && mlir::isa(block.front())) - { - work.push_back(&block); - } - } - - while (!work.empty()) - { - auto *block = work.pop_back_val(); - if (!handler.insert(block).second) - { - continue; - } - - auto ends = llvm::any_of(*block, [](mlir::Operation &op) { - return mlir::isa(op); - }); - if (!ends) - { - work.append(block->getSuccessors().begin(), block->getSuccessors().end()); - } - } - - return handler; - } - - // The move at `acquire` out of the owning local `varOp` reaches a release that other paths reach - // holding the value. Places a release of the slot on every edge into the join that holds the value - // and has not moved it, and erases every release a moved value reaches - when nothing uses the - // value, through the slot or through anything read out of it, where the paths have joined. False, - // with nothing placed or erased, when it cannot. - bool placeOnUnmovedEdges(mlir_ts::VariableOp varOp, mlir::Operation *acquire, mlir::Operation *retain, - mlir::Operation *taker, llvm::ArrayRef assignments, - llvm::SetVector &toErase) - { - joinedUse = nullptr; - auto &body = getFunction().getFunctionBody(); - if (!varOp || varOp.getCaptured().value_or(false) || varOp->getParentRegion() != &body || - acquire->getParentRegion() != &body || - llvm::any_of(assignments, [&](mlir::Operation *op) { return op->getParentRegion() != &body; })) - { - return false; - } - - auto slot = varOp.getResult(); - llvm::SmallVector releases; - for (auto *user : slot.getUsers()) - { - if (mlir::isa(user)) - { - if (user->getParentRegion() != &body) - { - return false; - } - - releases.push_back(user); - } - } - - // the slot's state through the function, forward from its entry - auto step = [&](mlir::Operation *op, unsigned state) -> unsigned { - if (op == varOp.getOperation() || llvm::is_contained(assignments, op)) - { - return SlotLive; - } - - if (op == acquire) - { - return SlotMoved; - } - - if (llvm::is_contained(releases, op)) - { - return SlotDead; - } - - return state; - }; - - llvm::DenseMap entry; - auto exitOf = [&](mlir::Block *block) { - auto state = entry.lookup(block); - for (auto &op : *block) - { - state = step(&op, state); - } - - return state; - }; - - entry[&body.front()] = SlotDead; - llvm::SmallVector work{&body.front()}; - while (!work.empty()) - { - auto *block = work.pop_back_val(); - auto state = exitOf(block); - for (auto *successor : block->getSuccessors()) - { - auto &into = entry[successor]; - if ((into | state) != into) - { - into |= state; - work.push_back(successor); - } - } - } - - // ...and before each op of the body - llvm::DenseMap before; - for (auto &block : body) - { - auto state = entry.lookup(&block); - for (auto &op : block) - { - before[&op] = state; - state = step(&op, state); - } - } - - auto joinedAt = [&](mlir::Operation *user) { - auto *top = body.findAncestorOpInRegion(*user); - return !top || isJoined(before.lookup(top)); - }; - - // every release a moved value reaches goes; one some path reaches with nothing in the slot - // is not this rule's - llvm::SmallVector erased; - for (auto *release : releases) - { - auto state = before.lookup(release); - if (state & SlotMoved) - { - if (state & SlotDead) - { - return false; - } - - erased.push_back(release); - } - } - - // the edges into the join that hold the value and did not move it - auto handler = handlerBlocks(); - llvm::SmallVector placed; - llvm::SetVector releasedInto; - for (auto &block : body) - { - if (!entry.count(&block)) - { - continue; // unreachable - } - - auto state = exitOf(&block); - for (auto index : llvm::seq(0, block.getNumSuccessors())) - { - auto into = entry.lookup(block.getSuccessor(index)); - if (!isJoined(into)) - { - continue; - } - - // a join some path reaches with nothing in the slot is not this rule's - if (into & SlotDead) - { - return false; - } - - // an edge that moved it, or from where the paths have joined already - if (state != SlotLive) - { - continue; - } - - auto *terminator = block.getTerminator(); - if (!mlir::isa(terminator) || handler.contains(&block) || - !dominance->dominates(varOp.getOperation(), terminator)) - { - return false; - } - - placed.push_back({slot, &block, static_cast(index), terminator->getLoc()}); - releasedInto.insert(block.getSuccessor(index)); - } - } - - if (placed.empty()) - { - return false; - } - - // Nothing uses the value where the paths have joined - a read there, on the path that did - // not move it, needs what the edge released - neither through the slot nor through anything - // read out of it: a field, a view, a borrowing local, a closure. The move itself hands the - // value to its new owner, whose reads are its own. A value the slot is given - its - // initializer, an assignment's - is the slot's only once it is stored: a use before that (a - // constructor's, or one around a loop before it is made again) is not a use of it. - // - // Past the joined points too: what was read out of the value before an edge released it - // may not be used after that edge - after a new value is stored into the slot, or after its - // scope ends - unless it was read out again (spec 30). A read of the slot after a new value - // is stored reads that one. - struct Origin - { - mlir::Operation *stored; - mlir::Operation *made; - }; - - llvm::SmallVector origins{{nullptr, nullptr}}; - llvm::SmallVector> values; - llvm::DenseSet> seen; - auto taint = [&](mlir::Value value, unsigned origin) { - if (carriesPointer(value.getType()) && seen.insert({value, origin}).second) - { - values.push_back({value, origin}); - } - }; - - auto given = [&](mlir::Value value, mlir::Operation *stored) { - origins.push_back({stored, value.getDefiningOp()}); - taint(value, origins.size() - 1); - }; - - taint(slot, 0); - if (varOp.getInitializer()) - { - given(varOp.getInitializer(), varOp); - } - - for (auto *assignment : assignments) - { - given(mlir::cast(assignment).getValue(), assignment); - } - - // reached from an edge that released the value, before the read it came from (or the op - // that made it) runs again - auto usedAfterRelease = [&](mlir::Operation *made, mlir::Operation *user) { - llvm::SmallVector madeAgain; - if (made) - { - madeAgain.push_back(made); - } - - return llvm::any_of(releasedInto, [&](mlir::Block *join) { return reachableFrom(join, user, madeAgain); }); - }; - - auto heldAt = [&](unsigned origin, mlir::Operation *user) { - auto &from = origins[origin]; - if (!from.stored) - { - return true; - } - - llvm::SmallVector madeAgain; - if (from.made) - { - madeAgain.push_back(from.made); - } - - return reachableAfter(from.stored, user, madeAgain); - }; - - while (!values.empty()) - { - auto [value, origin] = values.pop_back_val(); - for (auto &use : value.getUses()) - { - auto *user = use.getOwner(); - if (mlir::isa(user) || user == retain) - { - continue; - } - - // the slot's own releases and assignments are decided above - if (value == slot && (mlir::isa(user) || llvm::is_contained(assignments, user))) - { - continue; - } - - if (joinedAt(user) && heldAt(origin, user)) - { - joinedUse = user; - return false; - } - - if (value != slot && usedAfterRelease(origins[origin].made, user)) - { - joinedUse = user; - return false; - } - - if (user == taker) - { - continue; - } - - // a store of it into a local makes every read of that local one; one into a place - // is not followed, and the release is not placed - if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getValue() == value) - { - auto into = storeOp.getReference().getDefiningOp(); - if (!into || into.getCaptured().value_or(false)) - { - return false; - } - - taint(into.getResult(), origin); - continue; - } - - if (auto branch = mlir::dyn_cast(user)) - { - for (auto index : llvm::seq(0, user->getNumSuccessors())) - { - auto operands = branch.getSuccessorOperands(index); - for (auto [position, operand] : llvm::enumerate(operands.getForwardedOperands())) - { - if (operand == value) - { - taint(user->getSuccessor(index)->getArgument(operands.getProducedOperandCount() + position), - origin); - } - } - } - } - - // a call's result is its own block, unless its facts say it borrows this argument - a - // plain borrow, or a borrowing record that holds it (spec 32) - auto borrows = true; - if (isCall(user)) - { - auto args = callArgs(user); - borrows = llvm::any_of(resultBorrowsAll(user), [&](int index) { - return static_cast(index) < args.size() && args[index] == value; - }); - } - - // a read of the slot is what its uses came from - auto from = origin; - if (value == slot && user->getNumResults() > 0) - { - origins.push_back({nullptr, user}); - from = origins.size() - 1; - } - - for (auto result : user->getResults()) - { - if (borrows) - { - taint(result, from); - } - } - - // inside a region - a closure, an async body - it is the op that holds the region - // that hands it on - if (user->getParentRegion() != &body) - { - for (auto result : body.findAncestorOpInRegion(*user)->getResults()) - { - taint(result, origin); - } - } - } - } - - toErase.insert(erased.begin(), erased.end()); - edgeReleases.append(placed.begin(), placed.end()); - return true; - } - - // Puts in the releases placeOnUnmovedEdges decided: at the end of a block that goes only to the - // join, else in a block of its own on the edge. - void placeEdgeReleases() - { - for (auto &edge : edgeReleases) - { - auto *terminator = edge.from->getTerminator(); - mlir::OpBuilder builder(terminator); - if (terminator->getNumSuccessors() == 1) - { - builder.create(edge.loc, edge.slot); - continue; - } - - auto *join = terminator->getSuccessor(edge.successor); - auto operands = mlir::cast(terminator).getSuccessorOperands(edge.successor); - llvm::SmallVector forwarded(operands.getForwardedOperands()); - auto *split = new mlir::Block(); - split->insertAfter(edge.from); - builder.setInsertionPointToEnd(split); - builder.create(edge.loc, edge.slot); - builder.create(edge.loc, join, forwarded); - if (!forwarded.empty()) - { - operands.erase(0, forwarded.size()); - } - - terminator->setSuccessor(split, edge.successor); - } - - edgeReleases.clear(); - } - - // The stores of the assignments to an owning local: rc releases what the slot holds right - // before it stores the new value (an assignment, and a `var` declaration met again). - static llvm::SmallVector slotAssignments(mlir::Value slot) - { - llvm::SmallVector assignments; - for (auto *user : slot.getUsers()) - { - auto storeOp = mlir::dyn_cast(user); - auto releaseOp = storeOp ? mlir::dyn_cast_or_null(storeOp->getPrevNode()) - : mlir_ts::ReleaseSlotOp(); - if (releaseOp && storeOp.getReference() == slot && releaseOp.getSlot() == slot) - { - assignments.push_back(storeOp); - } - } - - return assignments; - } - - // A use of what `read` returned - directly or through casts - that the move at `taker` can - // reach, other than the move's own retain and taker. None if there is none. A path back - // through `read` itself reaches a use of a value read again: the read, met after the move, - // is the slot's own use to check (around a loop, of a value an assignment stored). - // `isRead` accepts a use that reads the value through its new owner (spec 29). - mlir::Operation *readUsedAfter(mlir_ts::LoadOp read, mlir::Operation *retain, mlir::Operation *taker, - mlir::Operation *kill, - llvm::function_ref isRead = nullptr) - { - llvm::SmallVector values{read.getResult()}; - while (!values.empty()) - { - auto current = values.pop_back_val(); - for (auto *user : current.getUsers()) - { - if (user == retain || user == taker || mlir::isa(user)) - { - continue; - } - - if (mlir::isa(user) || isView(user)) - { - values.push_back(user->getResult(0)); - continue; - } - - llvm::SmallVector kills{read.getOperation()}; - if (kill) - { - kills.push_back(kill); - } - - if (reachableAfter(taker, user, kills) && !(isRead && isRead(user, current))) - { - return user; - } - } - } - - return nullptr; - } - - // ---- Reads after a move, through the new owner (spec 29) ---- - // - // `h.c = c; print(c.x)`, `best = v; s += v.x`: after the move the value is the new owner's, and - // a read of it through the old name is a borrow of the place it moved into, as a read of - // `h.c` would be. It is bounded the way checkPlaceRead bounds such a read - by the ends of the - // place's roots and by the drops that may destroy what the place holds (an overwrite, through an - // interface view too; a call whose facts may drop; a call given a closure) - on a path from the - // move to the read on which the value is not made again or moved again. - - // Every use of `value`, through its views, is a read `isRead` accepts. A retain - rc's reference - // for an owner - is not. - static bool onlyReadThroughOwner(mlir::Value value, llvm::function_ref isRead) - { - auto all = true; - forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { - all = all && (mlir::isa(user) || - (!mlir::isa(user) && isRead(user, used))); - }); - - return all; - } - - // A use that only reads the value: a field, element or length read through it, a method reached - // through it, a comparison or arithmetic, a print, an assert, a test, or an argument the callee - // borrows. Anything else - a store, an owning `let`, a keeping call, a return - would be a second - // owner. - static bool isReadAfterMove(mlir::Operation *user, mlir::Value value) - { - if (mlir::isa(user)) - { - return isBorrow(user, value); - } - - return isCall(user) && !isKeptArgument(user, value); - } - - // The place a taker moves its value into, as a chain checkPlaceRead would read it: an owning - // local (a declaration, or an assignment), or a field or element - of a local or a parameter - // (`this`). False for anything else: a call that keeps it, a push, a return, a global, a - // captured variable, a handle's payload. - bool destinationChain(mlir::Operation *taker, Chain &chain) - { - auto ownSlot = [](mlir_ts::VariableOp varOp) { - return varOp && isOwningVariable(varOp) && !varOp.getCaptured().value_or(false); - }; - - if (auto varOp = mlir::dyn_cast(taker)) - { - if (!ownSlot(varOp)) - { - return false; - } - - chain.roots.push_back({varOp.getResult(), Chain::Slot}); - return true; - } - - auto storeOp = mlir::dyn_cast(taker); - if (!storeOp) - { - return false; - } - - auto ref = storeOp.getReference(); - if (auto varOp = ref.getDefiningOp()) - { - if (!ownSlot(varOp)) - { - return false; - } - - chain.roots.push_back({ref, Chain::Slot}); - return true; - } - - if (!isPlace(ref)) - { - return false; - } - - chain = chainOf(nullptr, ref); - if (!chain.handles.empty() || chain.roots.empty()) - { - return false; - } - - // a global's field, or a captured variable's: not a place of this function - return llvm::none_of(chain.roots, [](auto &root) { - auto loadOp = root.first.template getDefiningOp(); - return loadOp && (loadOp.getReference().template getDefiningOp() || - isLoadedCell(loadOp.getReference())); - }); - } - - // Is `use`, a use of `value` after `taker` moved it, a read through the place it moved into - // (spec 29)? `made` is where the value is made again (its definition, the local's declaration - // and assignments). Sound when no path from the move through an end or a drop of the place to - // the read - or to anything the read produced - makes the value again or moves it again: such a - // path is the only way the place can stop holding the value before the read. - bool readsThroughOwner(mlir::Operation *taker, mlir::Value value, mlir::Operation *use, - llvm::ArrayRef made) - { - if (!isReadAfterMove(use, value)) - { - return false; - } - - Chain chain; - if (!destinationChain(taker, chain)) - { - return false; - } - - // A read again of the old name after a drop still finds the moved value - the slot is not - // emptied by a move - so neither the read nor the slot's read is where it is made again. - llvm::SmallVector kills(made.begin(), made.end()); - kills.push_back(taker); - llvm::SmallVector uses; - uses.push_back({use, {kills.begin(), kills.end()}}); - for (auto result : use->getResults()) - { - if (!mayPointInto(result)) - { - continue; - } - - // what the read produced - a field's reference, a bound method - still points into the - // block; a call's result that borrows its argument is the value again and may not be kept - if (walkBorrowed(result, kills, uses, nullptr, Keeping{nullptr, false}, false, !!borrowingCall(result))) - { - return false; - } - } - - // a method reached through it whose callee keeps its `this`: a second owner - for (auto &borrowUse : uses) - { - if (!isCall(borrowUse.op)) - { - continue; - } - - auto args = callArgs(borrowUse.op); - for (auto index : ownedParams(borrowUse.op)) - { - if (static_cast(index) < args.size() && - mlir::isa_and_nonnull(args[index].getDefiningOp())) - { - return false; - } - } - } - - auto outlives = [&](mlir::Operation *end) { - if (end == taker || !reachableAfter(taker, end, made)) - { - return false; - } - - return llvm::any_of(uses, [&](BorrowUse &borrowUse) { - return (borrowUse.op != end || !borrowUse.callee) && reachableAfter(end, borrowUse.op, borrowUse.kills); - }); - }; - - for (auto *end : rootEnds(chain)) - { - if (outlives(end)) - { - return false; - } - } - - for (auto *drop : drops) - { - if (dropsChain(drop, chain, nullptr, value) && outlives(drop)) - { - return false; - } - } - - return true; - } - - // Where rc takes the receiver's reference: its `ts.Retain` of the value, when that comes - // first in the taker's block, else the taker itself (a declaration, whose `ts.RetainSlot` - // follows it). A release between the two - a `return`'s scope exit runs after its retain - // and before the store into the return slot - is after the move. - static mlir::Operation *acquirePoint(mlir::Operation *taker, mlir::Value value) - { - // a store into a record, which rc retains as a whole after it is filled - if (auto *retain = recordRetainOfStore(taker)) - { - return retain; - } - - mlir::Operation *first = taker; - forEachUse(value, [&](mlir::Operation *user, mlir::Value) { - if (mlir::isa(user) && user->getBlock() == taker->getBlock() && - user->isBeforeInBlock(first)) - { - first = user; - } - }); - - return first; - } - - // Did rc give this taker a reference of its own - a `ts.Retain` of the value in front of it, - // or the `ts.RetainSlot` of an owning local declared from it? Only then is there a release on - // the taker's side to give the value back once it has moved. `ts.NewInterface` is a taker to - // this pass but a view to rc, which retains nothing for it and relies on the instance's own - // release; moving the instance into it would leave nothing to release it at all. - static bool takerAcquires(mlir::Operation *taker, mlir::Value value) - { - // a callee that keeps its argument retained it in its own body under rc - if (isCall(taker)) - { - return true; - } - - if (recordRetainOfStore(taker)) - { - return true; - } - - if (auto varOp = mlir::dyn_cast(taker)) - { - return llvm::any_of(varOp.getResult().getUsers(), - [](mlir::Operation *user) { return mlir::isa(user); }); - } - - auto acquires = false; - forEachUse(value, [&](mlir::Operation *user, mlir::Value) { - acquires = acquires || (mlir::isa(user) && user->getBlock() == taker->getBlock() && - user->isBeforeInBlock(taker)); - }); - - return acquires; - } - - // The declaration's name survives only as debug metadata (`--di`). At this level MLIRGen's - // form of it is a NameLoc first inside the location fused with the scope - what LowerToLLVM's - // preserveTypesForDebugInfo reads - and after that pass it is a DILocalVariable. Without - // `--di` the error still points at the right place. - static llvm::StringRef varName(mlir_ts::VariableOp varOp) - { - return nameAt(varOp.getLoc()); - } - - static llvm::StringRef nameAt(mlir::Location location) - { - if (auto fused = mlir::dyn_cast>(location)) - { - if (auto name = fused.getMetadata().getName()) - { - return name.getValue(); - } - } - - if (auto scoped = mlir::dyn_cast>(location)) - { - if (auto named = mlir::dyn_cast_or_null(scoped.getLocations().front())) - { - return named.getName().getValue(); - } - } - - return "this value"; - } - - static llvm::StringRef describe(mlir::Value value) - { - llvm::StringRef name = "this value"; - forEachUse(value, [&](mlir::Operation *user, mlir::Value) { - if (auto varOp = mlir::dyn_cast(user); varOp && name == "this value") - { - name = varName(varOp); - } - }); - - return name; - } - - // A second reference nobody proved safe. `value` is the value it is to, when the caller knows - // it; a retain names its own. The shapes this pass recognises get the rule they break (spec - // 21.2); anything else, the catch-all. - void reportSecondReference(mlir::Operation *op, mlir::Value value = {}) - { - if (!reporting()) - { - return; - } - - llvm::StringRef name = "this value"; - if (auto retainSlotOp = mlir::dyn_cast(op)) - { - if (auto varOp = retainSlotOp.getSlot().getDefiningOp()) - { - name = varName(varOp); - } - } - else if (auto varOp = mlir::dyn_cast(op)) - { - name = varName(varOp); - } - - if (!value && mlir::isa(op)) - { - value = retainedValue(op); - } - - if (!value || !reportShapeOfSecondReference(op, rootOf(value), name)) - { - auto diag = op->emitError("'") << name << "' takes a second reference; -mm=own cannot prove a move or a borrow here yet"; - noteLostFacts(diag); - } - - signalPassFailure(); - } - - // The named shapes of a second reference to `root` (spec 21.2). False when it is none of them. - bool reportShapeOfSecondReference(mlir::Operation *op, mlir::Value root, llvm::StringRef name) - { - auto f = getFunction(); - - // returned, where another return gives a borrow of an argument and this one does not, or - // the other way round: the callers cannot be told which they get - if (isReturned(root)) - { - auto param = borrowedParam(root); - if (auto *other = otherReturn(param)) - { - auto diag = op->emitError("'") << f.getName() - << "' returns a borrow of its argument on some paths and another value on " - "others; -mm=own cannot tell its callers whether they own the result"; - diag.attachNote(other->getLoc()) - << (param >= 0 ? "returns another value here" : "returns a borrow of its argument here"); - return true; - } - } - - // a parameter's value, or a part of it, returned or kept: a fact its callers must know - // (spec 15), and noteLostFacts says why they cannot - if (auto param = borrowedParam(root); param >= 0) - { - auto diag = op->emitError("'") << (name == "this value" ? paramName(param) : name); - if (isReturned(root)) - { - diag << "' is a parameter, returned here, but -mm=own cannot tell the callers of '" << f.getName() - << "' that the result borrows it"; - } - else - { - diag << "' is a parameter, kept here, but -mm=own cannot make the callers of '" << f.getName() - << "' give it up"; - } - - noteLostFacts(diag); - return true; - } - - // a store into a local whose value is not this function's to replace: a parameter's slot, or - // a local that owns nothing (MLIRGen's takeOwnershipOfLocal) - auto storeOp = mlir::dyn_cast(op); - if (auto slot = storeOp ? storeOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); - slot && !isOwningVariable(slot) && !isResultSlot(slot)) - { - // a parameter's slot, assigned (isParameterSlot is the unassigned one) - auto argument = mlir::dyn_cast_or_null(slot.getInitializer()); - if (argument && argument.getOwner()->isEntryBlock() && - mlir::isa(argument.getOwner()->getParentOp())) - { - op->emitError("'") << varName(slot) << "' is a parameter and cannot be assigned under -mm=own yet"; - return true; - } - - // MLIRGen makes no owner of a local declared without a value, nor of one declared in a - // catch or finally clause, whose release would be a call inside an exception funclet - if (!slot.getInitializer()) - { - op->emitError("'") << varName(slot) - << "' owns nothing under -mm=own (it is declared without a value, or in a catch or " - "finally clause), so it cannot be given a new value here"; - return true; - } - } - - // merged from branches or loop iterations - if (auto argument = mlir::dyn_cast(root); argument && !argument.getOwner()->isEntryBlock()) - { - op->emitError("'") << name - << "' is merged from several branches (`?:`, `&&`, `||`, `??`, a loop); -mm=own cannot " - "prove which one owns it yet"; - return true; - } - - auto loadOp = root.getDefiningOp(); - - // a read of a global - if (auto addressOfOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::AddressOfOp()) - { - // a local that reads it: phase 3's borrow, which a call or an assignment of the global - // may end - if (mlir::isa(op)) - { - op->emitError("'") << name << "' borrows the global '" << addressOfOp.getGlobalName() - << "', but -mm=own cannot prove the global keeps its value while '" << name - << "' is used: a call or an assignment may overwrite it"; - return true; - } - - op->emitError("'") << name << "' borrows the global '" << addressOfOp.getGlobalName() - << "' and cannot be stored, returned or captured"; - return true; - } - - // a read of a captured variable, through its cell - if (loadOp && isLoadedCell(loadOp.getReference())) - { - op->emitError("'") << name << "' borrows a captured variable and cannot be stored, returned or captured"; - return true; - } - - // an object literal built in a local, holding a value it owns: moved only into an owning - // local or the result (recordRetainOf) - auto record = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); - if (!record && storeOp) - { - auto propertyRefOp = storeOp.getReference().getDefiningOp(); - record = propertyRefOp ? propertyRefOp->getOperand(0).getDefiningOp() : mlir_ts::VariableOp(); - } - - if (record && !isOwningVariable(record) && record.getInitializer() && - record.getInitializer().getDefiningOp()) - { - op->emitError("an object literal that holds a value it owns is moved only into a local or " - "returned as it is; -mm=own cannot move it here yet"); - return true; - } - - return false; - } - - // The name of parameter `index`, as its slot carries it (under `--di`). - llvm::StringRef paramName(int index) - { - llvm::StringRef name = "this value"; - getFunction()->walk([&](mlir_ts::VariableOp varOp) { - auto argument = mlir::dyn_cast_or_null(varOp.getInitializer()); - if (argument && argument.getOwner()->isEntryBlock() && static_cast(argument.getArgNumber()) == index) - { - name = varName(varOp); - } - }); - - return name; - } - - // Is `root`, or a view of it, stored into the result slot? - static bool isReturned(mlir::Value root) - { - auto returned = false; - forEachUse(root, [&](mlir::Operation *user, mlir::Value used) { - auto storeOp = mlir::dyn_cast(user); - auto slot = storeOp && storeOp.getValue() == used ? storeOp.getReference().getDefiningOp() - : mlir_ts::VariableOp(); - returned = returned || (slot && isResultSlot(slot)); - }); - - return returned; - } - - // A return that disagrees with one of a borrow of parameter `param` (with -1, with one that - // borrows nothing): a store into the result slot of a value that holds a block and borrows - // another parameter or none - or, for -1, borrows a parameter. Null when there is none. - mlir::Operation *otherReturn(int param) - { - mlir::Operation *found = nullptr; - getFunction()->walk([&](mlir_ts::StoreOp storeOp) { - auto slot = storeOp.getReference().getDefiningOp(); - if (found || !slot || !isResultSlot(slot) || holdsNoBlock(storeOp.getValue())) - { - return; - } - - auto borrows = borrowedParam(storeOp.getValue()); - if (param >= 0 ? borrows != param : borrows >= 0) - { - found = storeOp; - } - }); - - return found; - } - - // Where the body would have been fine had its callers known a fact about it, say why they - // cannot. - void noteLostFacts(mlir::InFlightDiagnostic &diag) - { - auto f = getFunction(); - if (auto why = f->getAttrOfType(OWN_FACTS_LOST_ATTR_NAME)) - { - diag.attachNote(f.getLoc()) << "'" << f.getName() << "' could return a borrow or keep a parameter, but " - << why.getValue(); - } - } - - void reportKeptOnSomePaths(mlir::Operation *move, mlir::Operation *exit, llvm::StringRef name) - { - if (!reporting()) - { - return; - } - - auto diag = move->emitError("'") << name - << "' is moved here on some paths only; -mm=own cannot release it " - "on the others yet"; - diag.attachNote(exit->getLoc()) << "returns here without moving it; the caller gave it up"; - signalPassFailure(); - } - - // A handle made into a union whose other members include one that owns a block (spec 23.2): - // `Shared | string` given a Shared. gc, none and rc accept it. - void reportHandleInOwningUnion(mlir::Operation *op) - { - auto handleIn = llvm::any_of(op->getOperandTypes(), [](mlir::Type type) { - return MLIRTypeHelper::isSharedHandleType(type); - }); - if (!handleIn) - { - return; - } - - for (auto resultType : op->getResultTypes()) - { - if (!MLIRTypeHelper::isSharedInOwningUnionType(resultType)) - { - continue; - } - - if (!reporting()) - { - return; - } - - auto unionType = resultType; - if (auto optionalType = mlir::dyn_cast(unionType)) - { - unionType = optionalType.getElementType(); - } - - mlir::Type shared; - mlir::Type owning; - for (auto member : mlir::cast(unionType).getTypes()) - { - if (mlir::isa(member)) - { - shared = shared ? shared : member; - } - else if (!owning && MLIRTypeHelper::ownsHeapMemoryOfUnionMember(member)) - { - owning = member; - } - } - - auto diag = op->emitError("a ") << printed(shared) << " in a union with "; - if (owning) - { - diag << "'" << printed(owning) << "', which -mm=own does not count,"; - } - else - { - diag << "a member -mm=own does not count,"; - } - - diag << " is not supported"; - signalPassFailure(); - return; - } - } - - static std::string printed(mlir::Type type) - { - llvm::SmallString<128> text; - llvm::raw_svector_ostream out(text); - MLIRPrinter printer{}; - printer.printType(out, type); - return text.str().str(); - } - - void reportGivenNotOwned(mlir::Operation *call, mlir::Value arg) - { - if (!reporting()) - { - return; - } - - llvm::StringRef name = ownerName(arg); - if (auto loadOp = rootOf(arg).getDefiningOp()) - { - if (auto varOp = loadOp.getReference().getDefiningOp()) - { - name = varName(varOp); - } - } - - auto callee = calleeName(call); - auto diag = call->emitError("argument '") << name << "' is given to '" << (callee.empty() ? "a function" : callee) - << "', which keeps it, but -mm=own cannot move it here: this " - "function does not own it"; - noteLostFacts(diag); - signalPassFailure(); - } - - void reportUseAfterMove(mlir::Operation *use, mlir::Operation *move, llvm::StringRef name) - { - if (!reporting()) - { - return; - } - - auto diag = use->emitError("'") << name << "' is used here after its value was moved"; - diag.attachNote(move->getLoc()) << "value moved here"; - signalPassFailure(); - } - - void reportMovedInLoop(mlir::Operation *move, llvm::StringRef name) - { - if (!reporting()) - { - return; - } - - move->emitError("'") << name - << "' is moved inside a loop but was made outside it; only a borrow could do " - "that, and -mm=own cannot prove one yet"; - signalPassFailure(); - } - - void reportBorrowOutlives(mlir::Operation *use, mlir::Operation *end, llvm::StringRef name, llvm::StringRef owner) - { - if (!reporting()) - { - return; - } - - auto diag = use->emitError("'") << name << "' borrows '" << owner << "' but is used here after '" << owner - << "' is released or overwritten"; - diag.attachNote(end->getLoc()) << "'" << owner << "' is released or overwritten here"; - signalPassFailure(); - } - - void reportPlaceBorrowOutlives(mlir::Operation *use, mlir::Operation *drop, llvm::StringRef name, - llvm::StringRef place) - { - if (!reporting()) - { - return; - } - - auto diag = use->emitError("'") << name << "' borrows " << place - << " but is used here after it may be released or overwritten"; - diag.attachNote(drop->getLoc()) << "it may be released or overwritten here"; - signalPassFailure(); - } - - void reportPlaceBorrowerAssigned(mlir::Operation *op, llvm::StringRef name, llvm::StringRef place) - { - if (!reporting()) - { - return; - } - - op->emitError("'") << name << "' borrows " << place << " and cannot be assigned; -mm=own cannot prove that yet"; - signalPassFailure(); - } - - void reportPlaceBorrowEscapes(mlir::Operation *op, llvm::StringRef name, llvm::StringRef place) - { - if (!reporting()) - { - return; - } - - auto diag = op->emitError("'") << name << "' borrows " << place << " and cannot be stored, returned or captured"; - noteLostFacts(diag); - signalPassFailure(); - } - - void reportMovedWhileBorrowed(mlir::Operation *op, llvm::StringRef owner, llvm::StringRef borrower) - { - if (!reporting()) - { - return; - } - - op->emitError("'") << owner << "' is moved here while '" << borrower - << "' borrows it; a borrowed value cannot be moved"; - signalPassFailure(); - } - - void reportBorrowEscapes(mlir::Operation *op, llvm::StringRef name, llvm::StringRef owner) - { - if (!reporting()) - { - return; - } - - auto diag = op->emitError("'") << name << "' borrows '" << owner << "' and cannot be stored, returned or captured"; - noteLostFacts(diag); - signalPassFailure(); - } - - void reportBorrowerAssigned(mlir::Operation *op, llvm::StringRef name, llvm::StringRef owner) - { - if (!reporting()) - { - return; - } - - op->emitError("'") << name << "' borrows '" << owner << "' and cannot be assigned; -mm=own cannot prove that yet"; - signalPassFailure(); - } - - void reportEscapingCellNotOwned(mlir::Operation *capture, mlir::Operation *escape, llvm::StringRef why) - { - if (!reporting()) - { - return; - } - - auto diag = capture->emitError("a closure that escapes owns what it captures, but -mm=own cannot move this " - "variable into it: ") - << why; - diag.attachNote(escape->getLoc()) << "the closure escapes here"; - signalPassFailure(); - } - - void reportUsedAfterEscapingCapture(mlir::Operation *use, mlir::Operation *capture, llvm::StringRef name) - { - if (!reporting()) - { - return; - } - - auto diag = use->emitError("'") << name << "' is captured by a closure that escapes, and is still used here"; - diag.attachNote(capture->getLoc()) << "the closure takes it here"; - signalPassFailure(); - } - - void reportClosureOutlives(mlir::Operation *use, mlir::Operation *end, llvm::StringRef closure, - llvm::StringRef captured) - { - if (!reporting()) - { - return; - } - - auto diag = use->emitError("'") << closure << "' borrows '" << captured << "', which it captures, but runs here after '" - << captured << "' is released"; - diag.attachNote(end->getLoc()) << "'" << captured << "' is released here"; - signalPassFailure(); - } - - void reportCaptureNotOwned(mlir::Operation *op, llvm::StringRef captured) - { - if (!reporting()) - { - return; - } - - op->emitError("'") << captured << "' is captured by a closure, but -mm=own cannot tell who owns its value here"; - signalPassFailure(); - } - - void reportCapturedParamAssigned(mlir::Operation *op, llvm::StringRef name) - { - if (!reporting()) - { - return; - } - - op->emitError("'") << name - << "' is a captured parameter and cannot be assigned under -mm=own: its value is the " - "caller's"; - signalPassFailure(); - } - - void reportMovedOnSomePaths(mlir::Operation *move, mlir::Operation *release, llvm::StringRef name, - mlir::Operation *usedAt = nullptr) - { - if (!reporting()) - { - return; - } - - auto diag = move->emitError("'") << name - << "' is moved here on some paths only; -mm=own cannot release it " - "on the others yet"; - diag.attachNote(release->getLoc()) << "released here on every path"; - if (usedAt) - { - diag.attachNote(usedAt->getLoc()) - << "used here, after the paths join, so it cannot be released before they do"; - } - - signalPassFailure(); - } -}; - -} // end anonymous namespace +} // namespace own_inference #undef DEBUG_TYPE std::unique_ptr mlir_ts::createOwnershipInferencePass() { - return std::make_unique(); + return std::make_unique(); } diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.h b/tslang/lib/TypeScript/OwnershipInferencePass.h new file mode 100644 index 000000000..31fd373bb --- /dev/null +++ b/tslang/lib/TypeScript/OwnershipInferencePass.h @@ -0,0 +1,1220 @@ +// OwnershipInferencePass, the per-function inference of -mm=own. Its methods are defined across +// the lib/TypeScript/OwnershipInference*.cpp files, one analysis each; templates stay here. + +#ifndef MLIR_TYPESCRIPT_OWNERSHIPINFERENCEPASS_H +#define MLIR_TYPESCRIPT_OWNERSHIPINFERENCEPASS_H + +#include "mlir/Pass/Pass.h" +#include "mlir/IR/Dominance.h" +#include "mlir/Dialect/LLVMIR/LLVMAttrs.h" +#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h" +#include "mlir/Interfaces/ControlFlowInterfaces.h" +#include "mlir/Interfaces/FunctionInterfaces.h" + +#include "TypeScript/TypeScriptDialect.h" +#include "TypeScript/TypeScriptOps.h" +#include "TypeScript/TypeScriptFunctionPass.h" +#include "TypeScript/Passes.h" +#include "TypeScript/Defines.h" +#include "TypeScript/MLIRLogic/MLIRTypeHelper.h" + +#include "OwnershipFacts.h" + +#include "llvm/ADT/MapVector.h" +#include "llvm/ADT/ScopeExit.h" +#include "llvm/ADT/SetVector.h" +#include "llvm/ADT/SmallPtrSet.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/STLFunctionalExtras.h" +#include "llvm/ADT/StringSet.h" +#include "llvm/Support/raw_ostream.h" + +#include +#include + +namespace mlir_ts = mlir::typescript; + +namespace own_inference +{ + +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" + +// 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. +// +// A fresh heap value may have at most one owner: one use that takes it - a local's +// declaration, a store into a field, element, global or return slot, an insertion into an +// array, a return - or, with none of those, the temporary itself, which rc gives back with a +// `ts.Release`. That owner's release is then its destroy, and every retain rc planted on the +// way is erased. Owners are counted from the uses themselves, not from rc's retains: rc also +// transfers ownership by *consuming* a value (`__owned_consumed`, a push, a field store), +// which leaves no retain behind to count. +// +// A move is decided by reachability on the affine CFG (spec 4.2 step 3): nothing may use the value +// after its taker, a taker that control comes back to without the value being made again is a +// move on every loop iteration (2.6), and each release the move reaches must be dominated by it +// and is erased. A release the move cannot reach stays - the owner on the paths that did not +// move. A local's release that paths which moved it and paths which did not both reach is placed +// instead on the edges where the latter join the former (spec 30). Anything else is an error at +// its location. +// +// It runs on every function: a `ts.Func`, and the `func.func` the async lowering outlines an +// `await`'s continuation or a `for await` body into, whose arguments are the values it captures. +class OwnershipInferencePass + : public mlir::PassWrapper> +{ + public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(OwnershipInferencePass) + + void runOnOperation() override; + + mlir::FunctionOpInterface getFunction(); + + void runOnFunction(); + + // The analysis of one function; true when it passed. In a dry run it erases nothing and strips + // no facts. + bool analyze(llvm::SetVector &toErase); + + // Each analysis starts from nothing this function has not set up again. + void resetFunctionState(); + + // What the ownership operations of one function name, in the order the walk meets them + // (collectOwnershipOps). + struct FunctionOps + { + // every value some ownership operation names, in the order the walk meets them + llvm::SetVector candidates; + llvm::SmallVector retains; + // reads out of fields and elements, and results that borrow an argument, and what may + // destroy what they read (spec 2.3, 2.4) + llvm::SmallVector placeReads; + // reads of what a mortal global holds (isGlobalRead) + llvm::SmallVector globalReads; + // calls whose callee keeps an argument: each such argument moves into the call + llvm::SmallVector keepingCalls; + }; + + struct SlotReceiver + { + mlir::Operation *retain; + mlir::Value value; + mlir_ts::LoadOp load; + }; + + // the receivers of each owning local, or of each parameter this function keeps + using Receivers = llvm::MapVector>; + + // Every op of the function that names an ownership: the values to check, the retains to + // decide, the reads out of places and globals, the calls that keep an argument, and the drops. + void collectOwnershipOps(FunctionOps &ops, llvm::SetVector &toErase); + + // The owner check of each value, then of each read out of a place and of a mortal global. The + // fresh values proven, whose retains go. + llvm::DenseSet checkOwnershipOps(const FunctionOps &ops, llvm::SetVector &toErase); + + // Each retain: erased, given to the receivers of an owning local or of a kept parameter, or + // reported. + void decideRetains(const FunctionOps &ops, const llvm::DenseSet &proven, Receivers &slotReceivers, + Receivers ¶mReceivers, llvm::SetVector &toErase); + + // The arguments a callee keeps, given to the receivers of the local or kept parameter they come + // from. + void giveKeptArguments(const FunctionOps &ops, Receivers &slotReceivers, Receivers ¶mReceivers); + + // The facts were for this pass only. + void stripFacts(); + + private: + mlir::DominanceInfo *dominance = nullptr; + + // What in this function may destroy a value held by a field or an element: an overwrite of a + // field or an element, an array op that removes elements, a call. + llvm::SmallVector drops; + + // derivedFrom's answer for each value it was asked about in this function; held by pointer, so + // an answer stays put while the next one is added + llvm::DenseMap>> derivedCache; + + // While non-zero, verdicts are tried without reporting: the next verdict gets its turn first. + unsigned quiet = 0; + + // The dry run (spec 22.3): the whole analysis, but nothing reported and nothing changed. A + // report it would make is counted in `failures`. + bool dryRun = false; + unsigned failures = 0; + // attributes the dry run set on ops that did not have them, taken off again after it + llvm::SmallVector> dryRunAttrs; + + // Whether a report is to be emitted: not under `quietly`'s trial, and not in the dry run, which + // only counts it. + bool reporting(); + + // `op->setAttr(name, unit)`, undone after the dry run when the op did not have it + void setAttrTracked(mlir::Operation *op, llvm::StringRef name); + + // Strings as values (spec 22.3): each string retain the dry run did not erase is rewritten, where + // its one use can be found, into a copy for that use. + void makeStringCopies(const llvm::SetVector &erased); + + // The value a parameter of this function is, when its callers were told this function keeps it + // or returns a borrow of it (spec 15): copying it would leak what the caller gave up. + bool borrowsKnownParam(mlir::Value value); + + // Rewrites one string retain into a copy for the use it was made for (spec 22.3), and erases it; + // false, leaving it for the real run to report, where there is no single such use in its block, + // or the value is not a string or is a known parameter's (spec 22.4). Its use walk is takerOf's + // without the twinReadAfter fallback: a retain whose value has no use of its own is not copied, + // so its error stays. + bool copyForRetain(mlir::Operation *retain); + + // The parameter this function's result borrows (`__own_result_borrows`), or -1; and the + // parameters (or views of them) whose rc reference for the caller was already decided. + int returnsBorrowOf = -1; + llvm::DenseSet returnedParams; + + // The parameters this function's result is a borrowing record of (OWN_RESULT_RECORD_ATTR_NAME, + // spec 32); empty for none. + llvm::SmallVector returnsRecordOf; + + // ---- Closures (spec 2.5, phase 5) ---- + // + // A closure is `ts.CreateBoundFunction(box, @f) {__owns_capture}`; its box holds a cell for each + // variable it captures by reference and a copy of each it captures by value. One that does not + // escape borrows all of it: rc's references for the box go, the box frees itself only + // (OWN_BORROWS_CAPTURES_ATTR_NAME), and nothing that may run the closure may come after anything + // it borrows is given back. One that escapes would own all of it, which is not done yet. + struct Closure + { + mlir_ts::CreateBoundFunctionOp op; + mlir::Value box; + // each store that fills the box, with the field it fills + llvm::SmallVector> fills; + bool hasCells = false; + // the first use that takes the closure out of this function's hands + mlir::Operation *escape = nullptr; + // what may run the closure: calls given it, called through it, or through its box + llvm::SmallVector uses; + // the calls through the box itself, made without the closure value (a folded `const f`) + llvm::SmallVector boxUses; + // where the closure value is given back: its releases, and those of the `let`s holding it + llvm::SmallVector ends; + // rc's references for the box: `ts.RetainCell` of each cell, `ts.Retain` of each copy + llvm::SmallVector retains; + // held by a local that owns nothing (`let f: () => number; f = () => ...`): an alias, so + // the closure's own releases stay its owner + bool aliased = false; + // a generator's box whose state object borrows from the generator's arguments + // (OWN_BORROWING_FIELDS_ATTR_NAME): a parameter's cell moves in, and the box frees the cell only + bool borrowsValues = false; + // copies only, and given to a generator's maker that borrows from it (`.map(f)`): the box + // borrows its copies, as a box with cells would + bool borrowsCopies = false; + }; + + llvm::SmallVector> closures; + // a copy stored into a borrowing box -> its closure + llvm::DenseMap captureStores; + // rc's references for boxes, which the closures decide + llvm::DenseSet captureRetains; + // each closure over a cell, by its `ts.CreateBoundFunction` + llvm::DenseMap closureOf; + // each generator's capture box, which escapes with its state object (classifyStateBoxes) + llvm::DenseMap stateBoxes; + // rc's retains for stores into borrowing fields, and those fields' releases (classifyBorrowingFields) + llvm::DenseSet fieldBorrowRetains; + llvm::SmallVector fieldBorrowReleases; + + // Finds the closures over cells and what they borrow, before anything else: whether a box's + // stores are reads or takers depends on it. A closure with copies only keeps what phase 4 does + // with it - its box owns them, and each copy moves in. + void classifyClosures(); + + // The fields of a generator's state object of this type that borrow from its arguments + // (OWN_BORROWING_FIELDS_ATTR_NAME, the signature pass's), the `.captured` box's first. + llvm::SmallVector borrowingFieldsOf(mlir::Type recordType); + + // Is `ref` a field of a generator's state object that only yields (OWN_YIELDED_FIELDS_ATTR_NAME)? + bool isYieldedField(mlir::Value ref); + + // Is `ref` a field of a generator's state object that borrows (other than the `.captured` box)? + bool isBorrowingField(mlir::Value ref); + + // A store into a borrowing field holds what it stores without owning it, as a borrowing box's + // 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(); + + // 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))`? + bool isBorrowingCell(mlir::Value ref); + + // Is `ref` a field of the `.captured` box of a generator's state object that borrows: + // `ts.PropertyRef(ts.Load(ts.PropertyRef(state, captured)), i)`? A field copied in by value holds + // the caller's value itself, a cell field the caller's cell. + bool isBorrowingBoxField(mlir::Value ref); + + // What a generator reads out of a borrowing cell or field is the caller's: every use of it must + // be a borrow - a store into another borrowing field is one (classifyBorrowingFields). A yield of + // it, a push, a return, a store anywhere else would give the caller's value a second owner, and + // rc retains nothing for some of these, so nothing else would see it. + void checkBorrowedReads(); + + // A call whose result borrows from its arguments (OWN_RESULT_BOUNDED_ATTR_NAME): a generator over + // a parameter's block. The result is owned as any fresh value is, but nothing may use it after + // one of those arguments is given back - released, assigned, or dropped from the place it was + // read from - and it may not be kept anywhere that could outlive them. Its releases are not + // uses: the state object's release gives back nothing it borrows. + void checkBoundedResults(); + + // The ends of a capture box given to a call with a bounded result: each release of a closure on + // it (the box goes with its last), and each end of what a field of it was filled from. Reports + // and returns false when a use of the result comes after one. + bool checkBoundedByBox(mlir::Operation *call, mlir::Value box, llvm::ArrayRef uses, + llvm::StringRef name); + + // Every use of a bounded result, through its views, the locals that hold it, and the function + // and object a call through it is made with. Answers the first use that keeps it - anything + // that is not a read, a call, a local, or rc's bookkeeping - or null. + mlir::Operation *boundedUses(mlir::Value result, llvm::SmallVectorImpl &uses); + + // A generator's maker puts what the generator captures - its parameters, an outer local - into + // a box, and the box into its state object's `.captured` field: the box of a closure that + // escapes, with the state object, at that store. There is no closure value, so no use runs it + // here and nothing calls through it. rc's `ts.RetainCell` of the box is the state object's + // reference, which moves in with it. A box used any other way is left to the RetainCell error. + void classifyStateBoxes(); + + // `ts.Store(box, ts.PropertyRef(state, i))`, where `state` is a local a `ts.Constant` + // initializes whose every read fills a block `ts.New` made: a generator's initial state. + static bool isStoreIntoState(mlir::Operation *user, mlir::Value box); + + // rc's reference for the box's copy of `value`: the retain of it nearest before the store that + // fills the box, in the same block. + mlir::Operation *retainBefore(mlir::Value value, mlir::Operation *store); + + // Where a closure value goes: through its views and the owning `let`s that hold it, each use + // that may run it joins `uses`, and each release `ends`. Answers the first use that takes it + // anywhere else - returned, stored, kept by a callee, captured - or null. Anything this does + // not know is such a use. + mlir::Operation *followClosure(mlir::Value closure, llvm::SmallVectorImpl &uses, + llvm::SmallVectorImpl &ends, bool &aliased); + + // Does a call given this value give it a closure, which may run and touch what its box holds? + static bool holdsCaptures(mlir::Value value); + + // The verdicts on the closures over cells: an escape is an error; a borrow must not be run after + // anything it borrows is given back - a cell this function owns at its releases, a copy at its + // owner's ends. A cell an enclosing closure holds and a parameter end nowhere here; a copy of a + // read out of a container is bounded by the read's own check, which sees the closure's uses + // (walkBorrowed). + void decideClosures(llvm::SetVector &toErase); + + // Is this the store that moves a cell into the box of a closure that escapes? + bool movesCellAway(mlir::Operation *op); + + // A closure that escapes owns what its box holds (spec 2.5, phase 5b). Its copies moved in as + // in phase 4. Each cell moves in where the box takes it: that store is the move, rc's + // `ts.RetainCell` for it goes, and so does each of the frame's `ts.ReleaseCell`s the move reaches, + // which it must dominate. Nothing in the frame may use the variable after the move - read it, + // assign it, capture it again - and the move may not come round a loop to a cell made outside + // it. A cell that borrows its value (a parameter's) or one an enclosing closure holds has + // nothing to move. And nothing may call through the box once the closure has gone. + void decideEscaping(Closure &closure, llvm::SetVector &toErase); + + // A captured variable whose value this function does not own - a parameter, `this` - has a cell + // that borrows the value: the cell's release frees the cell only (OWN_CELL_BORROWS_ATTR_NAME), + // and nothing may assign the variable, here or in a closure over it, since the old value is the + // caller's. A variable whose value nobody can be said to own is an error. + void decideCells(); + + // An argument read out of a local is lent to the call, but the arguments are read in order: a + // later one may give the local a new value before the call takes the first - `g(n, (n = mk(), 0))`, + // in a conditional argument too - or call a closure that assigns it, when it is captured. The + // assignment drops the block the argument is, and the callee reads freed memory. rc holds such an + // argument across the call instead (OwnedReturnConsumptionPass, holdGlobalArguments). A read of a + // global is checkGlobalRead's, and a store into a place in the local the place borrows' (a field + // read `g(n.v, (n.v = mk(), 0))`); a store into the local's slot ends `n.v` too. + void checkArgumentsAssigned(); + + // Does a closure built over this captured local assign it (OWN_ASSIGNS_CAPTURES_ATTR_NAME)? Yes too + // when its cell goes where this does not follow. + bool closureAssigns(mlir_ts::VariableOp local); + + template bool quietly(F check) + { + ++quiet; + auto result = check(); + --quiet; + return result; + } + + // Move, else borrow, else error (spec 2.2) for the receivers of one owning local. + void decideSlot(mlir::Value slot, llvm::ArrayRef receivers, llvm::SetVector &toErase); + + void decideSlotReceiver(mlir::Value slot, const SlotReceiver &receiver, llvm::StringRef owner, + llvm::SetVector &toErase, bool sole); + + // Receivers of one owning local that each read a value of its own (#528): `h.c = a; a = new C(); + // g.c = a`. An assignment comes between every two of their reads on every path from one to the + // other, so each is decided alone, as the one receiver of the value it reads. So are receivers + // no path joins at all, assigned or not: `if (c) return a; return a;` moves `a` once on each + // way out. + bool takeValuesOfTheirOwn(mlir::Value slot, llvm::ArrayRef receivers); + + // ---- A kept parameter released where it is not moved (spec 33) ---- + // + // `return v.x > 2 ? acc : V.plus(acc, v)`: `acc` is kept (OwnershipSignaturePass), so the caller + // moved it in; the path that returns it moves it out, and the others must release it. So must a + // callback that only borrows the accumulator its family gives it to keep. MLIRGen made no owner + // of a parameter, so its releases are put in here, before each way out of the body - a return, + // a throw that leaves the function - as MLIRGen puts an owning local's. The move's own check then + // erases those it reaches, and places the release on the edges into the join that did not move + // it (spec 30), statically: no run-time flag. Unwinding out of the function (a call that throws + // outside a `try`, a `finally` passed on the way out) gives nothing back: it leaks, as an owning + // local does there. + + // the releases put in for this run + llvm::SmallVector exitReleases; + + // The slot of parameter `index` (isParameterSlot), or null. + mlir_ts::VariableOp parameterSlot(int32_t index); + + // Is `slot` a parameter this function keeps and releases where it does not move it? + bool isReleasedParam(mlir::Value slot); + + // Does `value` come from a read of `slot`: computed from one, through any op? + static bool readsSlot(mlir::Value value, mlir::Value slot, int depth = 0); + + void releaseKeptParamsAtExits(); + + // A parameter this function keeps (`__own_params`): its caller moved the argument in, and this + // function has no release for it, so exactly one taker must move it on, on every way out of + // the function, and nothing may read it after that. + void decideParam(mlir::Value slot, llvm::ArrayRef receivers, llvm::SetVector &toErase); + + // The thrown value, read out of the exception object by a catch's copy thunk (`.eh.copy.*`, + // which the C++ runtime calls to copy the exception into the catch). rc retains it for the + // copy; own moves it: the exception object never gives its reference back (a throw hands the + // thrower's reference to it, and nothing releases it - rc leaks it the same way), and each + // exception is copied into one catch, since a TypeScript rethrow is a new throw. + bool isReadOutOfException(mlir::Value value); + + // The read of a parameter this function keeps, through its views. None for anything else. + mlir_ts::LoadOp keptParamLoadOf(mlir::Value value); + + // More than one receiver of one owning local. Each has to borrow: a borrow pins the owner, so + // none may move it. A receiver that is not a `let` reports its move error - another + // receiver's reads come after it - or, if its move alone would pass, that a borrow pins it. + void decideSlotReceivers(mlir::Value slot, llvm::ArrayRef receivers, llvm::StringRef owner, + llvm::SetVector &toErase); + + // A `let` that could borrow: owning, declared from a value, holding a reference it took + // itself (a `ts.RetainSlot`, not the value's own consumed one), and not captured. With + // `consumed`, also one that took over the reference rc thought the value carried: a call's + // result that turned out to borrow an argument carries none. + static mlir_ts::VariableOp borrowerOf(mlir::Operation *acquisition, bool consumed = false); + + // tryBorrow for the `let` an acquisition gives a reference: a `ts.Retain` of the value a + // consuming `let` takes over is that `let`'s, and goes with the borrow; a `ts.RetainSlot` is the + // `let`'s own bookkeeping already. + bool borrowWith(mlir_ts::VariableOp borrower, mlir::Operation *acquisition, llvm::ArrayRef ends, + llvm::StringRef owner, llvm::SetVector &toErase); + + // The owning `let` declared from `value` that takes over its reference, or none. + static mlir_ts::VariableOp consumingLetOf(mlir::Value value); + + // Where an owning local stops holding its value: each release of its slot and each store + // into it. + static llvm::SmallVector slotEnds(mlir::Value slot); + + // A borrow (spec 2.2, verdict 2): `borrower` takes no reference and gives none back, so its + // retain and every release of its slot go, and the owner keeps its own. Sound while no use of + // the borrower can run after any of `ends` - the owner's releases and assignments - and while + // nothing it holds is kept: not stored, returned, retained or assigned. A path back to a use + // through the borrower's own declaration is a new borrow, not this one. Reports and returns + // false otherwise. + bool tryBorrow(mlir_ts::VariableOp borrower, llvm::ArrayRef ends, llvm::StringRef owner, + llvm::SetVector &toErase); + + // A use of a borrowed value, and where the value it sees is made again: a path from a release + // to the use through one of `kills` reaches a new value, not this one. For the borrow's own + // uses that is the read or the borrowing `let`; for a use through a merge it is the merge's + // block, and through a local that owns nothing, that local's declaration and assignments. + struct BorrowUse + { + mlir::Operation *op; + llvm::SmallVector kills; + // the function value an indirect call goes through: read when the call starts, not while + // the callee runs + bool callee = false; + }; + + // A borrowing `let`'s slot: each read, and everything it leads to (walkBorrowed), joins + // `uses`, made again at `kills`; its `ts.RetainSlot` and `ts.ReleaseSlot`s are `bookkeeping`, + // which a borrow erases. Answers the first use that would keep what the slot holds - an + // assignment, or anything else that is not a read - or null. + mlir::Operation *collectBorrowerUses(mlir_ts::VariableOp borrower, llvm::ArrayRef kills, + llvm::SmallVectorImpl &uses, + llvm::SmallVectorImpl &bookkeeping); + + static bool isAssignmentOf(mlir::Operation *op, mlir_ts::VariableOp varOp); + + // What `use` hands the very value it uses on to, as that value: a cast or a view of it, the + // argument of the block a branch passes it to (a merge: `a ?? b`), or the reads of a local + // that owns nothing and is assigned it (`for (x of arr)` into an outer `let x`). Answers + // whether the use is one of those, even where nothing comes out of it. Where the value lands + // somewhere that is replaced - a block argument, a local - `renewedAt` gets what replaces it: + // the block's entry, the local's declaration (when it has an initializer: `let l!: V` stores + // nothing, and in a loop the slot keeps the last round's value) and assignments. + static bool passesOn(mlir::OpOperand &use, llvm::SmallVectorImpl &values, + llvm::SmallVectorImpl *renewedAt = nullptr); + + // Where a borrowed value goes. Every use of it, and of what it produces that may still point + // into the block, joins `uses`, made again at `kills`. The value itself - through what passesOn + // hands it on to - must not be kept anywhere; what it produces - a field reference, a value + // loaded through one, a bound method, a catch clause's non-owning local - still points into + // the borrowed block, so each of its uses is a use of the borrow too. A value that cannot point + // anywhere (a number, a boolean) ends the walk. Where passesOn hands the value to something + // that is replaced, the uses beyond are made again where it is replaced instead; the op that + // hands it on is itself a use, bounded by `kills`. `isBorrower` accepts a use of the value + // itself that the caller follows on its own (a borrowing `let`), made again at the kills it is + // given. Answers the first use that would keep the value. + // + // `keeping` names what rc planted around a borrow that it took for an owner and that goes + // with it: the temporary release of a result that borrows an argument (the start's, or that of + // a call it is passed to, which is a borrow of it too), and, in a function whose result + // borrows this very value's parameter, the retain rc made for the caller. Those join + // `bookkeeping`, not `uses`, and the return itself is a use, not a keep. + struct Keeping + { + // no default member initializers: GCC rejects them in a default argument of the class + // that encloses the struct + llvm::SmallVectorImpl *bookkeeping; + bool returns; + }; + + mlir::Operation *walkBorrowed( + mlir::Value start, llvm::ArrayRef kills, llvm::SmallVectorImpl &uses, + llvm::function_ref)> isBorrower = nullptr, + Keeping keeping = Keeping{nullptr, false}, bool countedHandle = false, bool startBorrowed = true); + + // Whether a call hands back a block of the caller's own, which points into nothing it was given: + // walkBorrowed then ends a borrow's walk at the call (the call itself is still a use for as long + // as it runs). Never a generator's state that borrows its arguments (`__own_result_bounded`). + // - A call known to make its result (`__own_fresh_result`: an iterator's `next()`, a field that + // only ever holds one function, another own module's `..new`) - the `{value, done}` record of + // `it.next()` points nowhere into `it`, and what it does after its next call cannot be a use of + // the method read out of `it`. + // - A direct call of a function this module defines (isFresh): its result points into nothing it + // was given but what its facts name - a borrow of an argument (`__own_result_borrows`, never + // fresh), a borrowing record (isFresh, but bounded by what it borrows), a generator's state + // that borrows its arguments - and a body whose callers cannot all know such a fact is an + // error. `getPoint(x, scene.camera)` makes a new vector: a call after it that may destroy + // `scene.camera` does not end that vector (spec 34). + static bool callResultIsOwn(mlir::Operation *call); + + // Is this what an indirect call goes through: its callee value, or, for a method read out of + // a field of an object (`o.m()`, `m` a field holding a function), the object it is bound to, + // which is the container of what was read, not what was borrowed. A method of a class bound + // to a borrowed instance (`h.c.m()`) is not one: its `this` is the borrow. + static bool isCalleeOperand(mlir::OpOperand &use); + + // ---- Borrowing records (spec 32) ---- + // + // An object literal boxed as an interface that holds values it does not own - `this`, a parameter, + // a field read, an owning `let`'s value, a captured variable's - beside fresh values, which move + // in. The signature pass marked its box (OWN_RECORD_BORROWED_FIELDS_ATTR_NAME): its release frees + // the box and the fields it owns, and leaves the others to their owners. So the box has one owner + // like any fresh block - the box's own check - and what it borrows bounds every use of it: a read + // out of a place is checked with the record's uses as its own (walkBorrowed), a local's value + // against the local's ends, a captured variable's against whatever may assign it. Returned, the + // record is the caller's to own and to bound (checkRecordResults), which the function's facts + // must say. Stored anywhere else that may outlive what it borrows, it is an error. + struct RecordWalk + { + llvm::SmallVector uses; + // the first use that would keep the record itself: a store into a place, a callee that keeps + // it, a capture + mlir::Operation *kept = nullptr; + llvm::SmallVector returns; + }; + + struct BorrowedField + { + mlir_ts::StoreOp store; + RecordFieldSource source; + }; + + struct BorrowingRecord + { + mlir_ts::NewOp box; + mlir_ts::VariableOp local; + llvm::SmallVector interfaces; + llvm::SmallVector borrowed; + RecordWalk walk; + }; + + llvm::SmallVector> records; + // a store of a place read into a borrowing record's field -> the record's uses, which are the + // read's uses too (walkBorrowed) + llvm::DenseMap recordStoreUses; + + // Can a value of this type point into a heap block - a reference into one, a block of its own, + // or a method bound to one? A number or a boolean read out of a borrowed block cannot. + bool mayPointInto(mlir::Value value); + + // The borrowing record built in `varOp` (classifyRecords), or null. + BorrowingRecord *borrowingRecordOf(mlir_ts::VariableOp varOp); + + // The boxes the signature pass marked as borrowing records, where every field still holds what it + // was marked for: a borrowed field a value this pass bounds (recordFieldSource), any other one a + // fresh value or nothing. A store into a borrowed field takes nothing, and whatever uses the record + // reads what it stored (walkBorrowed, through recordStoreUses). A box that is not what it was + // marked for is left to the record's retain, which is then an error. + void classifyRecords(); + + // Every use of a borrowing record (spec 32), and of what is read out of it - each may read what + // the record borrows - through its views, merges, and the locals that hold it, made again at + // `kills`. Its own retains and releases are the box's, which its own check decides. The record + // itself is `kept` when it is stored anywhere but a local, given to a callee that keeps it, or + // captured; returned, it is in `returns`. A call given it hands back its own block, unless that + // is a borrowing record that holds it, whose uses are then this one's as well. + void walkRecord(llvm::ArrayRef starts, llvm::ArrayRef kills, RecordWalk &walk); + + // The name the record goes by: the local that holds it, or a folded `const`'s. + llvm::StringRef recordName(mlir::Value value); + + // What a borrowed field holds, for an error: `'ray'`, `'this.pos'`, `'livec'`, ... + std::string describeRecordSource(BorrowedField field); + + // A record this function makes (spec 32): kept, it is an error; returned, each field it borrows + // must be a parameter's that this function's facts lend (`__own_result_record`), so that its + // callers bound it; and no use of it may come after what a field borrows may be released or + // overwritten - an owning local's ends, and whatever may assign a captured variable. A parameter + // and `this` are the caller's for the whole call; a read out of a place has the record's uses as + // its own (checkPlaceRead). + void checkBorrowingRecords(); + + // A call whose result is a borrowing record (OWN_RESULT_RECORD_ATTR_NAME, spec 32): the box is + // this function's to own - it is fresh, and its own check decides its moves and releases - but + // it may hold a borrow of each argument the facts name, as a call's result that borrows them + // does (checkPlaceRead): somewhere under each. So no use of it, through the locals it moves + // into, may come after one of those arguments may be released or overwritten, and it may not be + // kept. Returned, the arguments must be parameters this function's own facts lend. Its releases + // are not uses: they free only what the record owns. + void checkRecordResults(); + + // ---- Reads out of containers (spec 2.3, phase 3) ---- + // + // `h.c` and `arr[i]` read a value the container owns. Such a read is a borrow: it may not be + // kept, and none of its uses - nor any use of what it produces - may run after something that + // may destroy it (spec 2.2's dropping mutations): an overwrite of a place on the way from the + // read up to its root, an array op that removes elements, an end of the root itself, or a call + // that may reach the root. A path back to a use through the read itself is a new borrow. + + // A read of a heap value out of a field or an element, or a call whose result borrows an + // argument (spec 2.4, phase 4), seen through its views. Null for anything else. + mlir::Operation *placeReadOf(mlir::Value value); + + // How a handle read out of a place is used (spec 23.3), seen through casts, merges and locals + // that own nothing. Every use is classified, and nothing falls through (Ruling 9): + // - `counted`: a receiver that takes a count rc made for that very use + // (isCountedHandleReceiver) - a store, a push or an unshift, `new Shared`, an array + // literal's element, a return, an `any` box, or an owning local's initializer; + // - passed on: a cast or a view that is still a handle, a merge, a local that owns nothing - + // whose own uses are classified in turn; + // - `borrowed`: everything else. A use that reads through the place (`f(p, p.child)`, a + // `.value` read, `Shared.count`), and every use that is neither of the above: a tuple's + // element, an object literal's field, a capture, a `yield`, a receiver rc made no retain + // for, any op not named here. The read is then a borrow, and tenure-checked + // (checkPlaceRead). + // rc's retains count for nothing on their own: each is the count of the receiver right after + // it. `cur = cur.value.next` is counted only. + struct HandleReadUses + { + bool counted; + bool borrowed; + }; + + static HandleReadUses handleReadUses(mlir::Value value); + + // A handle read out of a place that rc gives a count of its own is a counted copy, not a borrow + // (spec 23.3) - but only when every use of it is one rc counts (Ruling 7), and a use that is + // not known to be one is not (Ruling 9). A read with one use that reads through the place as + // well (`f(p, q, q.child = p.child)`), or that keeps it with no count (`[(q.child = p.child), + // 1]`), is a borrow, and that use is tenure-checked; the counted uses are copies + // (checkPlaceRead). + static bool isCountedHandleRead(mlir::Value value); + + // A use that gives a handle an owner rc counts, with rc's retain for that very use: rc's + // retain itself, a store into a field, an element, a global, an owning local or the result + // slot (a return) that rc retained for (countedStoreRetain); a push or an unshift, `new + // Shared`, an array literal's element, an `any` box, a `ts.ReturnVal`, each right after rc's + // retain of what it takes (retainedRightBefore); or the initializer of an owning local, which + // takes a count of its own (`ts.RetainSlot`). Any of them without its retain is not one: it + // keeps the read without a count (`{ s: (q.child = p.child) }`'s field). + static bool isCountedHandleReceiver(mlir::Operation *user, mlir::Value value); + + // A use that passesOn hands on but that takes the value: a return's store into the function's + // result slot (isResultSlot), and a cast that boxes it into `any`. + static bool isHandOff(mlir::Operation *user, mlir::Value value); + + // A return or an `any` box of a handle that rc counts (walkBorrowed). + static bool isCountedHandOff(mlir::Operation *user, mlir::Value value); + + // rc's retains for what `op` takes: MLIRGen puts them in a row right before it + // (mlirGenRetainCaptured, retainInsertedElements), one for each operand it does not take + // over. Answers whether that row retains `value` once for each time `operands` hold it. A + // retain is right before one op only, so no other receiver can count it as well. + static bool retainedRightBefore(mlir::Operation *op, mlir::ValueRange operands, mlir::Value value); + + // The releases of the old value that rc's counted stores of a handle read make: `ts.Retain` of + // the value, `ts.ReleaseSlot` of the slot, `ts.Store` of the value into it, in a row. Seen + // through the read's casts, merges and locals that own nothing, as handleReadUses sees it. + static void collectCountedStoreReleases(mlir::Value value, llvm::SmallPtrSetImpl &releases); + + // rc's retain for a store of a value, when it made one: `ts.Retain` of the value right before + // the store, or before the `ts.ReleaseSlot` of the old value right before it - + // `Retain(v)`, [`ReleaseSlot(slot)`], `Store(v, slot)`. A return's store into the result + // slot comes after the scope exit's releases of the frame's locals and their slots: + // `Retain(v)`, [`Release(..)`, `ReleaseSlot(..)`...], `Store(v, result)`. Null for a store rc + // counts nothing for. + static mlir::Operation *countedStoreRetain(mlir_ts::StoreOp storeOp); + + // The call a value is the result of, when that result is a plain borrow of an argument. A borrowing + // record (spec 32) is not one: the caller owns the box (checkRecordResults). + static mlir::Operation *borrowingCall(mlir::Value value); + + // The places from a read up to its roots, and the roots: the values the function reached the + // first container through. A container reached through a merge (`(c ? a : b).x`) has a root + // on each way in. + struct Chain + { + enum Kind + { + Slot, // an owning local, or a value-type local reached by reference + Owned, // a value this function made + NotOwned // a parameter, `this`, a global, anything else + }; + + // a field's position and reference type; an element is position -1, a handle's payload + // (`s.value`) is SharedValue, a field reached through an interface is InterfaceField: its + // position in the object is not known here (spec 28) + llvm::SmallVector> places; + static constexpr int64_t SharedValue = -2; + static constexpr int64_t InterfaceField = -3; + llvm::SmallVector> roots; + // The roots that are handles - what a `s.value` was read through - each also NotOwned + // among `roots` (spec 23.3). Known from the op, not the root's type: a view may hide it. + llvm::SmallVector handles; + // Somewhere under a root, but where is not known: a result that borrows an argument may + // be any field or element under it. Every overwrite and every removal drops it. + bool anyPlace = false; + + bool owned() const + { + return llvm::none_of(roots, [](auto &root) { return root.second == NotOwned; }); + } + }; + + // With `place`, the chain of that place itself: where a store into it moves a value (spec 29). + Chain chainOf(mlir::Operation *read, mlir::Value place = {}); + + // Where the roots stop holding what the chain reads: a local's releases and assignments and + // every declaration or retain that may move it away; a made value's releases and every use + // that takes it; a global's assignments. + llvm::SmallVector rootEnds(const Chain &chain); + + // What a call has to be given to reach a root: the root, and where the root is a local + // declared from another (a borrow, or a move that left the other dead), that one too. + static llvm::SmallVector rootSources(const Chain &chain); + + // Everything that may point into what `start` points to: the forward closure over users' + // results that may point anywhere. A call's result is not followed: the call hands back a + // value it owns. + const llvm::DenseSet &derivedFrom(mlir::Value start); + + // May this op of the function's drops destroy what the chain reads? An overwrite of a place + // on it, matched by position and type (two references to one field of one object always + // agree on both; an unrelated field that happens to agree only adds an error); an array op + // that removes elements of the type of an element on it; a call whose callee may destroy + // anything (`__own_no_drops`, phase 4) - any such call, for a root this function does not + // own, since the owner may be a global the callee assigns; else one given something derived + // from the root that the read did not produce. + // `borrowed`, when given, is the value the borrow holds in place of the read's result (a value + // moved into the place, spec 29), and `read` may then be null. + bool dropsChain(mlir::Operation *drop, const Chain &chain, mlir::Operation *read, mlir::Value borrowed = {}); + + // A read out of a container is a borrow of it (spec 2.3), and so is a `let` declared from one: + // its retain and releases go when the check passes, as for any borrowing `let` (phase 2). + // 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 local that holds a borrow through `user`, and the field path under it that holds it (empty: + // the local's value itself): a store of it into the local, or into a field of the local's value + // tuple at any depth (an object literal built in place), or the local declared from it. + struct BorrowHolder + { + mlir_ts::VariableOp local; + llvm::SmallVector path; + }; + + static std::optional borrowHolderOf(mlir::Operation *user); + + // Where the holder is given a new value: the local's declaration when it has an initializer + // (`let l!: V` stores nothing, and in a loop the slot keeps the last round's value: fix-up + // re-review N4), and every store into the field path that holds the borrow or into a prefix of + // it (the whole local, `o.p` for `o.p.v`). A store into a sibling field, or below the path, + // gives the path no new value. + static llvm::SmallVector holderRenewals(BorrowHolder holder); + + // Whether one field path is on the way to the other: a read of either may reach the other's value. + static bool pathsMeet(llvm::ArrayRef a, llvm::ArrayRef b); + + // A read of what a mortal global holds (spec 25: an immortal one's one store is its + // initializer): a heap value it owns, not a handle (spec 23.3 counts those). + bool isGlobalRead(mlir_ts::LoadOp loadOp); + + // Such a read is a borrow of the value the global owns - `const c = K.w` names the read itself, + // with no retain - and an assignment of the global releases it. So no use of the read, nor of + // what it produces (walkBorrowed), may run after one in this function. A path back to a use + // through the read itself is a new read. A call that may assign the global, itself or through + // its callees, is such an assignment too (follow-up 11: `const c = G; reset(); print(c.x)`); the + // read given to that call as an argument is #639's. (C1 of the fix-up review: `const c = K.w; K.w = new V(i + 1); print(c.x)` in a loop + // read the block the assignment freed.) + // + // Where the walk meets a keep, the read is either followed into what holds it or rejected - + // never let go unchecked (fix-up re-review N2: a kept read is not always another check's). A + // local holds it (borrowHolderOf): an object literal built in place (`const o = { v: c }`, + // `{ p: { v: K.w } }`), a `let` or a destructured local declared or assigned from it + // (`let l = o.v`, `let { v } = o`). Every use of that local under the path that holds the borrow + // - its reads, a copy, a spread (`ts.DeconstructTuple`), what those lead to - is a use of the + // read, made again only where the local is given a new value (holderRenewals): never at the + // read, which renews `c` and not `o.v` - a holder declared before a loop keeps the old borrow + // across the back edge until it is stored again (fix-up re-review N3). The local's own retain + // and releases are its bookkeeping, which the owner checks decide. A sibling field never given + // the borrow is not followed. A value made from the borrow and stored into another local makes + // that one a holder too. Any other keep - a field of an object, an array, a global, a callee + // that keeps it, a capture, a return - cannot be bounded here and is an error. A string read is + // copied where it is kept (spec 22), so its keeps are not this check's. + void checkGlobalRead(mlir_ts::LoadOp read); + + // A borrow read out of a parameter this function keeps, taken before the parameter moves into a + // new owner (spec 33, review fix): from the move on it is bounded by that owner, as a read through + // the owner would be (spec 29) - by the owner's ends and the drops of its place after the move. + // `const inner = p.v; let r = p; r = new N(...); inner.x` reads what the assignment freed. A move + // into anything that is not such a place (a call that keeps it, a return, a push) ends the borrow + // there. So do the releases of the parameter's slot (spec 33's exit releases). True when some use + // of the borrow comes after one of these (`outlives` reported it). + bool keptParamOutlives(const Chain &chain, llvm::function_ref outlives); + + // A parameter, or a view of one (`u`, the payload `ts.Unbox` reads out of an `any`), that + // this function returns as a borrow (`__own_result_borrows`). rc retained it for the caller, + // who now takes no reference: the retain goes, and nothing may keep it. The caller's own + // borrow of the argument bounds it; nothing here can end that but a call, and a call that may + // is the caller's use of what it passed (spec 2.4, phase 4). + void checkReturnedParam(mlir::Value value, llvm::SetVector &toErase); + + // `the result of 'first'`, for a call whose result borrows an argument. + static std::string describeCall(mlir::Operation *call); + + // The function a call names - directly, or the method it calls through a vtable - or empty. + static std::string calleeName(mlir::Operation *call); + + // `'h.c'`, `'arr[]'`, `'h.c.v'` - or `a field`, `an element` when the root has no name (a + // temporary, or no --di). + std::string describePlace(mlir::Value ref); + + std::string pathOf(mlir::Value ref); + + static std::string fieldName(mlir_ts::PropertyRefOp propertyRefOp); + + // The name of a temporary that owns: a folded `const` has no ts.Variable, but under --di it + // has a ts.DebugVariable, whose location carries the name. + static llvm::StringRef ownerName(mlir::Value value); + + // A `ts.RetainSlot`'s slot holds its initializer where the retain is. MLIRGen puts the retain at + // the declaration (takeOwnershipOfLocal), and nothing between the two may write the slot: in + // their block, no op is given the slot, no call runs, and no op has regions (a store could hide + // in them). What is stored into the slot later is decided at its own store; this retain takes + // only the initializer. No closure captures it. + static bool slotHoldsInitializerAt(mlir::Operation *op); + + // The value a retain acquires: a Retain's operand, or a RetainSlot's variable's initializer, + // 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); + + // Does a value of this type own a block that a release would destroy? + bool ownsHeap(mlir::Value value); + + bool ownsHeap(mlir::Location location, mlir::Type type); + + // A tuple read out of a local that a `ts.Constant` initializes, whose owning fields hold what + // that constant put there - null, a number, an immortal literal - or a fresh value moved in: + // the initial state a generator's maker copies into its state object, or a record built to be + // returned (`return {value: new C(), done: false}`, a `yield`'s result). rc's retain of it is + // its reference for the values moved in, which own moves instead (recordRetainOf). + bool holdsConstantsOrMoves(mlir::Value value); + + // rc's one `ts.Retain` of a record local's one read, where the read is only returned: stored + // into a local that owns nothing and is read only by returns (the result slot), or copied into + // the box it is given in as an object or an interface (spec 27). It is the reference rc gives + // each fresh value stored into the record, so it is where they move in. Null for any other + // record. + static mlir::Operation *recordRetainOf(mlir_ts::VariableOp varOp); + + // `const b = [...a, ...a]`: MLIRGen builds the array in a local of its own that owns nothing, + // starting from a fresh one and pushing into it, and reads it once, when it is done, into the + // owning local it is made for. That local's `ts.RetainSlot` (`retain`) is the reference: the + // fresh array moves into the builder and then into the owner, and nothing else holds it. + bool isBuiltArrayRead(mlir::Operation *retain, mlir::Value value); + + // `return [...a]`: the builder of isBuiltArrayRead, read once, when it is done, and returned. + // The return's `ts.Retain` (`retain`) is the reference the caller gets: the fresh array moves + // into the builder and then out of the function, and nothing else holds it. The builder's + // other reads only bind its `push` (`ts.ThisSymbolRef @__array_push`) or are not used. A store + // into a field is the same move, into a container: `h.c = [...a]`, and a generator's local + // (`const groups = [...result]`), which is a field of its state object. + bool isBuiltArrayReturned(mlir::Operation *retain, mlir::Value value); + + // The one `ts.RetainSlot` of a local; null when there are none or several. + static mlir::Operation *onlyRetainSlotOf(mlir_ts::VariableOp varOp); + + // A local that owns nothing, is never captured, and is read only to be returned. + static bool isResultSlot(mlir_ts::VariableOp varOp); + + // The record whose field `taker` stores into, and rc's retain that moves the stored value in; + // null when `taker` is not such a store. + static mlir::Operation *recordRetainOfStore(mlir::Operation *taker); + + // A string constant cast to `string` is the global itself, which a release skips; so is a read + // of an immortal global (spec 25). + static bool isImmortalLiteral(mlir::Value value); + + // A use that reads the value without keeping it. Kept deliberately short: anything not listed + // is treated as taking the value, which can only turn a program into an error. + static bool isBorrow(mlir::Operation *user, mlir::Value value); + + // `ts.CreateBoundFunction(ts.Cast(object), method)` split straight back into `ts.GetThis` and + // `ts.GetMethod` for a call: a method of another module's class - its constructor, when it is + // built with `new` - called on an object here. It reads the object as `ts.ThisSymbolRef` does + // for a method of this module; nothing else may use the cast or the bound function, and the + // object comes out again only as a call's argument, which is a borrow of its own. An + // interface's `this` (`ts.ExtractInterfaceThis`) is bound so for a call of a function-typed + // field (`i.toString()` where `toString: () => string`). + static bool isBoundForCall(mlir::Operation *thisOp); + + // Is `value` an argument the call's callee keeps? + static bool isKeptArgument(mlir::Operation *call, mlir::Value value); + + // The moves out of a fresh or unknown SSA value. Each taking use (spec 11.1) is a move: + // - nothing else may use the value after it - another taker included - and a taker that + // control comes back to without the value being made again is a move on every iteration + // of a loop (spec 2.6); + // - where the temporary is also released (`__owned_result` rc gives back at block end), the + // taker must be one rc gave its own reference (takerAcquires), and each release the move + // reaches must be dominated by it - that release then has nothing to give back and is + // erased. A release the move cannot reach stays: it is the owner on the other paths. + // Reports and returns false otherwise. + bool checkValueMoves(mlir::Value value, llvm::SetVector &toErase); + + // No release of `value` comes after another on any path. Reports and returns false otherwise. + bool checkReleasedOnce(mlir::Value value); + + // The move verdict for an SSA value (phase 1). Reports and returns false when it is not one. + bool movesOf(mlir::Value value, llvm::SetVector &toErase); + + // A call that keeps an argument and is given the same value again through another operand - the + // value itself, a view of it, something read out of it, another read of its slot (`f(n, n)`, + // `f(h, n, n.v)` with only the first `n` kept): the callee takes the value while it borrows it + // through the other operand, and may free it under that borrow, so the other operand is a use + // after the move (spec 33, review). `sources` are what the moved value is: the value, or every + // read of the owning slot it came from. The call itself, or null. + mlir::Operation *callGivenAgain(mlir::Operation *taker, mlir::Value moved, llvm::ArrayRef sources); + + // Does control come back round to `taker` without making the value again? Only then is it a + // move on every iteration of a loop. + bool takerLoops(mlir::Operation *taker, mlir::Operation *definition); + + // Can control reach `to` after `from` has run, without passing `kill` on the way? `kill` is + // the source's definition - the producer of an SSA value, or a local's `ts.Variable` - and a + // path through it is a new value, not this one. Straight-line order within a block, then the + // CFG from the block's successors; a path back into `from`'s own block reaches every op in it. + // + // Ops nested in a region that is not the function body are compared by their ancestors in the + // body. That answers "reachable" whenever the two share an ancestor, which can only turn a + // program into an error. (By this pass async bodies and lambdas are functions of their own; + // after the affine lowering no op of a body holds a region, so the case is kept for safety.) + bool reachableAfter(mlir::Operation *from, mlir::Operation *to, mlir::Operation *kill); + + // Whether reachableAfter can order `from` and `to` at all: both inside the function body and not + // one op of it (nested in the same statement). A check whose safe answer is "not reachable" must + // refuse what it cannot order, where reachableAfter would answer "reachable". + bool orderedInBody(mlir::Operation *from, mlir::Operation *to); + + // The same, where a path through any of `kills` is a new value. A use that is itself a kill + // runs after the value is made again. + bool reachableAfter(mlir::Operation *from, mlir::Operation *to, llvm::ArrayRef kills); + + // The use that takes `value` for this retain: the declaration a `ts.RetainSlot` belongs to, + // or the one use of the value, or of a view of it, after a `ts.Retain` that is not a read. + static mlir::Operation *takerOf(mlir::Operation *retain, mlir::Value value); + + // rc reads a place twice for one copy - `.map(f)` boxes its array so - and retains the first + // read, whose only use is that retain: the read of the same place that follows it in its block, + // with nothing stored into the place between, is the same value. Null when there is none. + // The one op that uses `value`, but `--di`'s ts.DebugVariable, which only names it; or null. + static mlir::Operation *soleUser(mlir::Value value); + + static mlir::Value twinReadAfter(mlir::Operation *retain, mlir::Value value); + + // A move out of an owning local: `let b = a`, `h.c = a`, `return a`. rc read the slot and + // retained what it read for the receiver; the move erases that retain and every release of + // the slot the move reaches, which must be dominated by it - or, with `placeReleases`, is + // placed on the paths that did not move it (spec 30). Any other use of the slot the + // move reaches - a read, an assignment's own read of it - is a use after the move. Reports and + // returns false when it is not a move. + // + // An assignment the move reaches gives the slot a new value to own (#528): a path through it + // is a new value, as a path through the declaration is. Its release before the store goes + // with the move - what the slot held is the receiver's - and the releases only a path through + // an assignment reaches stay, to destroy the new value. + bool checkSlotMove(mlir::Operation *retain, mlir::Value value, mlir_ts::LoadOp load, + llvm::SetVector &toErase, bool placeReleases = false); + + // ---- A move on some paths only (spec 30) ---- + // + // `if (c) { best = v; }`: after the paths join, `v` holds its value on one and a moved one on + // the other, and its one release there has no single answer. No run-time record of the move is + // kept (spec 2.2); instead, where nothing uses the value after the join, the release is placed + // on each edge into the join that did not move it - freeing the value earlier, where it is + // already dead - and the releases after the join go. Each path frees the value once: through + // its new owner where it moved, through the edge where it did not. + + // What the slot of an owning local holds at a point: nothing yet or any more (declared later, + // released), its value, or a value moved out of it. A point some paths reach holding the value + // and others having moved it is where the paths join. + enum SlotState : unsigned + { + SlotDead = 1, + SlotLive = 2, + SlotMoved = 4 + }; + + static bool isJoined(unsigned state); + + // A value of this type can point into a block: anything but a number, a boolean, a literal or + // an empty value. + static bool carriesPointer(mlir::Type type); + + // A release on an edge into the join that did not move the local's value (spec 30), put in after + // the analysis. + struct EdgeRelease + { + mlir::Value slot; + mlir::Block *from; + unsigned successor; + mlir::Location loc; + }; + + llvm::SmallVector edgeReleases; + + // The use that kept placeOnUnmovedEdges from placing the releases, for the error's note. + mlir::Operation *joinedUse = nullptr; + + // Can control reach `to` from the start of `start`, without passing `kills` on the way? + bool reachableFrom(mlir::Block *start, mlir::Operation *to, llvm::ArrayRef kills); + + // The blocks of an exception handler: from a landing pad to the end of its catch. A release there + // would be a call inside an exception funclet. + llvm::SmallPtrSet handlerBlocks(); + + // The move at `acquire` out of the owning local `varOp` reaches a release that other paths reach + // holding the value. Places a release of the slot on every edge into the join that holds the value + // and has not moved it, and erases every release a moved value reaches - when nothing uses the + // value, through the slot or through anything read out of it, where the paths have joined. False, + // with nothing placed or erased, when it cannot. + // The slot of an owning local through the function (spec 30): its state at the entry and the exit + // of each block, and before each op of the body. + struct SlotStates + { + llvm::DenseMap entry; + llvm::DenseMap exit; + llvm::DenseMap before; + }; + + bool placeOnUnmovedEdges(mlir_ts::VariableOp varOp, mlir::Operation *acquire, mlir::Operation *retain, + mlir::Operation *taker, llvm::ArrayRef assignments, + llvm::SetVector &toErase); + + // The releases of the slot, when the local, the move, its assignments and its releases are all in + // the function's body. False when one is not. + bool slotReleasesInBody(mlir_ts::VariableOp varOp, mlir::Operation *acquire, + llvm::ArrayRef assignments, llvm::SmallVectorImpl &releases); + + // The slot's state through the function, forward from its entry: assigned, moved at `acquire`, + // released. + SlotStates slotStates(mlir_ts::VariableOp varOp, mlir::Operation *acquire, llvm::ArrayRef assignments, + llvm::ArrayRef releases); + + // Places a release of the slot on every edge into the join that holds the value and has not moved + // it. False, with nothing placed, when an edge cannot take one, or there is none. + bool unmovedEdges(mlir_ts::VariableOp varOp, const SlotStates &states, llvm::SmallVectorImpl &placed, + llvm::SetVector &releasedInto); + + // Whether nothing uses the value where the paths have joined, or after an edge released it. + // False, with the use in `joinedUse` where there is one, when the release cannot be placed. + bool noUseWhereJoined(mlir_ts::VariableOp varOp, mlir::Operation *retain, mlir::Operation *taker, + llvm::ArrayRef assignments, const SlotStates &states, + const llvm::SetVector &releasedInto); + + // Puts in the releases placeOnUnmovedEdges decided: at the end of a block that goes only to the + // join, else in a block of its own on the edge. + void placeEdgeReleases(); + + // The stores of the assignments to an owning local: rc releases what the slot holds right + // before it stores the new value (an assignment, and a `var` declaration met again). + static llvm::SmallVector slotAssignments(mlir::Value slot); + + // A use of what `read` returned - directly or through casts - that the move at `taker` can + // reach, other than the move's own retain and taker. None if there is none. A path back + // through `read` itself reaches a use of a value read again: the read, met after the move, + // is the slot's own use to check (around a loop, of a value an assignment stored). + // `isRead` accepts a use that reads the value through its new owner (spec 29). + mlir::Operation *readUsedAfter(mlir_ts::LoadOp read, mlir::Operation *retain, mlir::Operation *taker, + mlir::Operation *kill, + llvm::function_ref isRead = nullptr); + + // ---- Reads after a move, through the new owner (spec 29) ---- + // + // `h.c = c; print(c.x)`, `best = v; s += v.x`: after the move the value is the new owner's, and + // a read of it through the old name is a borrow of the place it moved into, as a read of + // `h.c` would be. It is bounded the way checkPlaceRead bounds such a read - by the ends of the + // place's roots and by the drops that may destroy what the place holds (an overwrite, through an + // interface view too; a call whose facts may drop; a call given a closure) - on a path from the + // move to the read on which the value is not made again or moved again. + + // Every use of `value`, through its views, is a read `isRead` accepts. A retain - rc's reference + // for an owner - is not. + static bool onlyReadThroughOwner(mlir::Value value, llvm::function_ref isRead); + + // A use that only reads the value: a field, element or length read through it, a method reached + // through it, a comparison or arithmetic, a print, an assert, a test, or an argument the callee + // borrows. Anything else - a store, an owning `let`, a keeping call, a return - would be a second + // owner. + static bool isReadAfterMove(mlir::Operation *user, mlir::Value value); + + // The place a taker moves its value into, as a chain checkPlaceRead would read it: an owning + // local (a declaration, or an assignment), or a field or element - of a local or a parameter + // (`this`). False for anything else: a call that keeps it, a push, a return, a global, a + // captured variable, a handle's payload. + bool destinationChain(mlir::Operation *taker, Chain &chain); + + // Is `use`, a use of `value` after `taker` moved it, a read through the place it moved into + // (spec 29)? `made` is where the value is made again (its definition, the local's declaration + // and assignments). Sound when no path from the move through an end or a drop of the place to + // the read - or to anything the read produced - makes the value again or moves it again: such a + // path is the only way the place can stop holding the value before the read. + bool readsThroughOwner(mlir::Operation *taker, mlir::Value value, mlir::Operation *use, + llvm::ArrayRef made); + + // Where rc takes the receiver's reference: its `ts.Retain` of the value, when that comes + // first in the taker's block, else the taker itself (a declaration, whose `ts.RetainSlot` + // follows it). A release between the two - a `return`'s scope exit runs after its retain + // and before the store into the return slot - is after the move. + static mlir::Operation *acquirePoint(mlir::Operation *taker, mlir::Value value); + + // Did rc give this taker a reference of its own - a `ts.Retain` of the value in front of it, + // or the `ts.RetainSlot` of an owning local declared from it? Only then is there a release on + // the taker's side to give the value back once it has moved. `ts.NewInterface` is a taker to + // this pass but a view to rc, which retains nothing for it and relies on the instance's own + // release; moving the instance into it would leave nothing to release it at all. + static bool takerAcquires(mlir::Operation *taker, mlir::Value value); + + // The declaration's name survives only as debug metadata (`--di`). At this level MLIRGen's + // form of it is a NameLoc first inside the location fused with the scope - what LowerToLLVM's + // preserveTypesForDebugInfo reads - and after that pass it is a DILocalVariable. Without + // `--di` the error still points at the right place. + static llvm::StringRef varName(mlir_ts::VariableOp varOp); + + static llvm::StringRef nameAt(mlir::Location location); + + static llvm::StringRef describe(mlir::Value value); + + // A second reference nobody proved safe. `value` is the value it is to, when the caller knows + // it; a retain names its own. The shapes this pass recognises get the rule they break (spec + // 21.2); anything else, the catch-all. + void reportSecondReference(mlir::Operation *op, mlir::Value value = {}); + + // The named shapes of a second reference to `root` (spec 21.2). False when it is none of them. + bool reportShapeOfSecondReference(mlir::Operation *op, mlir::Value root, llvm::StringRef name); + + // The name of parameter `index`, as its slot carries it (under `--di`). + llvm::StringRef paramName(int index); + + // Is `root`, or a view of it, stored into the result slot? + static bool isReturned(mlir::Value root); + + // A return that disagrees with one of a borrow of parameter `param` (with -1, with one that + // borrows nothing): a store into the result slot of a value that holds a block and borrows + // another parameter or none - or, for -1, borrows a parameter. Null when there is none. + mlir::Operation *otherReturn(int param); + + // Where the body would have been fine had its callers known a fact about it, say why they + // cannot. + void noteLostFacts(mlir::InFlightDiagnostic &diag); + + void reportKeptOnSomePaths(mlir::Operation *move, mlir::Operation *exit, llvm::StringRef name); + + // A handle made into a union whose other members include one that owns a block (spec 23.2): + // `Shared | string` given a Shared. gc, none and rc accept it. + void reportHandleInOwningUnion(mlir::Operation *op); + + static std::string printed(mlir::Type type); + + void reportGivenNotOwned(mlir::Operation *call, mlir::Value arg); + + void reportUseAfterMove(mlir::Operation *use, mlir::Operation *move, llvm::StringRef name); + + void reportMovedInLoop(mlir::Operation *move, llvm::StringRef name); + + void reportBorrowOutlives(mlir::Operation *use, mlir::Operation *end, llvm::StringRef name, llvm::StringRef owner); + + void reportPlaceBorrowOutlives(mlir::Operation *use, mlir::Operation *drop, llvm::StringRef name, + llvm::StringRef place); + + void reportPlaceBorrowerAssigned(mlir::Operation *op, llvm::StringRef name, llvm::StringRef place); + + void reportPlaceBorrowEscapes(mlir::Operation *op, llvm::StringRef name, llvm::StringRef place); + + void reportMovedWhileBorrowed(mlir::Operation *op, llvm::StringRef owner, llvm::StringRef borrower); + + void reportBorrowEscapes(mlir::Operation *op, llvm::StringRef name, llvm::StringRef owner); + + void reportBorrowerAssigned(mlir::Operation *op, llvm::StringRef name, llvm::StringRef owner); + + void reportEscapingCellNotOwned(mlir::Operation *capture, mlir::Operation *escape, llvm::StringRef why); + + void reportUsedAfterEscapingCapture(mlir::Operation *use, mlir::Operation *capture, llvm::StringRef name); + + void reportClosureOutlives(mlir::Operation *use, mlir::Operation *end, llvm::StringRef closure, + llvm::StringRef captured); + + void reportCaptureNotOwned(mlir::Operation *op, llvm::StringRef captured); + + void reportCapturedParamAssigned(mlir::Operation *op, llvm::StringRef name); + + void reportMovedOnSomePaths(mlir::Operation *move, mlir::Operation *release, llvm::StringRef name, + mlir::Operation *usedAt = nullptr); +}; + +} // namespace own_inference + +#endif // MLIR_TYPESCRIPT_OWNERSHIPINFERENCEPASS_H diff --git a/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp b/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp new file mode 100644 index 000000000..7a004e6e2 --- /dev/null +++ b/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp @@ -0,0 +1,867 @@ +// OwnershipInferencePass (see OwnershipInferencePass.h): reads out of containers (spec 2.3, +// phase 3) - handle reads, the chain from a read to its roots, and what may drop it. + +#include "OwnershipInferencePass.h" + +#define DEBUG_TYPE "own" + +namespace own_inference +{ + + mlir::Operation *OwnershipInferencePass::placeReadOf(mlir::Value value) + { + auto root = rootOf(value); + auto *def = root.getDefiningOp(); + // a handle read out of a place that rc gives an owner of its own is a counted copy, not a + // borrow (spec 23.3); any other is a borrow of the place, as a field read is + if (!def || !ownsHeap(root) || (isHandle(root) && isCountedHandleRead(root))) + { + return nullptr; + } + + if (auto loadOp = mlir::dyn_cast(def); loadOp && isPlace(loadOp.getReference())) + { + // a field of a generator's borrowing box: only its maker fills it, and nothing a call + // can reach overwrites it; it is the caller's, as a parameter is (checkBorrowedReads) + if (isBorrowingBoxField(loadOp.getReference())) + { + return nullptr; + } + + return def; + } + + return borrowingCall(root); + } + + OwnershipInferencePass::HandleReadUses OwnershipInferencePass::handleReadUses(mlir::Value value) + { + HandleReadUses result{false, false}; + llvm::SmallVector values{value}; + llvm::SmallPtrSet seen{value}; + while (!values.empty()) + { + auto current = values.pop_back_val(); + for (auto &use : current.getUses()) + { + auto *user = use.getOwner(); + if (mlir::isa(user)) + { + continue; + } + + // a return and an `any` box take the value, though passesOn would hand them on + if (isHandOff(user, current)) + { + (isCountedHandleReceiver(user, current) ? result.counted : result.borrowed) = true; + continue; + } + + llvm::SmallVector same; + if (passesOn(use, same)) + { + for (auto next : same) + { + // what is no longer a handle is no receiver: a test (`if (p.child)`) only + // reads it + if (!isHandle(next)) + { + result.borrowed = true; + } + else if (seen.insert(next).second) + { + values.push_back(next); + } + } + + continue; + } + + (isCountedHandleReceiver(user, current) ? result.counted : result.borrowed) = true; + } + } + + return result; + } + + bool OwnershipInferencePass::isCountedHandleRead(mlir::Value value) + { + auto uses = handleReadUses(value); + return uses.counted && !uses.borrowed; + } + + bool OwnershipInferencePass::isCountedHandleReceiver(mlir::Operation *user, mlir::Value value) + { + if (mlir::isa(user)) + { + return true; + } + + if (auto varOp = mlir::dyn_cast(user)) + { + return isOwningVariable(varOp) && llvm::any_of(varOp->getUsers(), [](mlir::Operation *varUser) { + return mlir::isa(varUser); + }); + } + + if (auto storeOp = mlir::dyn_cast(user)) + { + return storeOp.getValue() == value && countedStoreRetain(storeOp); + } + + if (auto pushOp = mlir::dyn_cast(user)) + { + return retainedRightBefore(user, pushOp.getItems(), value); + } + + if (auto unshiftOp = mlir::dyn_cast(user)) + { + return retainedRightBefore(user, unshiftOp.getItems(), value); + } + + if (auto castOp = mlir::dyn_cast(user)) + { + return mlir::isa(castOp.getType()) && retainedRightBefore(user, user->getOperands(), value); + } + + // `ts.ReturnVal` is the return before the affine lowering, which makes it a store into + // the result slot + if (mlir::isa(user)) + { + return retainedRightBefore(user, user->getOperands(), value); + } + + return false; + } + + bool OwnershipInferencePass::isHandOff(mlir::Operation *user, mlir::Value value) + { + if (auto storeOp = mlir::dyn_cast(user)) + { + auto slot = storeOp.getReference().getDefiningOp(); + return storeOp.getValue() == value && slot && isResultSlot(slot); + } + + auto castOp = mlir::dyn_cast(user); + return castOp && castOp.getIn() == value && mlir::isa(castOp.getType()); + } + + bool OwnershipInferencePass::isCountedHandOff(mlir::Operation *user, mlir::Value value) + { + return isHandOff(user, value) && isCountedHandleReceiver(user, value); + } + + bool OwnershipInferencePass::retainedRightBefore(mlir::Operation *op, mlir::ValueRange operands, mlir::Value value) + { + auto needed = llvm::count(operands, value); + if (needed == 0) + { + return false; + } + + for (auto *previous = op->getPrevNode(); needed > 0 && previous; previous = previous->getPrevNode()) + { + auto retainOp = mlir::dyn_cast(previous); + if (!retainOp) + { + break; + } + + if (retainOp.getReference() == value) + { + --needed; + } + } + + return needed == 0; + } + + void OwnershipInferencePass::collectCountedStoreReleases(mlir::Value value, llvm::SmallPtrSetImpl &releases) + { + llvm::SmallVector values{value}; + llvm::SmallPtrSet seen{value}; + while (!values.empty()) + { + auto current = values.pop_back_val(); + for (auto &use : current.getUses()) + { + llvm::SmallVector same; + if (passesOn(use, same)) + { + for (auto next : same) + { + if (isHandle(next) && seen.insert(next).second) + { + values.push_back(next); + } + } + + continue; + } + + auto storeOp = mlir::dyn_cast(use.getOwner()); + if (!storeOp || storeOp.getValue() != current || !countedStoreRetain(storeOp)) + { + continue; + } + + if (auto releaseSlotOp = mlir::dyn_cast_or_null(storeOp->getPrevNode())) + { + releases.insert(releaseSlotOp); + } + } + } + } + + mlir::Operation *OwnershipInferencePass::countedStoreRetain(mlir_ts::StoreOp storeOp) + { + auto *previous = storeOp->getPrevNode(); + if (auto slot = storeOp.getReference().getDefiningOp(); slot && isResultSlot(slot)) + { + // the scope exit's releases, of locals and of their slots, in any order + while (mlir::isa_and_nonnull(previous) && + previous->getOperand(0) != storeOp.getValue()) + { + previous = previous->getPrevNode(); + } + } + else if (auto releaseSlotOp = mlir::dyn_cast_or_null(previous)) + { + previous = releaseSlotOp.getSlot() == storeOp.getReference() ? releaseSlotOp->getPrevNode() : nullptr; + } + + auto retainOp = mlir::dyn_cast_or_null(previous); + return retainOp && retainOp.getReference() == storeOp.getValue() ? retainOp.getOperation() : nullptr; + } + + mlir::Operation *OwnershipInferencePass::borrowingCall(mlir::Value value) + { + auto *def = value.getDefiningOp(); + if (!def || !isCall(def) || resultBorrows(def) < 0) + { + return nullptr; + } + + auto args = callArgs(def); + auto indices = resultBorrowsAll(def); + return llvm::all_of(indices, [&](int index) { return static_cast(index) < args.size(); }) ? def : nullptr; + } + + OwnershipInferencePass::Chain OwnershipInferencePass::chainOf(mlir::Operation *read, mlir::Value place) + { + Chain chain; + llvm::SmallVector refs; + llvm::SmallVector first; + if (place) + { + refs.push_back(place); + } + else if (auto loadOp = mlir::dyn_cast(read)) + { + refs.push_back(loadOp.getReference()); + } + else + { + // a call's result that borrows its arguments - a plain borrow, or a borrowing record's box + // (spec 32), which may hold a borrow of each - is somewhere under each of them + chain.anyPlace = true; + auto args = callArgs(read); + for (auto index : resultBorrowsAll(read)) + { + if (static_cast(index) < args.size() && !argumentHoldsNoBlock(args[index]) && + !isImmortalLiteral(args[index])) + { + first.push_back(rootOf(args[index])); + } + } + } + + llvm::SmallPtrSet seen; + // the bases a `s.value` was read through, and what merges into them + llvm::SmallPtrSet handleBases; + while (!refs.empty() || !first.empty()) + { + llvm::SmallVector bases; + if (!first.empty()) + { + bases.swap(first); + } + else + { + auto ref = refs.pop_back_val(); + auto place = placeBase(ref); + assert(place && "chainOf walks places only"); + switch (place.kind) + { + case PlaceKind::Field: + chain.places.push_back({ref.getDefiningOp().getPosition(), ref.getType()}); + break; + case PlaceKind::SharedValue: + chain.places.push_back({Chain::SharedValue, ref.getType()}); + handleBases.insert(rootOf(place.base)); + break; + case PlaceKind::InterfaceField: + chain.places.push_back({Chain::InterfaceField, ref.getType()}); + break; + default: + chain.places.push_back({-1, ref.getType()}); + break; + } + + bases.push_back(rootOf(place.base)); + } + + while (!bases.empty()) + { + auto base = bases.pop_back_val(); + if (!seen.insert(base).second) + { + continue; + } + + // A handle (spec 23.3): other handles may reach its block, so this function owns + // nothing under it. A handle read out of a place still has that place above it, and + // one merged from several has each of them. + if (handleBases.contains(base) || isHandle(base)) + { + llvm::SmallVector merged; + if (auto argument = mlir::dyn_cast(base); argument && mergedInto(argument, merged)) + { + handleBases.insert(merged.begin(), merged.end()); + bases.append(merged.begin(), merged.end()); + continue; + } + + chain.roots.push_back({base, Chain::NotOwned}); + chain.handles.push_back(base); + if (auto loadOp = base.getDefiningOp(); loadOp && isPlace(loadOp.getReference())) + { + refs.push_back(loadOp.getReference()); + } + + continue; + } + + if (isPlace(base)) + { + refs.push_back(base); // a field of a value-type field: the same object + continue; + } + + if (auto loadOp = base.getDefiningOp(); loadOp && isPlace(loadOp.getReference())) + { + refs.push_back(loadOp.getReference()); + continue; + } + + // the object a method is called on + if (auto object = boundThis(base)) + { + bases.push_back(rootOf(object)); + continue; + } + + // a result that borrows an argument is somewhere under the argument + if (auto *call = borrowingCall(base)) + { + chain.anyPlace = true; + auto args = callArgs(call); + for (auto index : resultBorrowsAll(call)) + { + if (static_cast(index) < args.size()) + { + bases.push_back(rootOf(args[index])); + } + } + + continue; + } + + if (auto argument = mlir::dyn_cast(base); argument && mergedInto(argument, bases)) + { + continue; + } + + if (base.getDefiningOp()) + { + chain.roots.push_back({base, Chain::Slot}); + } + else if (auto slotLoad = slotLoadOf(base)) + { + chain.roots.push_back({slotLoad.getReference(), Chain::Slot}); + } + else + { + chain.roots.push_back({base, isFresh(base) ? Chain::Owned : Chain::NotOwned}); + } + } + } + + return chain; + } + + llvm::SmallVector OwnershipInferencePass::rootEnds(const Chain &chain) + { + llvm::SmallVector ends; + for (auto [root, kind] : chain.roots) + { + // A handle (spec 23.3), never owned: where this one is given back or overwritten - a + // local's release and assignments, a temporary's release; a global's assignments are + // below. A copy of it is counted, and moves nothing away. + if (llvm::is_contained(chain.handles, root)) + { + auto loadOp = root.getDefiningOp(); + if (auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp()) + { + for (auto *user : varOp->getUsers()) + { + if (mlir::isa(user)) + { + ends.push_back(user); + } + } + } + else if (!loadOp) + { + forEachUse(root, [&](mlir::Operation *use, mlir::Value) { + if (mlir::isa(use)) + { + ends.push_back(use); + } + }); + } + } + + if (kind == Chain::Slot) + { + for (auto *user : root.getUsers()) + { + if (mlir::isa(user)) + { + ends.push_back(user); + } + else if (auto loadOp = mlir::dyn_cast(user)) + { + forEachUse(loadOp.getResult(), [&](mlir::Operation *use, mlir::Value used) { + auto varOp = mlir::dyn_cast(use); + if (mlir::isa(use) || (varOp && isOwningVariable(varOp)) || + (isCall(use) && isKeptArgument(use, used))) + { + ends.push_back(use); + } + }); + } + } + } + else if (kind == Chain::Owned) + { + forEachUse(root, [&](mlir::Operation *use, mlir::Value used) { + if (mlir::isa(use) || !isBorrow(use, used)) + { + ends.push_back(use); + } + }); + } + else if (auto loadOp = root.getDefiningOp()) + { + if (auto global = loadOp.getReference().getDefiningOp()) + { + getFunction().walk([&](mlir::Operation *op) { + mlir::Value slot; + if (auto releaseSlotOp = mlir::dyn_cast(op)) + { + slot = releaseSlotOp.getSlot(); + } + else if (auto storeOp = mlir::dyn_cast(op)) + { + slot = storeOp.getReference(); + } + + auto other = slot ? slot.getDefiningOp() : mlir_ts::AddressOfOp(); + if (other && other.getGlobalName() == global.getGlobalName()) + { + ends.push_back(op); + } + }); + } + } + } + + return ends; + } + + llvm::SmallVector OwnershipInferencePass::rootSources(const Chain &chain) + { + llvm::SmallVector sources; + for (auto &root : chain.roots) + { + auto value = root.first; + sources.push_back(value); + for (auto steps = 0; steps < 8; ++steps) + { + auto varOp = value.getDefiningOp(); + if (!varOp || !varOp.getInitializer()) + { + break; + } + + auto initializer = rootOf(varOp.getInitializer()); + auto slotLoad = slotLoadOf(initializer); + value = slotLoad ? slotLoad.getReference() : initializer; + sources.push_back(value); + } + } + + return sources; + } + + const llvm::DenseSet &OwnershipInferencePass::derivedFrom(mlir::Value start) + { + if (auto found = derivedCache.find(start); found != derivedCache.end()) + { + return *found->second; + } + + auto &slot = derivedCache[start]; + slot = std::make_shared>(); + auto &derived = *slot; + llvm::SmallVector work{start}; + while (!work.empty()) + { + auto value = work.pop_back_val(); + if (!derived.insert(value).second) + { + continue; + } + + for (auto &use : value.getUses()) + { + auto *user = use.getOwner(); + if (isCall(user)) + { + // ...but a borrowing record given this value holds it (spec 32) + auto args = callArgs(user); + if (isRecordResult(user) && llvm::any_of(resultBorrowsAll(user), [&](int index) { + return static_cast(index) < args.size() && args[index] == value; + })) + { + work.push_back(user->getResult(0)); + } + + continue; + } + + llvm::SmallVector next(user->getResults().begin(), user->getResults().end()); + passesOn(use, next); + for (auto result : next) + { + if (mayPointInto(result)) + { + work.push_back(result); + } + } + } + } + + return derived; + } + + bool OwnershipInferencePass::dropsChain(mlir::Operation *drop, const Chain &chain, mlir::Operation *read, mlir::Value borrowed) + { + if (read && drop == read) + { + return false; // a call that returns a borrow has not destroyed it by returning it + } + + if (chain.anyPlace && !isCall(drop)) + { + return true; + } + + if (auto releaseSlotOp = mlir::dyn_cast(drop)) + { + // An assignment to a captured variable destroys its old value, which is reached only + // through a read of the variable's cell: a root this function does not own. + auto slot = releaseSlotOp.getSlot(); + if (isCellVariable(slot) || isLoadedCell(slot)) + { + return !chain.owned(); + } + + // An assignment to a global destroys its old value and all that value owns. A root this + // function does not own - `this`, a parameter, a global's or a field's value - may be + // that very block, or under it (spec 29). A root this function made cannot. An immortal + // global's one store is its initializer, whose old value is the empty one (spec 25). + if (auto addressOfOp = slot.getDefiningOp()) + { + return !addressOfOp->hasAttr(OWN_IMMORTAL_GLOBAL_ATTR_NAME) && !chain.owned(); + } + + // `x.value = ...` through any handle, of any type, may replace the payload another + // handle reads (spec 23.3): a handle converts to one of a base class's type + if (slot.getDefiningOp()) + { + return llvm::any_of(chain.places, [&](auto &place) { return place.first == Chain::SharedValue; }); + } + + auto isField = [](int64_t position) { return position >= 0 || position == Chain::InterfaceField; }; + int64_t position = -1; + if (auto propertyRefOp = slot.getDefiningOp()) + { + position = propertyRefOp.getPosition(); + } + else if (isInterfaceField(slot)) + { + position = Chain::InterfaceField; + } + + // A field reached through an interface may be any field of the object under it, of any + // type (a class may declare the field narrower than the interface): an overwrite through + // an interface drops every field on the chain, and any field's overwrite drops a field + // read through an interface (spec 28). + return llvm::any_of(chain.places, [&](auto &place) { + if (isField(position) && isField(place.first) && + (position == Chain::InterfaceField || place.first == Chain::InterfaceField)) + { + return true; + } + + return place.first == position && place.second == slot.getType(); + }); + } + + if (isCall(drop)) + { + if (callNoDrops(drop)) + { + return false; + } + + // a call given a closure may run it, and its box may hold anything + if (!chain.owned() || llvm::any_of(drop->getOperands(), holdsCaptures)) + { + return true; + } + + auto &fromRead = derivedFrom(borrowed ? borrowed : read->getResult(0)); + auto sources = rootSources(chain); + return llvm::any_of(drop->getOperands(), [&](mlir::Value operand) { + return !fromRead.contains(operand) && llvm::any_of(sources, [&](mlir::Value source) { + return derivedFrom(source).contains(operand); + }); + }); + } + + // pop, shift, splice, `length =`; push and unshift on an array reached through a handle + auto refType = mlir::dyn_cast(drop->getOperand(0).getType()); + auto arrayType = refType ? mlir::dyn_cast(refType.getElementType()) : mlir_ts::ArrayType(); + if (!arrayType) + { + return true; + } + + return llvm::any_of(chain.places, [&](auto &place) { + auto placeType = mlir::dyn_cast(place.second); + return place.first == -1 && placeType && placeType.getElementType() == arrayType.getElementType(); + }); + } + + bool OwnershipInferencePass::checkPlaceRead(mlir::Operation *read, llvm::SetVector &toErase) + { + auto loadOp = mlir::dyn_cast(read); + auto place = loadOp ? describePlace(loadOp.getReference()) : describeCall(read); + auto result = read->getResult(0); + // A handle read rc counts (spec 23.3): its counted uses are copies (walkBorrowed), and the + // release of the old value a counted store makes, right before it stores the retained + // read, cannot free the block read: it holds the retain's count by then + // (`Shared.count(q.child = p.child)`). + auto countedHandle = isHandle(result) && handleReadUses(result).counted; + llvm::SmallPtrSet countedStoreReleases; + if (countedHandle) + { + collectCountedStoreReleases(result, countedStoreReleases); + } + + llvm::StringRef name = ownerName(result); + 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; + } + + name = varName(borrower); + if (auto *keeps = collectBorrowerUses(borrower, kills, uses, bookkeeping); keeps && !letKept) + { + letKept = keeps; + letKeeps = borrower; + } + + return true; + }, keeping, countedHandle); + + if (kept || letKept) + { + if (!kept && isAssignmentOf(letKept, letKeeps)) + { + reportPlaceBorrowerAssigned(letKept, varName(letKeeps), place); + } + else + { + reportPlaceBorrowEscapes(kept ? kept : letKept, name, place); + } + + return false; + } + + auto chain = chainOf(read); + // A call given the borrow is a use of it for as long as the callee runs, so a call that + // may destroy it is a use after that, whatever the order of its arguments. The function + // value it goes through is only read as it starts. + auto outlives = [&](mlir::Operation *drop) { + for (auto &use : uses) + { + if ((use.op != drop || !use.callee) && reachableAfter(drop, use.op, use.kills)) + { + reportPlaceBorrowOutlives(use.op, drop, name, place); + return true; + } + } + + return false; + }; + + for (auto *end : rootEnds(chain)) + { + if (outlives(end)) + { + return false; + } + } + + if (keptParamOutlives(chain, outlives)) + { + return false; + } + + for (auto *drop : drops) + { + if (countedStoreReleases.contains(drop)) + { + continue; + } + + if (dropsChain(drop, chain, read) && outlives(drop)) + { + return false; + } + } + + toErase.insert(bookkeeping.begin(), bookkeeping.end()); + return true; + } + + bool OwnershipInferencePass::keptParamOutlives(const Chain &chain, llvm::function_ref outlives) + { + for (auto &root : chain.roots) + { + auto load = keptParamLoadOf(root.first); + if (!load) + { + continue; + } + + // the moves of the parameter: what takes a read of its slot + llvm::SmallVector> takers; + for (auto *user : load.getReference().getUsers()) + { + if (mlir::isa(user)) + { + if (outlives(user)) + { + return true; + } + + continue; + } + + auto readOp = mlir::dyn_cast(user); + if (!readOp) + { + continue; + } + + forEachUse(readOp.getResult(), [&](mlir::Operation *use, mlir::Value used) { + auto varOp = mlir::dyn_cast(use); + if (mlir::isa(use)) + { + auto *taker = takerOf(use, used); + takers.push_back({taker ? taker : use, used}); + } + else if ((varOp && isOwningVariable(varOp)) || (isCall(use) && isKeptArgument(use, used))) + { + takers.push_back({use, used}); + } + }); + } + + for (auto [taker, moved] : takers) + { + Chain owner; + if (!destinationChain(taker, owner)) + { + if (outlives(taker)) + { + return true; + } + + continue; + } + + auto afterMove = [&](mlir::Operation *end) { + return end != taker && reachableAfter(taker, end, llvm::ArrayRef{}) && outlives(end); + }; + + for (auto *end : rootEnds(owner)) + { + if (afterMove(end)) + { + return true; + } + } + + for (auto *drop : drops) + { + if (dropsChain(drop, owner, nullptr, moved) && afterMove(drop)) + { + return true; + } + } + } + } + + return false; + } + + void OwnershipInferencePass::checkReturnedParam(mlir::Value value, llvm::SetVector &toErase) + { + llvm::SmallVector uses; + llvm::SmallVector bookkeeping; + if (auto *kept = walkBorrowed(value, {}, uses, nullptr, Keeping{&bookkeeping, true})) + { + reportBorrowEscapes(kept, ownerName(value), "a parameter"); + return; + } + + toErase.insert(bookkeeping.begin(), bookkeeping.end()); + } + +} // namespace own_inference diff --git a/tslang/lib/TypeScript/OwnershipInferenceReadsAfterMove.cpp b/tslang/lib/TypeScript/OwnershipInferenceReadsAfterMove.cpp new file mode 100644 index 000000000..d5699e09e --- /dev/null +++ b/tslang/lib/TypeScript/OwnershipInferenceReadsAfterMove.cpp @@ -0,0 +1,221 @@ +// OwnershipInferencePass (see OwnershipInferencePass.h): reads after a move, through the new +// owner (spec 29). + +#include "OwnershipInferencePass.h" + +#define DEBUG_TYPE "own" + +namespace own_inference +{ + + bool OwnershipInferencePass::onlyReadThroughOwner(mlir::Value value, llvm::function_ref isRead) + { + auto all = true; + forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { + all = all && (mlir::isa(user) || + (!mlir::isa(user) && isRead(user, used))); + }); + + return all; + } + + bool OwnershipInferencePass::isReadAfterMove(mlir::Operation *user, mlir::Value value) + { + if (mlir::isa(user)) + { + return isBorrow(user, value); + } + + return isCall(user) && !isKeptArgument(user, value); + } + + bool OwnershipInferencePass::destinationChain(mlir::Operation *taker, Chain &chain) + { + auto ownSlot = [](mlir_ts::VariableOp varOp) { + return varOp && isOwningVariable(varOp) && !varOp.getCaptured().value_or(false); + }; + + if (auto varOp = mlir::dyn_cast(taker)) + { + if (!ownSlot(varOp)) + { + return false; + } + + chain.roots.push_back({varOp.getResult(), Chain::Slot}); + return true; + } + + auto storeOp = mlir::dyn_cast(taker); + if (!storeOp) + { + return false; + } + + auto ref = storeOp.getReference(); + if (auto varOp = ref.getDefiningOp()) + { + if (!ownSlot(varOp)) + { + return false; + } + + chain.roots.push_back({ref, Chain::Slot}); + return true; + } + + if (!isPlace(ref)) + { + return false; + } + + chain = chainOf(nullptr, ref); + if (!chain.handles.empty() || chain.roots.empty()) + { + return false; + } + + // a global's field, or a captured variable's: not a place of this function + return llvm::none_of(chain.roots, [](auto &root) { + auto loadOp = root.first.template getDefiningOp(); + return loadOp && (loadOp.getReference().template getDefiningOp() || + isLoadedCell(loadOp.getReference())); + }); + } + + bool OwnershipInferencePass::readsThroughOwner(mlir::Operation *taker, mlir::Value value, mlir::Operation *use, + llvm::ArrayRef made) + { + if (!isReadAfterMove(use, value)) + { + return false; + } + + Chain chain; + if (!destinationChain(taker, chain)) + { + return false; + } + + // A read again of the old name after a drop still finds the moved value - the slot is not + // emptied by a move - so neither the read nor the slot's read is where it is made again. + llvm::SmallVector kills(made.begin(), made.end()); + kills.push_back(taker); + llvm::SmallVector uses; + uses.push_back({use, {kills.begin(), kills.end()}}); + for (auto result : use->getResults()) + { + if (!mayPointInto(result)) + { + continue; + } + + // what the read produced - a field's reference, a bound method - still points into the + // block; a call's result that borrows its argument is the value again and may not be kept + if (walkBorrowed(result, kills, uses, nullptr, Keeping{nullptr, false}, false, !!borrowingCall(result))) + { + return false; + } + } + + // a method reached through it whose callee keeps its `this`: a second owner + for (auto &borrowUse : uses) + { + if (!isCall(borrowUse.op)) + { + continue; + } + + auto args = callArgs(borrowUse.op); + for (auto index : ownedParams(borrowUse.op)) + { + if (static_cast(index) < args.size() && + mlir::isa_and_nonnull(args[index].getDefiningOp())) + { + return false; + } + } + } + + auto outlives = [&](mlir::Operation *end) { + if (end == taker || !reachableAfter(taker, end, made)) + { + return false; + } + + return llvm::any_of(uses, [&](BorrowUse &borrowUse) { + return (borrowUse.op != end || !borrowUse.callee) && reachableAfter(end, borrowUse.op, borrowUse.kills); + }); + }; + + for (auto *end : rootEnds(chain)) + { + if (outlives(end)) + { + return false; + } + } + + for (auto *drop : drops) + { + if (dropsChain(drop, chain, nullptr, value) && outlives(drop)) + { + return false; + } + } + + return true; + } + + mlir::Operation *OwnershipInferencePass::acquirePoint(mlir::Operation *taker, mlir::Value value) + { + // a store into a record, which rc retains as a whole after it is filled + if (auto *retain = recordRetainOfStore(taker)) + { + return retain; + } + + mlir::Operation *first = taker; + forEachUse(value, [&](mlir::Operation *user, mlir::Value) { + if (mlir::isa(user) && user->getBlock() == taker->getBlock() && + user->isBeforeInBlock(first)) + { + first = user; + } + }); + + return first; + } + + bool OwnershipInferencePass::takerAcquires(mlir::Operation *taker, mlir::Value value) + { + // a callee that keeps its argument retained it in its own body under rc + if (isCall(taker)) + { + return true; + } + + if (recordRetainOfStore(taker)) + { + return true; + } + + if (auto varOp = mlir::dyn_cast(taker)) + { + return llvm::any_of(varOp.getResult().getUsers(), + [](mlir::Operation *user) { return mlir::isa(user); }); + } + + auto acquires = false; + forEachUse(value, [&](mlir::Operation *user, mlir::Value) { + acquires = acquires || (mlir::isa(user) && user->getBlock() == taker->getBlock() && + user->isBeforeInBlock(taker)); + }); + + return acquires; + } + +} // namespace own_inference diff --git a/tslang/lib/TypeScript/OwnershipInferenceRecords.cpp b/tslang/lib/TypeScript/OwnershipInferenceRecords.cpp new file mode 100644 index 000000000..51c66ccd6 --- /dev/null +++ b/tslang/lib/TypeScript/OwnershipInferenceRecords.cpp @@ -0,0 +1,479 @@ +// OwnershipInferencePass (see OwnershipInferencePass.h): borrowing records (spec 32). + +#include "OwnershipInferencePass.h" + +#define DEBUG_TYPE "own" + +namespace own_inference +{ + + bool OwnershipInferencePass::mayPointInto(mlir::Value value) + { + auto type = value.getType(); + return mlir::isa(type) || + ownsHeap(value); + } + + OwnershipInferencePass::BorrowingRecord *OwnershipInferencePass::borrowingRecordOf(mlir_ts::VariableOp varOp) + { + for (auto &record : records) + { + if (record->local == varOp) + { + return record.get(); + } + } + + return nullptr; + } + + void OwnershipInferencePass::classifyRecords() + { + getFunction().walk([&](mlir_ts::NewOp newOp) { + auto fields = recordBorrowedFields(newOp); + auto varOp = fields.empty() ? mlir_ts::VariableOp() : recordLocalOf(newOp.getResult()); + if (!varOp) + { + return; + } + + auto record = std::make_shared(); + record->box = newOp; + record->local = varOp; + for (auto *user : varOp->getUsers()) + { + auto propertyRefOp = mlir::dyn_cast(user); + if (!propertyRefOp) + { + continue; + } + + auto borrows = llvm::is_contained(fields, static_cast(propertyRefOp.getPosition())); + for (auto *fieldUser : propertyRefOp->getUsers()) + { + auto storeOp = mlir::dyn_cast(fieldUser); + if (!storeOp || storeOp.getReference() != propertyRefOp.getResult()) + { + continue; + } + + auto source = recordFieldSource(storeOp.getValue()); + if (source == RecordFieldSource::NoBlock) + { + continue; + } + + if (source == RecordFieldSource::Unknown || borrows != (source != RecordFieldSource::Fresh)) + { + return; + } + + if (borrows) + { + record->borrowed.push_back({storeOp, source}); + } + } + } + + for (auto *user : newOp->getUsers()) + { + if (auto castOp = mlir::dyn_cast(user)) + { + for (auto *castUser : castOp->getUsers()) + { + if (auto newInterfaceOp = mlir::dyn_cast(castUser)) + { + record->interfaces.push_back(newInterfaceOp.getResult()); + } + } + } + } + + // a path back to a use through the record's making is another record + llvm::SmallVector kills{newOp.getOperation(), varOp.getOperation()}; + walkRecord(record->interfaces, kills, record->walk); + for (auto &field : record->borrowed) + { + setAttrTracked(field.store, OWN_CAPTURE_BORROW_ATTR_NAME); + recordStoreUses[field.store.getOperation()] = record.get(); + } + + records.push_back(std::move(record)); + }); + } + + void OwnershipInferencePass::walkRecord(llvm::ArrayRef starts, llvm::ArrayRef kills, RecordWalk &walk) + { + struct Pending + { + mlir::Value value; + bool record; + }; + + llvm::SmallVector work; + llvm::SmallPtrSet seen; + auto push = [&](mlir::Value value, bool record) { + if ((record || mayPointInto(value)) && seen.insert(value).second) + { + work.push_back({value, record}); + } + }; + + auto followSlot = [&](mlir_ts::VariableOp varOp, bool record) { + for (auto *slotUser : varOp->getUsers()) + { + // its releases and its other values are the slot's own + if (auto loadOp = mlir::dyn_cast(slotUser)) + { + push(loadOp.getResult(), record); + } + } + }; + + auto keep = [&](mlir::Operation *user) { walk.kept = walk.kept ? walk.kept : user; }; + + for (auto start : starts) + { + push(start, true); + } + + while (!work.empty()) + { + auto pending = work.pop_back_val(); + auto value = pending.value; + for (auto &use : value.getUses()) + { + auto *user = use.getOwner(); + if (mlir::isa(user) || + (pending.record && mlir::isa(user))) + { + continue; + } + + // the same value: a view, a cast, a merge + if ((isView(user) && use.getOperandNumber() == 0) || mlir::isa(user)) + { + push(user->getResult(0), pending.record); + continue; + } + + if (auto branchOp = mlir::dyn_cast(user)) + { + if (auto argument = branchOp.getSuccessorBlockArgument(use.getOperandNumber())) + { + push(*argument, pending.record); + } + + continue; + } + + // a local that holds it: its reads + auto varOp = mlir::dyn_cast(user); + auto storeOp = mlir::dyn_cast(user); + if (varOp || (storeOp && storeOp.getValue() == value)) + { + auto slotOp = varOp ? varOp : storeOp.getReference().getDefiningOp(); + if (slotOp && !slotOp.getCaptured().value_or(false)) + { + followSlot(slotOp, pending.record); + continue; + } + + // a place, a global, a captured variable: what is read out of the record is checked + // where it is read (checkPlaceRead) + walk.uses.push_back({user, {kills.begin(), kills.end()}}); + if (pending.record) + { + keep(user); + } + + continue; + } + + walk.uses.push_back({user, {kills.begin(), kills.end()}, isCall(user) && isCalleeOperand(use)}); + if (mlir::isa(user)) + { + if (pending.record) + { + walk.returns.push_back(user); + } + + continue; + } + + if (isCall(user)) + { + if (pending.record && isKeptArgument(user, value)) + { + keep(user); + continue; + } + + // a result that borrows it - a plain borrow (`id(r)`), or a borrowing record that + // holds it - points where it points + auto args = callArgs(user); + auto given = [&](llvm::ArrayRef indices) { + return llvm::any_of(indices, [&](int index) { + return static_cast(index) < args.size() && args[index] == value; + }); + }; + + if (user->getNumResults() == 1 && given(resultBorrowsAll(user))) + { + push(user->getResult(0), pending.record || isRecordResult(user)); + } + + // ...and a generator's state that borrows it (OWN_RESULT_BOUNDED_ATTR_NAME) + if (auto bounded = user->getAttrOfType(OWN_RESULT_BOUNDED_ATTR_NAME); + bounded && user->getNumResults() == 1 && + given(llvm::SmallVector(bounded.asArrayRef().begin(), bounded.asArrayRef().end()))) + { + push(user->getResult(0), false); + } + + continue; + } + + if (pending.record && !isBorrow(user, value)) + { + keep(user); + continue; + } + + // a field written through the record: its old value may be one the record borrows, + // which the overwrite would release, and the field may be one its release leaves + if (pending.record && mlir::isa(user)) + { + for (auto *fieldUser : user->getResult(0).getUsers()) + { + auto fieldStore = mlir::dyn_cast(fieldUser); + if (mlir::isa(fieldUser) || + (fieldStore && fieldStore.getReference() == user->getResult(0))) + { + keep(fieldUser); + } + } + } + + // what it produces - a field's reference, a value read out of it, a bound method - may + // still point into what the record borrows + for (auto result : user->getResults()) + { + push(result, false); + } + } + } + } + + llvm::StringRef OwnershipInferencePass::recordName(mlir::Value value) + { + llvm::StringRef name = ownerName(value); + forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { + if (auto varOp = mlir::dyn_cast(user); varOp && varOp.getInitializer() == used) + { + name = varName(varOp); + } + }); + + return name; + } + + std::string OwnershipInferencePass::describeRecordSource(BorrowedField field) + { + auto value = field.store.getValue(); + auto quoted = [](llvm::StringRef name) { return "'" + name.str() + "'"; }; + switch (field.source) + { + case RecordFieldSource::Param: + return quoted(paramName(borrowedParam(value))); + case RecordFieldSource::Place: + if (auto loadOp = rootOf(value).getDefiningOp(); loadOp && isPlace(loadOp.getReference())) + { + return describePlace(loadOp.getReference()); + } + + return describeCall(rootOf(value).getDefiningOp()); + case RecordFieldSource::Slot: + return quoted(varName(slotLoadOf(value).getReference().getDefiningOp())); + case RecordFieldSource::Cell: + return "a captured variable"; + default: + return "a value it does not own"; + } + } + + void OwnershipInferencePass::checkBorrowingRecords() + { + for (auto &record : records) + { + auto name = recordName(record->interfaces.empty() ? record->box.getResult() : record->interfaces.front()); + if (record->walk.kept) + { + reportPlaceBorrowEscapes(record->walk.kept, name, + record->borrowed.empty() ? "a value it does not own" + : describeRecordSource(record->borrowed.front())); + continue; + } + + auto ok = true; + for (auto *returned : record->walk.returns) + { + for (auto &field : record->borrowed) + { + if (!ok || field.source == RecordFieldSource::Immortal) + { + continue; + } + + auto param = field.source == RecordFieldSource::Param || field.source == RecordFieldSource::Place + ? borrowedParam(field.store.getValue()) + : -1; + if (param < 0 || !llvm::is_contained(returnsRecordOf, param)) + { + reportPlaceBorrowEscapes(returned, name, describeRecordSource(field)); + ok = false; + } + } + } + + for (auto &field : record->borrowed) + { + if (!ok) + { + break; + } + + auto value = field.store.getValue(); + Chain chain; + if (field.source == RecordFieldSource::Slot) + { + chain.roots.push_back({slotLoadOf(value).getReference(), Chain::Slot}); + } + else if (auto load = field.source == RecordFieldSource::Param ? keptParamLoadOf(value) : mlir_ts::LoadOp()) + { + // a parameter this function keeps is its own to move away, as a local's value is + chain.roots.push_back({load.getReference(), Chain::Slot}); + } + else if (field.source == RecordFieldSource::Cell) + { + chain.roots.push_back({rootOf(value), Chain::NotOwned}); + } + else + { + continue; + } + + auto ends = rootEnds(chain); + if (field.source == RecordFieldSource::Cell) + { + for (auto *drop : drops) + { + if (dropsChain(drop, chain, nullptr, value)) + { + ends.push_back(drop); + } + } + } + + for (auto *end : ends) + { + for (auto &use : record->walk.uses) + { + if (ok && (use.op != end || !use.callee) && reachableAfter(end, use.op, use.kills)) + { + if (field.source == RecordFieldSource::Slot) + { + reportBorrowOutlives(use.op, end, name, varName(chain.roots.front().first.getDefiningOp())); + } + else + { + reportPlaceBorrowOutlives(use.op, end, name, describeRecordSource(field)); + } + + ok = false; + } + } + } + } + } + } + + void OwnershipInferencePass::checkRecordResults() + { + getFunction().walk([&](mlir::Operation *call) { + if (!isCall(call) || !isRecordResult(call) || call->getNumResults() != 1) + { + return; + } + + auto result = call->getResult(0); + auto place = describeCall(call); + auto name = recordName(result); + RecordWalk walk; + llvm::SmallVector kills{call}; + walkRecord({result}, kills, walk); + if (walk.kept) + { + reportPlaceBorrowEscapes(walk.kept, name, place); + return; + } + + auto args = callArgs(call); + for (auto *returned : walk.returns) + { + for (auto index : resultBorrowsAll(call)) + { + if (static_cast(index) >= args.size() || argumentHoldsNoBlock(args[index]) || + isImmortalLiteral(args[index])) + { + continue; + } + + auto param = borrowedParam(args[index]); + if (param < 0 || !llvm::is_contained(returnsRecordOf, param)) + { + reportPlaceBorrowEscapes(returned, name, place); + return; + } + } + } + + auto chain = chainOf(call); + // rc's retain of a fresh argument right before the call is no end: the box's own check + // decides it, and the use that takes it is an end of its own + llvm::SmallVector ends; + for (auto *end : rootEnds(chain)) + { + auto ownedRetain = mlir::isa(end) && llvm::any_of(chain.roots, [&](auto &root) { + return root.second == Chain::Owned && rootOf(end->getOperand(0)) == root.first; + }); + if (!ownedRetain) + { + ends.push_back(end); + } + } + + for (auto *drop : drops) + { + if (drop != call && dropsChain(drop, chain, call)) + { + ends.push_back(drop); + } + } + + for (auto *end : ends) + { + for (auto &use : walk.uses) + { + if ((use.op != end || !use.callee) && reachableAfter(end, use.op, use.kills)) + { + reportPlaceBorrowOutlives(use.op, end, name, place); + return; + } + } + } + }); + } + +} // namespace own_inference diff --git a/tslang/lib/TypeScript/OwnershipSignatureCallbacks.cpp b/tslang/lib/TypeScript/OwnershipSignatureCallbacks.cpp new file mode 100644 index 000000000..39fb5e4a6 --- /dev/null +++ b/tslang/lib/TypeScript/OwnershipSignatureCallbacks.cpp @@ -0,0 +1,423 @@ +// OwnershipSignaturePass (see OwnershipSignaturePass.h): callbacks (spec 33) - the functions passed +// for a parameter, and the facts their families agree on. + +#include "OwnershipSignaturePass.h" + +#define DEBUG_TYPE "own" + +namespace own_signature +{ + + bool OwnershipSignaturePass::isFunctionValueType(mlir::Type type) + { + return mlir::isa(type); + } + + bool OwnershipSignaturePass::onlyCalledParam(mlir::BlockArgument argument) + { + for (auto *user : argument.getUsers()) + { + auto varOp = mlir::dyn_cast(user); + auto index = -1; + if (!varOp || !isParameterSlot(varOp, index)) + { + return false; + } + + for (auto *slotUser : varOp->getUsers()) + { + if (mlir::isa(slotUser)) + { + continue; + } + + auto loadOp = mlir::dyn_cast(slotUser); + if (!loadOp) + { + return false; + } + + for (auto &use : loadOp->getUses()) + { + auto *call = use.getOwner(); + if (!isCall(call) || calleeValue(call) != use.get() || + llvm::is_contained(callArgs(call), use.get())) + { + return false; + } + } + } + } + + return true; + } + + mlir_ts::SymbolRefOp OwnershipSignaturePass::passedFunction(mlir::Value value, int32_t &shift) + { + while (auto castOp = value.getDefiningOp()) + { + if (!isFunctionValueType(castOp.getIn().getType()) || !isFunctionValueType(castOp.getType())) + { + return {}; + } + + value = castOp.getIn(); + } + + if (auto refOp = value.getDefiningOp()) + { + shift = 0; + return refOp; + } + + auto boundOp = value.getDefiningOp(); + if (!boundOp || closureOfBox(boundOp.getThisVal()) != boundOp) + { + return {}; + } + + shift = 1; + return boundOp.getFunc().getDefiningOp(); + } + + bool OwnershipSignaturePass::passedOnly(mlir::Value value, llvm::function_ref passes, int depth) + { + if (depth > 8) + { + return false; + } + + for (auto &use : value.getUses()) + { + auto *user = use.getOwner(); + if (mlir::isa(user) || passes(use)) + { + continue; + } + + auto castOp = mlir::dyn_cast(user); + auto boundOp = mlir::dyn_cast(user); + if (castOp && isFunctionValueType(castOp.getType()) && passedOnly(castOp.getResult(), passes, depth + 1)) + { + continue; + } + + if (boundOp && boundOp.getFunc() == use.get() && closureOfBox(boundOp.getThisVal()) == boundOp && + passedOnly(boundOp.getResult(), passes, depth + 1)) + { + continue; + } + + return false; + } + + return true; + } + + void OwnershipSignaturePass::collectCallbackFamilies(mlir::ModuleOp module) + { + auto uses = mlir::SymbolTable::getSymbolUses(&module.getBodyRegion()); + if (!uses) + { + return; + } + + llvm::DenseMap> usersOf; + for (auto &use : *uses) + { + if (auto funcOp = functions.lookup(use.getSymbolRef().getRootReference().getValue())) + { + usersOf[funcOp].push_back(use.getUser()); + } + } + + auto callsOnly = [&](mlir_ts::FuncOp funcOp) { + auto &users = usersOf[funcOp]; + return !users.empty() && llvm::all_of(users, [&](mlir::Operation *user) { + auto callee = directCallee(user); + return callee && callee.getValue() == funcOp.getSymName(); + }); + }; + + for (auto &entry : functions) + { + auto funcOp = entry.second; + if (funcOp.isDeclaration() || callableFromOutside(funcOp) || declaresFacts(funcOp) || !callsOnly(funcOp)) + { + continue; + } + + for (auto argument : funcOp.getBody().front().getArguments()) + { + if (!isFunctionValueType(argument.getType()) || !onlyCalledParam(argument)) + { + continue; + } + + auto index = static_cast(argument.getArgNumber()); + CallbackFamily family; + for (auto *user : usersOf[funcOp]) + { + auto args = callArgs(user); + auto shift = 0; + auto refOp = static_cast(index) < args.size() ? passedFunction(args[index], shift) + : mlir_ts::SymbolRefOp(); + auto member = refOp ? functions.lookup(refOp.getIdentifier()) : mlir_ts::FuncOp(); + if (!member || member.isDeclaration() || callableFromOutside(member) || declaresFacts(member)) + { + family.closed = false; + break; + } + + auto symbol = member.getSymNameAttr(); + auto same = llvm::find_if(family.members, [&](const CallbackMember &known) { return known.symbol == symbol; }); + if (same != family.members.end() && same->shift != shift) + { + family.closed = false; + break; + } + + if (same == family.members.end()) + { + family.members.push_back({symbol, shift}); + } + + family.passing.push_back(refOp); + } + + callbackFamilies[{funcOp, index}] = std::move(family); + } + } + + // A member's symbol used anywhere else - a call through another value, a field, a vtable, a + // global - reaches callers this cannot see; so does one passed to a family that is not closed. + // Each family that has such a member is open, until nothing changes. + auto passesToClosed = [&](mlir::OpOperand &use) { + auto *call = use.getOwner(); + auto callee = directCallee(call); + auto calleeOp = callee ? functions.lookup(callee.getValue()) : mlir_ts::FuncOp(); + if (!calleeOp) + { + return false; + } + + // the argument this use is, by its operand's position + auto args = callArgs(call); + if (args.empty() || use.getOperandNumber() < args.getBeginOperandIndex() || + use.getOperandNumber() >= args.getBeginOperandIndex() + args.size()) + { + return false; + } + + auto index = static_cast(use.getOperandNumber() - args.getBeginOperandIndex()); + auto found = callbackFamilies.find({calleeOp, index}); + return found != callbackFamilies.end() && found->second.closed; + }; + + for (auto changed = true; changed;) + { + changed = false; + for (auto &[key, family] : callbackFamilies) + { + if (!family.closed) + { + continue; + } + + for (auto &member : family.members) + { + auto memberOp = functions.lookup(member.symbol.getValue()); + auto escapes = llvm::any_of(usersOf[memberOp], [&](mlir::Operation *user) { + if (auto callee = directCallee(user); callee && callee.getValue() == member.symbol.getValue()) + { + return false; + } + + auto refOp = mlir::dyn_cast(user); + return !refOp || !user->getParentOfType() || + !passedOnly(refOp.getResult(), passesToClosed); + }); + + if (escapes) + { + family.closed = false; + changed = true; + break; + } + } + } + } + + for (auto &[key, family] : callbackFamilies) + { + if (family.closed) + { + callbackPassing.insert(family.passing.begin(), family.passing.end()); + } + } + } + + void OwnershipSignaturePass::resolveCallback(mlir_ts::LoadOp loadOp, Callees &callees) + { + auto varOp = loadOp.getReference().getDefiningOp(); + auto funcOp = loadOp->getParentOfType(); + auto index = -1; + if (!varOp || !funcOp || !isParameterSlot(varOp, index)) + { + return; + } + + auto found = callbackFamilies.find({funcOp.getOperation(), index}); + if (found == callbackFamilies.end() || !found->second.closed || found->second.members.empty()) + { + return; + } + + callees.viaCallback = true; + callees.known = true; + for (auto &member : found->second.members) + { + callees.named.push_back(member.symbol); + callees.known = addDefined(member.symbol, callees) && callees.known; + callees.shifts.push_back(member.shift); + } + + callees.known = callees.known && callees.shifts.size() == callees.funcs.size(); + } + + bool OwnershipSignaturePass::shiftFacts(const Facts &facts, int32_t shift, Facts &shifted) + { + shifted = Facts(); + shifted.record = facts.record; + for (auto [from, to] : {std::make_pair(&facts.owned, &shifted.owned), std::make_pair(&facts.borrows, &shifted.borrows)}) + { + for (auto index : *from) + { + if (index < shift) + { + return false; + } + + to->push_back(index - shift); + } + } + + return true; + } + + bool OwnershipSignaturePass::canReleaseParam(mlir_ts::FuncOp funcOp, int32_t index) + { + if (funcOp.getBody().empty() || static_cast(index) >= funcOp.getNumArguments()) + { + return false; + } + + auto argument = funcOp.getBody().front().getArgument(index); + if (isHandle(argument) || isCopyableString(argument.getType()) || holdsNoBlock(argument)) + { + return false; + } + + return llvm::any_of(argument.getUsers(), [&](mlir::Operation *user) { + auto varOp = mlir::dyn_cast(user); + auto position = -1; + return varOp && isParameterSlot(varOp, position) && position == index; + }); + } + + bool OwnershipSignaturePass::meetCallbacks(llvm::DenseSet &demoted) + { + auto changed = false; + for (auto &[key, family] : callbackFamilies) + { + if (!family.closed) + { + continue; + } + + llvm::SmallVector members; + llvm::SmallVector seen; + auto shiftable = true; + for (auto &member : family.members) + { + auto funcOp = functions.lookup(member.symbol.getValue()); + Facts shifted; + shiftable = shiftable && funcOp && shiftFacts(factsEffective.lookup(funcOp), member.shift, shifted); + members.push_back(funcOp); + seen.push_back(shifted); + } + + if (shiftable && llvm::all_equal(seen)) + { + continue; + } + + // a parameter some member keeps, and each other one only borrows: kept by all + llvm::SmallVector kept; + for (auto &facts : seen) + { + kept.append(facts.owned.begin(), facts.owned.end()); + } + + llvm::sort(kept); + kept.erase(std::unique(kept.begin(), kept.end()), kept.end()); + auto promotable = shiftable && llvm::all_of(seen, [&](const Facts &facts) { + return facts.borrows == seen.front().borrows && facts.record == seen.front().record && + llvm::none_of(kept, [&](int32_t index) { return llvm::is_contained(facts.borrows, index); }); + }); + + for (auto [position, member] : llvm::enumerate(family.members)) + { + auto funcOp = members[position]; + // a member another caller reaches without the facts is given none (findOpen) + for (auto index : kept) + { + promotable = promotable && !demoted.contains(funcOp) && !open.contains(funcOp) && + !interfaceReached.contains(funcOp) && + (llvm::is_contained(seen[position].owned, index) || + canReleaseParam(funcOp, index + member.shift)); + } + } + + if (promotable) + { + for (auto [position, member] : llvm::enumerate(family.members)) + { + auto funcOp = members[position]; + auto facts = factsEffective.lookup(funcOp); + for (auto index : kept) + { + auto param = index + member.shift; + if (!llvm::is_contained(facts.owned, param)) + { + facts.owned.insert(llvm::upper_bound(facts.owned, param), param); + } + } + + if (facts != factsEffective.lookup(funcOp)) + { + factsEffective[funcOp] = facts; + changed = true; + } + } + + continue; + } + + for (auto funcOp : members) + { + if (funcOp && !declaresCallerFacts(funcOp)) + { + demoted.insert(funcOp); + if (factsEffective.erase(funcOp)) + { + changed = true; + } + } + } + } + + return changed; + } + +} // namespace own_signature diff --git a/tslang/lib/TypeScript/OwnershipSignatureClosedWorld.cpp b/tslang/lib/TypeScript/OwnershipSignatureClosedWorld.cpp new file mode 100644 index 000000000..749d60f0d --- /dev/null +++ b/tslang/lib/TypeScript/OwnershipSignatureClosedWorld.cpp @@ -0,0 +1,452 @@ +// OwnershipSignaturePass (see OwnershipSignaturePass.h): the closed world - which functions another +// module can call, which escape as values, and the families that are open. + +#include "OwnershipSignaturePass.h" + +#define DEBUG_TYPE "own" + +namespace own_signature +{ + + bool OwnershipSignaturePass::callableFromOutside(mlir_ts::FuncOp funcOp) + { + if (!funcOp.isPrivate() && (funcOp.isDeclaration() || funcOp->hasAttr("export") || funcOp->hasAttr(DLL_EXPORT))) + { + return true; + } + + // its class: the longest own class name that prefixes its symbol + auto symbol = funcOp.getSymName(); + llvm::StringRef owner; + for (auto &entry : ownClasses) + { + auto name = entry.getKey(); + if (name.size() > owner.size() && symbol.size() > name.size() && symbol.starts_with(name) && symbol[name.size()] == '.') + { + owner = name; + } + } + + if (owner.empty()) + { + return !funcOp.isPrivate(); + } + + auto nameable = [&](llvm::StringRef name) { + auto last = name.rsplit('.').second; + return nameableClasses.contains(name) || exportWords.contains(name) || (!last.empty() && exportWords.contains(last)); + }; + + if (!funcOp.isPrivate() && nameable(owner)) + { + return true; + } + + // An override of a method of a nameable class, or of one another module defines (no own + // vtable here), fills a slot of that class's vtable: another module calls it through the + // base (review6r1 p5, p6, p7; controller ruling: any base of that kind on the chain). The + // base is the first field of a derived class's storage. + auto storage = classStorages.lookup(owner); + if (!storage) + { + // its storage is not known here: no telling what it extends + return !funcOp.isPrivate(); + } + + for (auto depth = 0; depth < 64; ++depth) + { + auto fields = storage.getFields(); + auto base = fields.empty() ? mlir_ts::ClassStorageType() : mlir::dyn_cast(fields.front().type); + if (!base) + { + return false; + } + + auto name = base.getName().getValue(); + if (nameable(name) || !ownClasses.contains(name)) + { + return true; + } + + storage = base; + } + + return true; + } + + bool OwnershipSignaturePass::holdsFunction(mlir::Type type, bool fields, SeenTypes &seen) + { + if (!type || !seen.insert({type, fields}).second) + { + return false; + } + + if (mlir::isa(type)) + { + return true; + } + + if (auto interfaceType = mlir::dyn_cast(type)) + { + auto found = interfaceVTableTypes.find(interfaceType.getName().getValue()); + if (found == interfaceVTableTypes.end()) + { + return true; + } + + return llvm::any_of(found->second, [&](mlir_ts::TupleType vtableType) { + return llvm::any_of(llvm::drop_begin(vtableType.getFields(), 1), + [&](const mlir_ts::FieldInfo &info) { return holdsFunction(info.type, /*fields=*/true, seen); }); + }); + } + + auto any = [&](llvm::ArrayRef infos) { + return llvm::any_of(infos, [&](const mlir_ts::FieldInfo &info) { return holdsFunction(info.type, fields, seen); }); + }; + + if (auto tupleType = mlir::dyn_cast(type)) + { + return any(tupleType.getFields()); + } + + if (auto constTupleType = mlir::dyn_cast(type)) + { + return any(constTupleType.getFields()); + } + + if (auto classStorageType = mlir::dyn_cast(type)) + { + return fields && any(classStorageType.getFields()); + } + + if (auto objectStorageType = mlir::dyn_cast(type)) + { + return fields && any(objectStorageType.getFields()); + } + + if (mlir::isa(type) && !fields) + { + return false; + } + + auto found = false; + type.walkImmediateSubElements([](mlir::Attribute) {}, [&](mlir::Type sub) { found = found || holdsFunction(sub, fields, seen); }); + return found; + } + + mlir_ts::ClassStorageType OwnershipSignaturePass::cppClassOfMethod(mlir_ts::FuncOp funcOp) + { + auto inputs = funcOp.getFunctionType().getInputs(); + auto classType = inputs.empty() ? mlir_ts::ClassType() : mlir::dyn_cast(inputs.front()); + auto storage = classType ? mlir::dyn_cast(classType.getStorageType()) : mlir_ts::ClassStorageType(); + if (!storage || !storage.isCpp()) + { + return {}; + } + + auto name = storage.getName().getValue(); + auto symbol = funcOp.getSymName(); + return symbol.size() > name.size() && symbol.starts_with(name) && symbol[name.size()] == '.' ? storage : mlir_ts::ClassStorageType(); + } + + bool OwnershipSignaturePass::addressType(mlir::Type type) + { + return mlir::isa(type); + } + + void OwnershipSignaturePass::findEscapingFunctions(mlir::ModuleOp module) + { + // every body-less function: C's, another module's, a `declare class`'s (its methods, its + // `..instanceOf`, its `..new`), a `@cpp` class's slot + for (auto &entry : functions) + { + auto funcOp = entry.second; + if (funcOp.isDeclaration() && !scalarSignature(funcOp.getFunctionType())) + { + functionsEscape = true; + return; + } + + // a method of a `@cpp` class: C++ calls it through the class's C++ vtable + if (cppClassOfMethod(funcOp)) + { + functionsEscape = true; + return; + } + } + + // every global declared with no initializer or imported: a `declare var`, a `declare + // class`'s vtable (functions) and type name (a string) + for (auto &entry : globals) + { + auto globalOp = entry.second; + auto foreign = globalOp->hasAttr("import") || globalOp->hasAttr(DLL_IMPORT) || globalOp.getInitializerRegion().empty() || + globalOp.getInitializerRegion().front().empty(); + if (foreign && !scalarType(globalOp.getType())) + { + functionsEscape = true; + return; + } + } + + // every `@cpp` class's fields; a base (the first field, a storage) is a `@cpp` class of its + // own, judged on its own + for (auto &entry : classStorages) + { + auto storage = entry.second; + if (!storage.isCpp()) + { + continue; + } + + for (auto [index, field] : llvm::enumerate(storage.getFields())) + { + auto base = index == 0 ? mlir::dyn_cast(field.type) : mlir_ts::ClassStorageType(); + if (base ? !base.isCpp() : !scalarType(field.type)) + { + functionsEscape = true; + return; + } + } + } + + module.walk([&](mlir::Operation *op) { + if (functionsEscape) + { + return; + } + + // a function found in a library at run time: scalar through every cast of its address + if (mlir::isa(op)) + { + functionsEscape = !llvm::all_of(op->getResult(0).getUsers(), [](mlir::Operation *user) { + auto castOp = mlir::dyn_cast(user); + auto functionType = castOp ? mlir::dyn_cast(castOp.getType()) : mlir_ts::FunctionType(); + return functionType && scalarSignature(functionType); + }); + return; + } + + // `inline_asm(...)` and `call_intrinsic(...)`: a function given to either is data + // of no type (review6r5 h1, h2: `inline_asm("mov $1, $0", "=r,r", onSig)`) + if (mlir::isa(op)) + { + functionsEscape = llvm::any_of(op->getOperands(), [&](mlir::Value operand) { + SeenTypes seen; + return holdsFunction(operand.getType(), /*fields=*/true, seen); + }); + return; + } + + auto castOp = mlir::dyn_cast(op); + if (!castOp) + { + return; + } + + // a vtable's address (a class's, an interface's, a thunk's cell) seen as an opaque + // pointer or an integer: the compiler's own, inside a vtable or storing an object's + // vtable, never a value a program gives C + auto vtableOp = op->getParentOfType(); + auto addressOfOp = addressOfThroughCasts(castOp.getIn()); + auto isVTable = [](llvm::StringRef name) { return name.ends_with("..vtbl") || name.contains("..vtbl."); }; + if ((vtableOp && isVTable(vtableOp.getSymName())) || (addressOfOp && isVTable(addressOfOp.getGlobalName()))) + { + return; + } + + // an opaque pointer or a reference made out of a number: an address of foreign memory + if (mlir::isa(castOp.getType()) && !addressType(castOp.getIn().getType())) + { + functionsEscape = true; + return; + } + + // a function seen as something that holds none; a boolean cannot carry one. A class or + // object takes its fields along, seen as anything (`any`, an opaque pointer, an integer, + // a reference: `>k`, then a load of the field, review6r5 g2) but a class + // (its base) or an interface (a view of it, read through its vtable only) + if (mlir::isa(castOp.getType())) + { + return; + } + + auto fields = !mlir::isa(castOp.getType()); + SeenTypes seenIn; + SeenTypes seenOut; + functionsEscape = holdsFunction(castOp.getIn().getType(), fields, seenIn) && + !holdsFunction(castOp.getType(), /*fields=*/true, seenOut); + }); + } + + void OwnershipSignaturePass::collectClassStorages(mlir::ModuleOp module) + { + llvm::DenseSet seen; + std::function visit = [&](mlir::Type type) { + if (!type || !seen.insert(type).second) + { + return; + } + + if (auto storage = mlir::dyn_cast(type)) + { + classStorages[storage.getName().getValue()] = storage; + for (auto &field : storage.getFields()) + { + visit(field.type); + } + + return; + } + + type.walkImmediateSubElements([](mlir::Attribute) {}, visit); + }; + + for (auto &entry : functions) + { + visit(entry.second.getFunctionType()); + } + + module.walk([&](mlir::Operation *op) { + for (auto result : op->getResults()) + { + if (mlir::isa(result.getType())) + { + visit(result.getType()); + } + } + }); + } + + bool OwnershipSignaturePass::onlyCalled(mlir::Value value) + { + for (auto &use : value.getUses()) + { + auto *user = use.getOwner(); + if (mlir::isa(user) && + calleeValue(user) == use.get() && use.getOperandNumber() == 0) + { + continue; + } + + if (mlir::isa(user)) + { + continue; + } + + if (mlir::isa(user) && onlyCalled(user->getResult(0))) + { + continue; + } + + return false; + } + + return true; + } + + void OwnershipSignaturePass::findOpen(mlir::ModuleOp module) + { + for (auto &entry : functions) + { + if (callableFromOutside(entry.second)) + { + open.try_emplace(entry.second, "it can be called from another module; '@" LENDS_DECORATOR "' or '@" KEEPS_DECORATOR + "' would say it in its signature"); + } + } + + // the module is a symbol table, which getSymbolUses does not look inside: walk its body + auto uses = mlir::SymbolTable::getSymbolUses(&module.getBodyRegion()); + if (!uses) + { + // an op this pass cannot read symbols through: nothing is closed + for (auto &entry : functions) + { + open.try_emplace(entry.second, "its callers cannot all be found"); + } + + return; + } + + for (auto &use : *uses) + { + auto symbol = use.getSymbolRef().getRootReference(); + auto funcOp = functions.lookup(symbol.getValue()); + if (!funcOp) + { + continue; + } + + auto *user = use.getUser(); + if (mlir::isa(user)) + { + continue; + } + + if (mlir::isa(user) && isClassVTableEntry(user)) + { + continue; + } + + // passed only to a parameter whose family is closed: its calls are resolved (spec 33) + if (callbackPassing.contains(user)) + { + continue; + } + + int64_t index = -1; + if (auto refOp = mlir::dyn_cast(user)) + { + index = refOp.getIndex(); + } + else if (auto refOp = mlir::dyn_cast(user)) + { + index = refOp.getIndex(); + } + + auto inFunction = !!user->getParentOfType(); + if (inFunction && mlir::isa(user) && + onlyCalled(user->getResult(0))) + { + // a virtual call this pass cannot resolve reaches its family without the facts + if (index >= 0) + { + Callees callees; + resolveFamily(index, symbol, callees); + if (!callees.known) + { + openFamily(index, symbol, "an override may be defined in another module"); + } + } + + continue; + } + + open.try_emplace(funcOp, "it is used other than by a call"); + if (index >= 0) + { + openFamily(index, symbol, "it is used other than by a call"); + } + } + } + + void OwnershipSignaturePass::openFamily(int64_t index, mlir::StringAttr symbol, const char *why) + { + for (auto member : familyOf(index, symbol)) + { + if (auto memberOp = functions.lookup(member.getValue())) + { + open.try_emplace(memberOp, why); + } + } + } + +} // namespace own_signature diff --git a/tslang/lib/TypeScript/OwnershipSignatureGenerators.cpp b/tslang/lib/TypeScript/OwnershipSignatureGenerators.cpp new file mode 100644 index 000000000..231728223 --- /dev/null +++ b/tslang/lib/TypeScript/OwnershipSignatureGenerators.cpp @@ -0,0 +1,749 @@ +// OwnershipSignaturePass (see OwnershipSignaturePass.h): generators - the iterator protocol, the +// fields a generator yields, and states that borrow their arguments (phase 7b). + +#include "OwnershipSignaturePass.h" + +#define DEBUG_TYPE "own" + +namespace own_signature +{ + + bool OwnershipSignaturePass::isIteratorNext(mlir::Operation *op) + { + if (op->getNumResults() != 1) + { + return false; + } + + auto tupleType = mlir::dyn_cast(op->getResult(0).getType()); + if (!tupleType || tupleType.getFields().size() != 2) + { + return false; + } + + auto fieldName = [&](int index) { + auto id = mlir::dyn_cast_or_null(tupleType.getFields()[index].id); + return id ? id.getValue() : llvm::StringRef(); + }; + if (fieldName(0) != "value" || fieldName(1) != "done") + { + return false; + } + + auto isNext = [](llvm::StringRef name) { return name == "next" || name.ends_with(".next"); }; + if (auto callee = directCallee(op)) + { + return isNext(callee.getValue()); + } + + auto function = calleeValue(op); + if (!function) + { + return false; + } + + if (auto getMethodOp = function.getDefiningOp()) + { + function = getMethodOp.getBoundFunc(); + } + + if (auto boundOp = function.getDefiningOp()) + { + function = boundOp.getFunc(); + } + + if (auto loadOp = function.getDefiningOp()) + { + if (auto refOp = loadOp.getReference().getDefiningOp()) + { + auto identifier = mlir::dyn_cast(refOp.getIdentifier()); + return identifier && isNext(identifier.getValue()); + } + + if (auto propertyRefOp = loadOp.getReference().getDefiningOp()) + { + auto storageType = storageTupleOfType(propertyRefOp->getOperand(0).getType()); + if (!storageType || propertyRefOp.getPosition() >= storageType.getFields().size()) + { + return false; + } + + auto id = mlir::dyn_cast_or_null(storageType.getFields()[propertyRefOp.getPosition()].id); + return id && isNext(id.getValue()); + } + + return false; + } + + if (auto refOp = function.getDefiningOp()) + { + return isNext(refOp.getIdentifier()); + } + + if (auto refOp = function.getDefiningOp()) + { + return isNext(refOp.getIdentifier()); + } + + if (auto refOp = function.getDefiningOp()) + { + return isNext(refOp.getIdentifier()); + } + + if (auto refOp = function.getDefiningOp()) + { + return isNext(refOp.getIdentifier()); + } + + return false; + } + + void OwnershipSignaturePass::findImportedStates(mlir::ModuleOp module) + { + llvm::MapVector states; + for (auto &call : calls) + { + if (!makesGeneratorState(call.op) || (call.callees.known && !call.callees.funcs.empty())) + { + continue; + } + + auto tupleType = storageTupleOfType(call.op->getResult(0).getType()); + auto borrows = !declaredByCallees(call.callees, OWN_DECLARED_BORROWS_ATTR_NAME).empty(); + states[tupleType] = states.lookup(tupleType) || borrows; + } + + if (states.empty()) + { + return; + } + + auto *context = &getContext(); + llvm::SmallVector entries; + if (auto existing = module->getAttrOfType(OWN_BORROWING_FIELDS_ATTR_NAME)) + { + entries.append(existing.begin(), existing.end()); + } + + for (auto &[type, borrows] : states) + { + auto tupleType = mlir::cast(type); + auto captured = fieldIndex(tupleType, CAPTURED_NAME); + // the box first, when it borrows; -1 names no field, so a box that owns is released + llvm::SmallVector fields{borrows && captured >= 0 ? captured : -1}; + for (auto [index, field] : llvm::enumerate(tupleType.getFields())) + { + if (static_cast(index) != captured && ownsHeap(module.getLoc(), field.type)) + { + fields.push_back(static_cast(index)); + } + } + + entries.push_back(mlir::ArrayAttr::get(context, {mlir::TypeAttr::get(type), mlir::DenseI32ArrayAttr::get(context, fields)})); + } + + module->setAttr(OWN_BORROWING_FIELDS_ATTR_NAME, mlir::ArrayAttr::get(context, entries)); + } + + bool OwnershipSignaturePass::makesGeneratorState(mlir::Operation *op) + { + if (op->getNumResults() != 1 || !mlir::isa(op->getResult(0).getType())) + { + return false; + } + + auto tupleType = storageTupleOfType(op->getResult(0).getType()); + if (!tupleType) + { + return false; + } + + auto hasStep = false; + auto hasNext = false; + for (auto field : tupleType.getFields()) + { + if (auto id = mlir::dyn_cast_or_null(field.id)) + { + hasStep = hasStep || id.getValue() == ".step"; + hasNext = hasNext || id.getValue() == "next"; + } + } + + return hasStep && hasNext; + } + + mlir_ts::TupleType OwnershipSignaturePass::storageTupleOfType(mlir::Type type) + { + if (auto objectType = mlir::dyn_cast(type)) + { + if (auto storageType = mlir::dyn_cast(objectType.getStorageType())) + { + return mlir_ts::TupleType::get(type.getContext(), storageType.getFields()); + } + + // an object a declaration names by its shape (another module's generator) + return mlir::dyn_cast(objectType.getStorageType()); + } + + if (auto refType = mlir::dyn_cast(type)) + { + type = refType.getElementType(); + } + else if (auto valueRefType = mlir::dyn_cast(type)) + { + type = valueRefType.getElementType(); + } + + return mlir::dyn_cast(type); + } + + bool OwnershipSignaturePass::isRecordFieldStore(mlir::Operation *user, mlir::Value value) + { + auto storeOp = mlir::dyn_cast(user); + auto propertyRefOp = storeOp && storeOp.getValue() == value + ? storeOp.getReference().getDefiningOp() + : mlir_ts::PropertyRefOp(); + auto recordOp = propertyRefOp ? propertyRefOp->getOperand(0).getDefiningOp() : mlir_ts::VariableOp(); + return recordOp && recordOp.getInitializer() && recordOp.getInitializer().getDefiningOp(); + } + + void OwnershipSignaturePass::findYieldedFields(mlir::ModuleOp module) + { + // (state type, field) -> its reads and its stores + struct FieldUses + { + llvm::SmallVector reads; + llvm::SmallVector stores; + bool other = false; + }; + + llvm::MapVector, FieldUses> fields; + module.walk([&](mlir_ts::PropertyRefOp propertyRefOp) { + auto tupleType = storageTupleOf(propertyRefOp->getOperand(0).getType()); + auto position = static_cast(propertyRefOp.getPosition()); + if (!tupleType || fieldIndex(tupleType, ".step") < 0 || position == fieldIndex(tupleType, CAPTURED_NAME) || + !mlir::isa(propertyRefOp.getType()) || + !ownsHeap(propertyRefOp.getLoc(), mlir::cast(propertyRefOp.getType()).getElementType())) + { + return; + } + + auto &uses = fields[{tupleType, position}]; + for (auto *user : propertyRefOp->getUsers()) + { + if (auto loadOp = mlir::dyn_cast(user)) + { + uses.reads.push_back(loadOp); + } + else if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getReference() == propertyRefOp.getResult()) + { + uses.stores.push_back(storeOp); + } + else if (!mlir::isa(user)) + { + uses.other = true; + } + } + }); + + // a field the signature pass already found borrowing is the caller's, not the generator's + llvm::DenseSet> borrowing; + if (auto entries = module->getAttrOfType(OWN_BORROWING_FIELDS_ATTR_NAME)) + { + for (auto entry : entries.getAsRange()) + { + for (auto index : mlir::cast(entry[1]).asArrayRef()) + { + borrowing.insert({mlir::cast(entry[0]).getValue(), index}); + } + } + } + + llvm::MapVector> yielded; + for (auto &[key, uses] : fields) + { + if (uses.other || uses.reads.empty() || uses.stores.empty() || borrowing.contains(key)) + { + continue; + } + + // read only to be put into a yield's record + auto onlyYielded = llvm::all_of(uses.reads, [](mlir_ts::LoadOp loadOp) { + auto users = llvm::make_filter_range(loadOp->getUsers(), [](mlir::Operation *user) { + return !mlir::isa(user); + }); + return llvm::hasSingleElement(users) && isRecordFieldStore(*users.begin(), loadOp.getResult()); + }); + + // each value stored is yielded later in its block: nothing between stops the generator + auto eachYielded = llvm::all_of(uses.stores, [&](mlir_ts::StoreOp storeOp) { + return llvm::any_of(uses.reads, [&](mlir_ts::LoadOp loadOp) { + return loadOp->getBlock() == storeOp->getBlock() && storeOp->isBeforeInBlock(loadOp); + }); + }); + + if (!onlyYielded || !eachYielded) + { + continue; + } + + yielded[key.first].push_back(key.second); + for (auto loadOp : uses.reads) + { + loadOp->setAttr(OWN_FRESH_RESULT_ATTR_NAME, mlir::UnitAttr::get(&getContext())); + } + } + + if (yielded.empty()) + { + return; + } + + auto *context = &getContext(); + llvm::SmallVector entries; + for (auto &[type, indices] : yielded) + { + entries.push_back(mlir::ArrayAttr::get(context, {mlir::TypeAttr::get(type), mlir::DenseI32ArrayAttr::get(context, indices)})); + } + + module->setAttr(OWN_YIELDED_FIELDS_ATTR_NAME, mlir::ArrayAttr::get(context, entries)); + } + + void OwnershipSignaturePass::findBoundedForwarders() + { + llvm::DenseMap calleesOf; + for (auto &call : calls) + { + calleesOf[call.op] = &call.callees; + } + + for (auto &entry : functions) + { + auto funcOp = entry.second; + if (funcOp.isDeclaration() || !funcOp->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME) || borrowsFound.count(funcOp)) + { + continue; + } + + llvm::SmallVector found; + auto forwards = true; + for (auto value : returnedValues(funcOp)) + { + if (holdsNoBlock(value)) + { + continue; + } + + auto *call = rootOf(value).getDefiningOp(); + auto known = call ? calleesOf.find(call) : calleesOf.end(); + if (known == calleesOf.end()) + { + forwards = false; + break; + } + + llvm::SmallVector bounded = declaredByCallees(*known->second, OWN_DECLARED_BORROWS_ATTR_NAME); + if (bounded.empty() && known->second->funcs.size() == 1 && !known->second->viaCallback) + { + auto made = declared(known->second->funcs.front(), OWN_RESULT_BOUNDED_ATTR_NAME); + bounded.assign(made.begin(), made.end()); + } + + if (bounded.empty()) + { + forwards = false; + break; + } + + auto args = callArgs(call); + for (auto index : bounded) + { + auto param = static_cast(index) < args.size() ? parameterOf(args[index], funcOp) : -1; + if (param < 0) + { + forwards = false; + break; + } + + if (!llvm::is_contained(found, param)) + { + found.push_back(param); + } + } + } + + if (forwards && !found.empty()) + { + llvm::sort(found); + borrowsFound[funcOp] = found; + } + } + } + + int32_t OwnershipSignaturePass::fieldIndex(mlir_ts::TupleType tupleType, llvm::StringRef name) + { + for (auto [index, field] : llvm::enumerate(tupleType.getFields())) + { + if (auto id = mlir::dyn_cast_or_null(field.id); id && id.getValue() == name) + { + return static_cast(index); + } + } + + return -1; + } + + mlir::Value OwnershipSignaturePass::boxStoredIntoState(mlir_ts::NewOp newOp, int32_t captured) + { + mlir_ts::StoreOp fill; + for (auto *user : newOp->getUsers()) + { + if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getReference() == newOp.getResult()) + { + fill = storeOp; + } + } + + auto loadOp = fill ? fill.getValue().getDefiningOp() : mlir_ts::LoadOp(); + auto stateOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + if (!stateOp) + { + return {}; + } + + mlir::Value box; + for (auto *user : stateOp->getUsers()) + { + auto propertyRefOp = mlir::dyn_cast(user); + if (!propertyRefOp || propertyRefOp.getPosition() != captured) + { + continue; + } + + for (auto *fieldUser : propertyRefOp->getUsers()) + { + auto storeOp = mlir::dyn_cast(fieldUser); + if (!storeOp || box) + { + return {}; + } + + box = storeOp.getValue(); + } + } + + auto boxOp = box ? box.getDefiningOp() : mlir_ts::VariableOp(); + return boxOp && boxOp->hasAttr(CAPTURE_BOX_ATTR_NAME) ? box : mlir::Value(); + } + + bool OwnershipSignaturePass::calledOnlyDirectly(mlir::ModuleOp module, mlir_ts::FuncOp funcOp, bool publicToo) + { + if (!funcOp.isPrivate() && !publicToo) + { + return false; + } + + auto uses = mlir::SymbolTable::getSymbolUses(funcOp.getSymNameAttr(), &module.getBodyRegion()); + if (!uses) + { + return false; + } + + for (auto &use : *uses) + { + auto *user = use.getUser(); + if (mlir::isa(user)) + { + continue; + } + + // a method whose signature gives its callers the bound: called through its class's + // vtable or a reference to it, which the facts reach too (declaredByCallees) + if (publicToo && + (isClassVTableEntry(user) || + (mlir::isa(user) && + onlyCalled(user->getResult(0))))) + { + continue; + } + + auto symbolRefOp = mlir::dyn_cast(user); + if (!symbolRefOp) + { + return false; + } + + for (auto *refUser : symbolRefOp->getUsers()) + { + auto boundOp = mlir::dyn_cast(refUser); + if (!boundOp || !llvm::all_of(boundOp->getUsers(), [](mlir::Operation *boundUser) { + return mlir::isa(boundUser); + })) + { + return false; + } + } + } + + return true; + } + + bool OwnershipSignaturePass::isParameterCell(mlir_ts::VariableOp cellOp, mlir_ts::FuncOp maker, int32_t &index) + { + auto argument = cellOp.getInitializer() ? mlir::dyn_cast(cellOp.getInitializer()) + : mlir::BlockArgument(); + if (!cellOp.getCaptured().value_or(false) || isOwningVariable(cellOp) || !argument || + !argument.getOwner()->isEntryBlock() || argument.getOwner()->getParentOp() != maker.getOperation()) + { + return false; + } + + index = static_cast(argument.getArgNumber()); + return true; + } + + bool OwnershipSignaturePass::readsParameterSlot(mlir::Value value, mlir_ts::FuncOp maker, int32_t &index) + { + auto loadOp = rootOf(value).getDefiningOp(); + auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + int position = -1; + if (!varOp || !isParameterSlot(varOp, position) || varOp->getParentOfType() != maker) + { + return false; + } + + index = static_cast(position); + return true; + } + + bool OwnershipSignaturePass::readsParameterField(mlir::Value value, mlir_ts::FuncOp maker, int32_t &index) + { + auto loadOp = rootOf(value).getDefiningOp(); + auto propertyRefOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); + auto argument = propertyRefOp ? mlir::dyn_cast(propertyRefOp->getOperand(0)) : mlir::BlockArgument(); + if (!argument || !argument.getOwner()->isEntryBlock() || argument.getOwner()->getParentOp() != maker.getOperation()) + { + return false; + } + + index = static_cast(argument.getArgNumber()); + return true; + } + + void OwnershipSignaturePass::findBorrowingStates(mlir::ModuleOp module) + { + llvm::MapVector states; + llvm::DenseMap made; + module.walk([&](mlir_ts::NewOp newOp) { + auto valueRefType = mlir::dyn_cast(newOp.getType()); + auto tupleType = valueRefType ? mlir::dyn_cast(valueRefType.getElementType()) + : mlir_ts::TupleType(); + if (!tupleType) + { + return; + } + + ++made[tupleType]; + auto captured = fieldIndex(tupleType, CAPTURED_NAME); + auto maker = newOp->getParentOfType(); + // an open maker has callers this pass cannot give the bound to, unless its signature + // gives it to them (`@borrows`) + if (captured < 0 || !maker || + !calledOnlyDirectly(module, maker, /*publicToo=*/maker->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME))) + { + return; + } + + auto box = boxStoredIntoState(newOp, captured); + if (!box) + { + return; + } + + BorrowingState state{maker, captured}; + for (auto *user : box.getUsers()) + { + auto propertyRefOp = mlir::dyn_cast(user); + if (!propertyRefOp) + { + continue; + } + + for (auto *fieldUser : propertyRefOp->getUsers()) + { + auto storeOp = mlir::dyn_cast(fieldUser); + if (!storeOp) + { + return; + } + + auto value = storeOp.getValue(); + int32_t index = -1; + if (auto cellOp = value.getDefiningOp(); cellOp && isParameterCell(cellOp, maker, index)) + { + auto cellType = mlir::cast(cellOp.getType()); + // a handle parameter's cell holds a count of its own (spec 23.2), which only + // the box's release gives back: a box holding one owns what it holds + if (MLIRTypeHelper::isSharedHandleType(cellType.getElementType())) + { + return; + } + + if (ownsHeap(cellOp.getLoc(), cellType.getElementType())) + { + state.bounded.push_back(index); + } + + continue; + } + + // a copy of what a parameter holds in a field: `.map`'s maker is a closure body + // that copies its own box's fields into the generator's + if (!ownsHeap(value.getLoc(), value.getType())) + { + continue; + } + + // `this`, which the box holds by value (`.this`), not in a cell: read out of + // the parameter's slot + if (readsParameterSlot(value, maker, index)) + { + if (MLIRTypeHelper::isSharedHandleType(value.getType())) + { + return; + } + + if (!llvm::is_contained(state.bounded, index)) + { + state.bounded.push_back(index); + } + + continue; + } + + if (!readsParameterField(value, maker, index)) + { + return; // a box with anything but what the parameters hold owns what it holds + } + + if (!llvm::is_contained(state.bounded, index)) + { + state.bounded.push_back(index); + } + } + } + + if (!state.bounded.empty()) + { + states[tupleType] = state; + } + }); + + // made in one place only: the maker is the one function a bound can come from + states.remove_if([&](auto &entry) { return made[entry.first] != 1; }); + for (auto &[type, state] : states) + { + auto bounded = state.bounded; + llvm::sort(bounded); + borrowsFound[state.maker] = bounded; + } + if (states.empty()) + { + return; + } + + // Each store into a field of a state type, by field: what the field may be given. + llvm::DenseMap, llvm::SmallVector> stored; + module.walk([&](mlir_ts::StoreOp storeOp) { + auto propertyRefOp = storeOp.getReference().getDefiningOp(); + auto tupleType = propertyRefOp ? storageTupleOf(propertyRefOp->getOperand(0).getType()) : mlir_ts::TupleType(); + if (tupleType && states.count(tupleType)) + { + stored[{tupleType, propertyRefOp.getPosition()}].push_back(storeOp.getValue()); + } + }); + + for (auto &[type, state] : states) + { + auto tupleType = mlir::cast(type); + // least fixpoint: a field borrows once everything stored into it is a read of a borrowing + // cell or field + for (auto changed = true; changed;) + { + changed = false; + for (auto [index, field] : llvm::enumerate(tupleType.getFields())) + { + auto position = static_cast(index); + auto found = stored.find({type, position}); + if (position == state.captured || state.fields.contains(position) || found == stored.end() || + !ownsHeap(state.maker.getLoc(), field.type)) + { + continue; + } + + if (llvm::all_of(found->second, [&](mlir::Value value) { return readsBorrowed(value, tupleType, state); })) + { + state.fields.insert(position); + changed = true; + } + } + } + } + + auto *context = &getContext(); + llvm::SmallVector entries; + for (auto &[type, state] : states) + { + llvm::SmallVector fields{state.captured}; + fields.append(state.fields.begin(), state.fields.end()); + entries.push_back(mlir::ArrayAttr::get( + context, {mlir::TypeAttr::get(type), mlir::DenseI32ArrayAttr::get(context, fields)})); + state.maker->setAttr(OWN_RESULT_BOUNDED_ATTR_NAME, mlir::DenseI32ArrayAttr::get(context, state.bounded)); + } + + module->setAttr(OWN_BORROWING_FIELDS_ATTR_NAME, mlir::ArrayAttr::get(context, entries)); + } + + bool OwnershipSignaturePass::readsBorrowed(mlir::Value value, mlir_ts::TupleType tupleType, const BorrowingState &state) + { + auto loadOp = rootOf(value).getDefiningOp(); + if (!loadOp) + { + return false; + } + + auto isStateField = [&](mlir::Value ref, int32_t &position) { + auto propertyRefOp = ref.getDefiningOp(); + if (!propertyRefOp || storageTupleOf(propertyRefOp->getOperand(0).getType()) != tupleType) + { + return false; + } + + position = propertyRefOp.getPosition(); + return true; + }; + + int32_t position = -1; + if (isStateField(loadOp.getReference(), position)) + { + return state.fields.contains(position); + } + + // a field of the box the state holds, copied in by value, or a cell read out of it + auto isBoxField = [&](mlir::Value ref) { + auto fieldRef = ref.getDefiningOp(); + auto boxLoad = fieldRef ? fieldRef->getOperand(0).getDefiningOp() : mlir_ts::LoadOp(); + return boxLoad && isStateField(boxLoad.getReference(), position) && position == state.captured; + }; + + if (isBoxField(loadOp.getReference())) + { + return true; + } + + auto cellLoad = loadOp.getReference().getDefiningOp(); + return cellLoad && isBoxField(cellLoad.getReference()); + } + +} // namespace own_signature diff --git a/tslang/lib/TypeScript/OwnershipSignatureGlobals.cpp b/tslang/lib/TypeScript/OwnershipSignatureGlobals.cpp new file mode 100644 index 000000000..b679c02d4 --- /dev/null +++ b/tslang/lib/TypeScript/OwnershipSignatureGlobals.cpp @@ -0,0 +1,599 @@ +// OwnershipSignaturePass (see OwnershipSignaturePass.h): globals - what an initializer moves in, +// immortal globals (spec 25), globals given a borrow, calls in a global's initializer, the globals a +// call may assign. + +#include "OwnershipSignaturePass.h" + +#define DEBUG_TYPE "own" + +namespace own_signature +{ + + bool OwnershipSignaturePass::movesIntoResult(mlir::Value value) + { + return llvm::all_of(value.getUses(), [](mlir::OpOperand &use) { + auto *user = use.getOwner(); + if (mlir::isa(user)) + { + return true; + } + + // given to `new Shared(x)`: it moves into the handle's block, which owns it from then on + // whatever becomes of the handle - rc counts the handle (spec 23.2, 26) + if (auto sharedNewOp = mlir::dyn_cast(user)) + { + return sharedNewOp.getValue() == use.get(); + } + + if ((isView(user) && use.getOperandNumber() == 0) || mlir::isa(user)) + { + return movesIntoResult(user->getResult(0)); + } + + // copied into a block `ts.New` made, which itself moves in: an object literal's box + if (auto storeOp = mlir::dyn_cast(user); storeOp && use.getOperandNumber() == 0) + { + auto block = storeOp.getReference(); + return block.getDefiningOp() && + llvm::all_of(block.getUses(), [&](mlir::OpOperand &blockUse) { + return blockUse.getOwner() == storeOp.getOperation() || + (isView(blockUse.getOwner()) && movesIntoResult(blockUse.getOwner()->getResult(0))); + }); + } + + return false; + }); + } + + bool OwnershipSignaturePass::holdsOnlyConstants(mlir::Value value) + { + auto loadOp = value.getDefiningOp(); + auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + if (!varOp || !varOp.getInitializer() || !varOp.getInitializer().getDefiningOp()) + { + return false; + } + + return llvm::all_of(varOp->getUsers(), [](mlir::Operation *user) { + if (mlir::isa(user)) + { + return true; + } + + auto propertyRefOp = mlir::dyn_cast(user); + return propertyRefOp && llvm::all_of(propertyRefOp->getUsers(), [&](mlir::Operation *fieldUser) { + auto storeOp = mlir::dyn_cast(fieldUser); + return mlir::isa(fieldUser) || + (storeOp && storeOp.getReference() == propertyRefOp.getResult() && + holdsNoBlock(storeOp.getValue())); + }); + }); + } + + bool OwnershipSignaturePass::mayRunAgain(mlir::Operation *op) + { + for (auto *at = op; at && !mlir::isa(at); at = at->getParentOp()) + { + if (mlir::isa(at)) + { + return true; + } + + auto *block = at->getBlock(); + if (!block) + { + return true; // not in a function: nothing is known of how often it runs + } + + llvm::SmallPtrSet seen; + llvm::SmallVector work(block->getSuccessors().begin(), block->getSuccessors().end()); + while (!work.empty()) + { + auto *next = work.pop_back_val(); + if (next == block) + { + return true; + } + + if (seen.insert(next).second) + { + work.append(next->getSuccessors().begin(), next->getSuccessors().end()); + } + } + } + + return false; + } + + void OwnershipSignaturePass::findImmortalGlobals(mlir::ModuleOp module) + { + // the static constructors, which run once before the program + llvm::StringSet<> constructors; + module.walk([&](mlir_ts::GlobalConstructorOp constructorOp) { constructors.insert(constructorOp.getGlobalName()); }); + + // a static's address taken out of a class's vtable (`k.w = x`, `k: typeof K`) and written + // through: it may be any class's static the vtable slot holds, so no static is immortal. + // MLIRGen makes a `ts.ThisVirtualSymbolRef` only for a method (a bound function, never a + // `!ts.ref`), so no program reaches this through one today; it is listed for an IR that would. + auto vtableWritten = false; + module.walk([&](mlir::Operation *op) { + if (mlir::isa(op) && + mlir::isa(op->getResult(0).getType()) && + !llvm::all_of(op->getUsers(), [](mlir::Operation *user) { return mlir::isa(user); })) + { + vtableWritten = true; + } + }); + + auto uses = mlir::SymbolTable::getSymbolUses(&module.getBodyRegion()); + if (!uses) + { + return; // an op this pass cannot read symbols through: nothing is known never written + } + + llvm::StringMap> usesOf; + for (auto &use : *uses) + { + usesOf[use.getSymbolRef().getRootReference().getValue()].push_back(use.getUser()); + } + + // each candidate with the value its initializer gives it, and the store that does, if any + llvm::MapVector> candidates; + auto mth = typeHelper(&getContext()); + module.walk([&](mlir_ts::GlobalOp globalOp) { + if (!mlir::isa(globalOp.getType()) || !mth.ownsHeapMemory(globalOp.getLoc(), globalOp.getType()) || + globalOp->hasAttr("export") || globalOp->hasAttr("import") || globalOp->hasAttr(DLL_EXPORT) || + globalOp->hasAttr(DLL_IMPORT) || !globalOp.getInitializerRegion().hasOneBlock()) + { + return; + } + + auto resultOp = mlir::dyn_cast(globalOp.getInitializerBlock()->getTerminator()); + if (!resultOp || resultOp->getNumOperands() != 1) + { + return; + } + + mlir_ts::StoreOp initStore; + for (auto *user : usesOf.lookup(globalOp.getSymName())) + { + // its vtable's entry, which only a `ts.VirtualSymbolRef` reads (vtableWritten) + if (mlir::isa(user) && user->getParentOfType()) + { + if (vtableWritten) + { + return; + } + + continue; + } + + if (mlir::isa(user)) + { + continue; // only loaded, or vtableWritten + } + + auto addressOfOp = mlir::dyn_cast(user); + if (!addressOfOp) + { + return; + } + + auto released = false; + auto stored = false; + for (auto *addressUser : addressOfOp->getUsers()) + { + if (mlir::isa(addressUser)) + { + continue; + } + + // the old value, the default, given up before the store + if (mlir::isa(addressUser)) + { + released = true; + continue; + } + + // the one store, in a static constructor, run once + auto storeOp = mlir::dyn_cast(addressUser); + auto funcOp = storeOp ? storeOp->getParentOfType() : mlir_ts::FuncOp(); + if (initStore || !storeOp || storeOp.getReference() != addressOfOp.getResult() || + storeOp.getValue() == addressOfOp.getResult() || !funcOp || !constructors.contains(funcOp.getSymName()) || + mayRunAgain(storeOp)) + { + return; + } + + initStore = storeOp; + stored = true; + } + + if (released && !stored) + { + return; + } + } + + // a static's region yields only its default, which the store replaces + if (initStore && !regionYieldsDefault(globalOp)) + { + return; + } + + candidates[globalOp] = {initStore ? initStore.getValue() : resultOp->getOperand(0), initStore}; + }); + + // given a fresh object, nothing, or another immortal global's value: in module order, until + // no more are found + llvm::StringSet<> immortal; + auto madeHere = [&](mlir::Value value) { + auto root = rootOf(value); + if (!root.getDefiningOp() || isCall(root.getDefiningOp()) || !isFresh(root)) + { + return false; + } + + // and not given back before the global takes it + auto released = false; + forEachUse(root, [&](mlir::Operation *user, mlir::Value) { released = released || mlir::isa(user); }); + return !released; + }; + // isImmortalGlobalRead, of the globals found so far: none is marked yet + auto readsImmortal = [&](mlir::Value value) { + auto addressOfOp = globalReadOf(value); + return addressOfOp && immortal.contains(addressOfOp.getGlobalName()); + }; + + for (auto found = true; found;) + { + found = false; + for (auto &[globalOp, init] : candidates) + { + auto value = init.first; + if (!immortal.contains(globalOp.getSymName()) && (madeHere(value) || holdsNoBlock(value) || readsImmortal(value))) + { + immortal.insert(globalOp.getSymName()); + found = true; + } + } + } + + auto unit = mlir::UnitAttr::get(&getContext()); + for (auto &[globalOp, init] : candidates) + { + if (!immortal.contains(globalOp.getSymName())) + { + continue; + } + + globalOp->setAttr(OWN_IMMORTAL_GLOBAL_ATTR_NAME, unit); + if (init.second) + { + init.second->setAttr(OWN_IMMORTAL_GLOBAL_ATTR_NAME, unit); + } + + for (auto *user : usesOf.lookup(globalOp.getSymName())) + { + if (mlir::isa(user)) + { + user->setAttr(OWN_IMMORTAL_GLOBAL_ATTR_NAME, unit); + } + } + } + } + + void OwnershipSignaturePass::moveIntoGlobals(mlir::ModuleOp module) + { + module.walk([&](mlir_ts::GlobalOp globalOp) { + llvm::SmallVector retains; + globalOp.walk([&](mlir_ts::RetainOp retainOp) { retains.push_back(retainOp); }); + for (auto retainOp : retains) + { + // a handle's retain is rc's: the global's count, which own leaves in place (spec 23.2) + if (touchesHandle(retainOp)) + { + continue; + } + + auto value = retainOp.getReference(); + + // an immortal global's value (spec 25): owned by no one, held by any number + if (isImmortalGlobalRead(value)) + { + retainOp.erase(); + continue; + } + + // nothing in the region gives the block back, or takes a second reference to it: + // the global is its one owner + auto released = false; + auto retained = 0; + forEachUse(rootOf(value), [&](mlir::Operation *user, mlir::Value) { + released = released || mlir::isa(user); + retained += mlir::isa(user); + }); + + if (retained == 1 && !released && (isFresh(value) || holdsOnlyConstants(value) || isConstantData(value)) && movesIntoResult(value)) + { + retainOp.erase(); + continue; + } + + retainOp.emitError("the initializer of global '") + << globalOp.getSymName() << "' takes a second reference; -mm=own cannot prove a move or a borrow here yet"; + signalPassFailure(); + } + }); + } + + void OwnershipSignaturePass::rejectBorrowedGlobalInitializers(mlir::ModuleOp module) + { + module.walk([&](mlir_ts::GlobalOp globalOp) { + auto *block = globalOp.getInitializerBlock(); + auto resultOp = block && !block->empty() ? mlir::dyn_cast(block->getTerminator()) + : mlir_ts::GlobalResultOp(); + if (!resultOp || resultOp->getNumOperands() != 1) + { + return; + } + + auto value = resultOp->getOperand(0); + while (value.getDefiningOp() && isView(value.getDefiningOp())) + { + value = value.getDefiningOp()->getOperand(0); + } + + auto loadOp = value.getDefiningOp(); + auto *place = loadOp ? loadOp.getReference().getDefiningOp() : nullptr; + // an immortal global's value (spec 25) is owned by no one: any number of globals hold it + if (!place || !mlir::isa(place) || + isHandle(value) || isConstantData(value) || isImmortalGlobalRead(value) || + !ownsHeap(value.getLoc(), value.getType())) + { + return; + } + + resultOp.emitError("the initializer of global '") + << globalOp.getSymName() + << "' reads a value another owner keeps; -mm=own cannot make a global a second owner"; + signalPassFailure(); + }); + } + + void OwnershipSignaturePass::pinAssignedGlobals() + { + llvm::DenseMap> callsIn; + for (auto &call : calls) + { + callsIn[call.op->getParentOfType()].push_back(&call); + } + + llvm::DenseMap> assigns; + llvm::DenseSet assignsAny; + for (auto &entry : functions) + { + auto funcOp = entry.second; + funcOp.walk([&](mlir::Operation *op) { + mlir::Value slot; + if (auto storeOp = mlir::dyn_cast(op)) + { + slot = storeOp.getReference(); + } + else if (auto releaseSlotOp = mlir::dyn_cast(op)) + { + slot = releaseSlotOp.getSlot(); + } + + if (auto addressOfOp = slot ? slot.getDefiningOp() : mlir_ts::AddressOfOp()) + { + assigns[funcOp].insert(addressOfOp.getGlobalName()); + } + }); + } + + // what a call may assign, from what its callees do so far + auto assignedBy = [&](Call &call, llvm::SetVector &globals) { + if (!callMayDrop(call.callees)) + { + return false; + } + + if (!call.callees.known) + { + return true; + } + + auto any = false; + for (auto callee : call.callees.funcs) + { + any |= assignsAny.contains(callee); + if (auto found = assigns.find(callee); found != assigns.end()) + { + globals.insert(found->second.begin(), found->second.end()); + } + } + + return any; + }; + + for (auto changed = true; changed;) + { + changed = false; + for (auto &entry : functions) + { + auto funcOp = entry.second; + for (auto *call : callsIn[funcOp]) + { + llvm::SetVector globals; + if (assignedBy(*call, globals)) + { + changed |= assignsAny.insert(funcOp).second; + } + + for (auto global : globals) + { + changed |= assigns[funcOp].insert(global); + } + } + } + } + + for (auto &call : calls) + { + llvm::SetVector globals; + if (assignedBy(call, globals)) + { + call.op->setAttr(OWN_ASSIGNS_ANY_GLOBAL_ATTR_NAME, mlir::UnitAttr::get(&getContext())); + continue; + } + + if (globals.empty()) + { + continue; + } + + llvm::SmallVector names; + for (auto global : globals) + { + names.push_back(mlir::StringAttr::get(&getContext(), global)); + } + + call.op->setAttr(OWN_ASSIGNS_GLOBALS_ATTR_NAME, mlir::ArrayAttr::get(&getContext(), names)); + } + } + + void OwnershipSignaturePass::rejectGlobalArgumentsDropped() + { + for (auto &call : calls) + { + if (!callMayDrop(call.callees)) + { + continue; + } + + for (auto argument : callArgs(call.op)) + { + // a method's object: `this` of the method bound to it + if (auto getThisOp = argument.getDefiningOp()) + { + auto *boundOp = getThisOp->getOperand(0).getDefiningOp(); + if (mlir::isa_and_nonnull(boundOp)) + { + argument = boundOp->getOperand(0); + } + } + + // an immortal global (spec 25) is written by its initializer only, and that runs once + // (a static's is a store in its static constructor, outside any loop, which + // writtenGlobals counts), and no release frees its block + if (isHandle(argument) || isImmortalGlobalRead(argument) || !ownsHeap(call.op->getLoc(), argument.getType())) + { + continue; + } + + auto loadOp = rootOf(argument).getDefiningOp(); + auto global = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::AddressOfOp(); + if (!global || !writtenGlobals.contains(global.getGlobalName())) + { + continue; + } + + call.op->emitError("a value read out of the global '") + << global.getGlobalName() + << "' is lent to a call that may release or overwrite it; -mm=own cannot prove it outlives the call"; + signalPassFailure(); + } + } + } + + void OwnershipSignaturePass::ownGlobalInitializerCalls(mlir::ModuleOp module) + { + module.walk([&](mlir_ts::GlobalOp globalOp) { + // collected first: a move erases a release the walk has not reached yet + llvm::SmallVector calls; + globalOp.walk([&](mlir::Operation *op) { + if (isCall(op)) + { + calls.push_back(op); + } + }); + + for (auto *op : calls) + { + auto callees = resolve(op); + auto facts = callFacts(callees); + auto calleeName = callees.funcs.empty() ? llvm::StringRef("the callee") : callees.funcs.front().getSymName(); + if (!facts.borrows.empty()) + { + op->emitError("the initializer of global '") + << globalOp.getSymName() << "' holds the result of '" << calleeName + << "', which borrows its argument; a global cannot hold a borrow under -mm=own"; + signalPassFailure(); + continue; + } + + auto args = callArgs(op); + llvm::SmallPtrSet given; + for (auto index : facts.owned) + { + if (index < 0 || index >= static_cast(args.size()) || + !given.insert(rootOf(args[index])).second || !moveIntoCallee(op, args[index])) + { + op->emitError("the initializer of global '") + << globalOp.getSymName() << "' gives '" << calleeName + << "' a value it keeps, but -mm=own cannot move it there"; + signalPassFailure(); + break; + } + } + } + }); + } + + bool OwnershipSignaturePass::moveIntoCallee(mlir::Operation *call, mlir::Value value) + { + auto root = rootOf(value); + if (isConstantData(root)) + { + return true; + } + + if (!isFresh(root) || root.getDefiningOp()->getBlock() != call->getBlock()) + { + return false; + } + + mlir::Operation *release = nullptr; + auto usedAfter = false; + forEachUse(root, [&](mlir::Operation *user, mlir::Value) { + if (user == call) + { + return; + } + + if (user->getBlock() != call->getBlock()) + { + usedAfter = true; + } + else if (call->isBeforeInBlock(user)) + { + usedAfter = usedAfter || release || !mlir::isa(user); + release = mlir::isa(user) ? user : release; + } + }); + + if (usedAfter) + { + return false; + } + + if (release) + { + release->erase(); + } + + return true; + } + +} // namespace own_signature diff --git a/tslang/lib/TypeScript/OwnershipSignatureInterfaces.cpp b/tslang/lib/TypeScript/OwnershipSignatureInterfaces.cpp new file mode 100644 index 000000000..58e6b18ec --- /dev/null +++ b/tslang/lib/TypeScript/OwnershipSignatureInterfaces.cpp @@ -0,0 +1,1241 @@ +// OwnershipSignaturePass (see OwnershipSignaturePass.h): interfaces closed in this module (G9, +// spec 31) - their method and field families, and the casts that open them. + +#include "OwnershipSignaturePass.h" + +#define DEBUG_TYPE "own" + +namespace own_signature +{ + + std::pair OwnershipSignaturePass::slotField(mlir::Value value) + { + while (auto castOp = value ? value.getDefiningOp() : mlir_ts::CastOp()) + { + value = castOp.getIn(); + } + + auto propertyRefOp = value ? value.getDefiningOp() : mlir_ts::PropertyRefOp(); + auto nullObject = propertyRefOp ? propertyRefOp.getObjectRef().getDefiningOp() : mlir_ts::CastOp(); + if (!nullObject || !nullObject.getIn().getDefiningOp() || + !mlir::isa(nullObject.getType())) + { + return {}; + } + + auto tupleType = storageTupleOf(nullObject.getType()); + if (!tupleType) + { + return {}; + } + + return {tupleType, static_cast(propertyRefOp.getPosition())}; + } + + void OwnershipSignaturePass::collectFieldFunctions(mlir::ModuleOp module) + { + llvm::DenseMap, llvm::SetVector> stored; + llvm::DenseSet> unknownFields; + llvm::DenseSet unknownTypes; + // a function of unknown origin stored through an interface this pass cannot follow + auto unknownAnywhere = false; + + auto store = [&](std::pair key, mlir::Value value) { + auto symbol = slotFunction(value); + auto funcOp = symbol ? functions.lookup(symbol.getValue()) : mlir_ts::FuncOp(); + if (funcOp && !funcOp.isDeclaration()) + { + stored[key].insert(symbol); + } + else + { + unknownFields.insert(key); + } + }; + + auto isFunctionType = [](mlir::Type type) { + return mlir::isa(type); + }; + + module.walk([&](mlir::Operation *op) { + // the literal's own values + if (auto constantOp = mlir::dyn_cast(op)) + { + auto constTupleType = mlir::dyn_cast(constantOp.getType()); + auto values = mlir::dyn_cast(constantOp.getValue()); + if (!constTupleType || !values) + { + return; + } + + mlir::Type tupleType = mlir_ts::TupleType::get(&getContext(), constTupleType.getFields()); + for (auto [index, value] : llvm::enumerate(values)) + { + std::pair key{tupleType, static_cast(index)}; + if (auto symbol = mlir::dyn_cast(value)) + { + auto funcOp = functions.lookup(symbol.getValue()); + if (funcOp && !funcOp.isDeclaration()) + { + stored[key].insert(symbol.getAttr()); + } + else + { + unknownFields.insert(key); + } + } + else if (!mlir::isa(value) && index < constTupleType.getFields().size() && + isFunctionType(constTupleType.getFields()[index].type)) + { + unknownFields.insert(key); + } + } + + return; + } + + // a store into the field through the object + if (auto propertyRefOp = mlir::dyn_cast(op)) + { + auto tupleType = storageTupleOf(propertyRefOp.getObjectRef().getType()); + if (!tupleType) + { + return; + } + + std::pair key{tupleType, static_cast(propertyRefOp.getPosition())}; + for (auto &use : propertyRefOp->getUses()) + { + auto *user = use.getOwner(); + if (mlir::isa(user)) + { + continue; + } + + auto storeOp = mlir::dyn_cast(user); + if (storeOp && storeOp.getReference() == propertyRefOp.getResult() && storeOp.getValue() != propertyRefOp.getResult()) + { + store(key, storeOp.getValue()); + continue; + } + + // an interface vtable's offset of the field (slotField) reads nothing + if (user->getParentOfType() && mlir::isa(user)) + { + continue; + } + + unknownFields.insert(key); + } + + return; + } + + // a store into the field through an interface: into each literal its vtables reach + if (auto interfaceRefOp = mlir::dyn_cast(op)) + { + auto refType = mlir::dyn_cast(interfaceRefOp.getType()); + if (!refType || !isFunctionType(refType.getElementType())) + { + return; + } + + auto interfaceType = mlir::dyn_cast(interfaceRefOp.getInterfaceVal().getType()); + auto found = interfaceType ? interfaceFieldFamilies.find({interfaceType.getName().getValue().str(), + static_cast(interfaceRefOp.getIndex()) + + INTERFACE_VTABLE_HEADER_SLOTS}) + : interfaceFieldFamilies.end(); + for (auto &use : interfaceRefOp->getUses()) + { + auto *user = use.getOwner(); + if (mlir::isa(user)) + { + continue; + } + + auto storeOp = mlir::dyn_cast(user); + if (storeOp && storeOp.getReference() == interfaceRefOp.getResult() && found != interfaceFieldFamilies.end() && + found->second.closed) + { + for (auto target : found->second.targets) + { + store(target, storeOp.getValue()); + } + + continue; + } + + unknownAnywhere = true; + } + + return; + } + + // an object of the literal's type made other than by the literal: its fields may hold + // anything. A cast keeps the type's own value (the literal's box seen as the object, an + // object, a reference to its storage) or makes the null object a vtable's offsets are + // taken from; from anything else it reinterprets. + if (auto castOp = mlir::dyn_cast(op)) + { + auto tupleType = storageTupleOf(castOp.getType()); + if (tupleType && !mlir::isa(castOp.getIn().getType()) && + storageTupleOf(castOp.getIn().getType()) != tupleType) + { + unknownTypes.insert(tupleType); + } + + return; + } + + if (mlir::isa(op) || isCall(op) || isView(op) || mlir::isa(op)) + { + return; + } + + for (auto result : op->getResults()) + { + if (mlir::isa(result.getType())) + { + if (auto tupleType = storageTupleOf(result.getType())) + { + unknownTypes.insert(tupleType); + } + } + } + }); + + for (auto &[key, family] : interfaceFieldFamilies) + { + if (!family.closed) + { + continue; + } + + for (auto target : family.targets) + { + if (unknownAnywhere || unknownTypes.contains(target.first) || unknownFields.contains(target)) + { + family.closed = false; + break; + } + + for (auto member : stored.lookup(target)) + { + if (!llvm::is_contained(family.members, member)) + { + family.members.push_back(member); + } + } + } + + family.closed = family.closed && !family.members.empty(); + } + } + + void OwnershipSignaturePass::valueInterfaces(mlir::Type type, llvm::StringSet<> &found, int depth) + { + if (!type || depth > 16) + { + return; + } + + if (auto interfaceType = mlir::dyn_cast(type)) + { + found.insert(interfaceType.getName().getValue()); + return; + } + + auto fields = [&](llvm::ArrayRef infos) { + for (auto &info : infos) + { + valueInterfaces(info.type, found, depth + 1); + } + }; + + if (auto tupleType = mlir::dyn_cast(type)) + { + fields(tupleType.getFields()); + } + else if (auto constTupleType = mlir::dyn_cast(type)) + { + fields(constTupleType.getFields()); + } + else if (auto unionType = mlir::dyn_cast(type)) + { + for (auto member : unionType.getTypes()) + { + valueInterfaces(member, found, depth + 1); + } + } + else if (auto optionalType = mlir::dyn_cast(type)) + { + valueInterfaces(optionalType.getElementType(), found, depth + 1); + } + else if (auto arrayType = mlir::dyn_cast(type)) + { + valueInterfaces(arrayType.getElementType(), found, depth + 1); + } + else if (auto constArrayType = mlir::dyn_cast(type)) + { + valueInterfaces(constArrayType.getElementType(), found, depth + 1); + } + else if (auto refType = mlir::dyn_cast(type)) + { + valueInterfaces(refType.getElementType(), found, depth + 1); + } + else if (auto valueRefType = mlir::dyn_cast(type)) + { + valueInterfaces(valueRefType.getElementType(), found, depth + 1); + } + else if (auto boundRefType = mlir::dyn_cast(type)) + { + valueInterfaces(boundRefType.getElementType(), found, depth + 1); + } + else if (auto sharedType = mlir::dyn_cast(type)) + { + valueInterfaces(sharedType.getElementType(), found, depth + 1); + } + } + + void OwnershipSignaturePass::anyInterfaces(mlir::Type type, llvm::StringSet<> &found, llvm::DenseSet &seen, + llvm::StringSet<> *classes) + { + if (!type || !seen.insert(type).second) + { + return; + } + + if (auto interfaceType = mlir::dyn_cast(type)) + { + found.insert(interfaceType.getName().getValue()); + return; + } + + auto fields = [&](llvm::ArrayRef infos) { + for (auto &info : infos) + { + anyInterfaces(info.type, found, seen, classes); + } + }; + + // the types whose fields are not parameters of the type + if (auto classStorageType = mlir::dyn_cast(type)) + { + if (classes) + { + classes->insert(classStorageType.getName().getValue()); + } + + fields(classStorageType.getFields()); + return; + } + + if (auto objectStorageType = mlir::dyn_cast(type)) + { + fields(objectStorageType.getFields()); + return; + } + + if (auto tupleType = mlir::dyn_cast(type)) + { + fields(tupleType.getFields()); + return; + } + + if (auto constTupleType = mlir::dyn_cast(type)) + { + fields(constTupleType.getFields()); + return; + } + + type.walkImmediateSubElements([](mlir::Attribute) {}, [&](mlir::Type sub) { anyInterfaces(sub, found, seen, classes); }); + } + + void OwnershipSignaturePass::functionValues(mlir::Type type, llvm::SmallVectorImpl &found, int depth) + { + if (!type || depth > 16) + { + return; + } + + if (mlir::isa(type)) + { + found.push_back(type); + return; + } + + auto fields = [&](llvm::ArrayRef infos) { + for (auto &info : infos) + { + functionValues(info.type, found, depth + 1); + } + }; + + if (auto tupleType = mlir::dyn_cast(type)) + { + fields(tupleType.getFields()); + } + else if (auto constTupleType = mlir::dyn_cast(type)) + { + fields(constTupleType.getFields()); + } + else if (auto unionType = mlir::dyn_cast(type)) + { + for (auto member : unionType.getTypes()) + { + functionValues(member, found, depth + 1); + } + } + else if (auto optionalType = mlir::dyn_cast(type)) + { + functionValues(optionalType.getElementType(), found, depth + 1); + } + else if (auto arrayType = mlir::dyn_cast(type)) + { + functionValues(arrayType.getElementType(), found, depth + 1); + } + else if (auto constArrayType = mlir::dyn_cast(type)) + { + functionValues(constArrayType.getElementType(), found, depth + 1); + } + else if (auto sharedType = mlir::dyn_cast(type)) + { + functionValues(sharedType.getElementType(), found, depth + 1); + } + } + + std::pair, llvm::SmallVector> OwnershipSignaturePass::signatureOf(mlir::Type type) + { + llvm::SmallVector inputs; + llvm::SmallVector results; + if (auto functionType = mlir::dyn_cast(type)) + { + inputs.append(functionType.getInputs().begin(), functionType.getInputs().end()); + results.append(functionType.getResults().begin(), functionType.getResults().end()); + } + else if (auto boundType = mlir::dyn_cast(type)) + { + inputs.append(boundType.getInputs().begin(), boundType.getInputs().end()); + results.append(boundType.getResults().begin(), boundType.getResults().end()); + } + else if (auto hybridType = mlir::dyn_cast(type)) + { + inputs.append(hybridType.getInputs().begin(), hybridType.getInputs().end()); + results.append(hybridType.getResults().begin(), hybridType.getResults().end()); + } + else + { + // a kind this does not read: never the same signature as another + inputs.push_back(type); + } + + return {inputs, results}; + } + + bool OwnershipSignaturePass::castReinterpretsFunction(mlir::Type in, mlir::Type out, llvm::StringSet<> &interfaces, bool &boxing) + { + // `!ts.opaque` is the compiler's own (a vtable slot read as the `.instanceOf` it holds), not + // a value a program can put a function into + auto isOpaque = [](mlir::Type type) { return mlir::isa(type); }; + llvm::SmallVector inFunctions; + llvm::SmallVector outFunctions; + functionValues(in, inFunctions); + functionValues(out, outFunctions); + boxing = false; + if ((inFunctions.empty() && outFunctions.empty()) || in == out) + { + return false; + } + + auto mention = [&](llvm::ArrayRef types) { + for (auto type : types) + { + llvm::DenseSet seen; + anyInterfaces(type, interfaces, seen); + } + }; + + if (isOpaque(out)) + { + boxing = true; + mention(inFunctions); + return false; + } + + // the same signatures, but for `this` seen as an opaque pointer on one side, or bound on one + // side only (a closure's box, a bound function's object: the caller supplies neither) + auto sameSignature = [](mlir::Type a, mlir::Type b) { + auto [aInputs, aResults] = signatureOf(a); + auto [bInputs, bResults] = signatureOf(b); + auto dropBoundThis = [](mlir::Type type, llvm::SmallVector &inputs) { + if (!inputs.empty() && (mlir::isa(type) || mlir::isa(inputs.front()))) + { + inputs.erase(inputs.begin()); + } + }; + + if (aInputs.size() == bInputs.size() + 1) + { + dropBoundThis(a, aInputs); + } + else if (bInputs.size() == aInputs.size() + 1) + { + dropBoundThis(b, bInputs); + } + + if (aResults != bResults || aInputs.size() != bInputs.size()) + { + return false; + } + + for (auto [index, pair] : llvm::enumerate(llvm::zip(aInputs, bInputs))) + { + auto [aInput, bInput] = pair; + if (aInput != bInput && + !(index == 0 && (mlir::isa(aInput) || mlir::isa(bInput)))) + { + return false; + } + } + + return true; + }; + + // a function out of `any`, or one function seen as another; a function seen as something + // that is no function (a vtable slot's reference to it) is the compiler's own + auto reinterprets = isOpaque(in) && !outFunctions.empty(); + if (!inFunctions.empty() && !outFunctions.empty()) + { + reinterprets = reinterprets || inFunctions.size() != outFunctions.size(); + for (auto [inFunction, outFunction] : llvm::zip(inFunctions, outFunctions)) + { + reinterprets = reinterprets || !sameSignature(inFunction, outFunction); + } + } + + if (reinterprets) + { + mention(inFunctions); + mention(outFunctions); + } + + return reinterprets; + } + + void OwnershipSignaturePass::memberTypeInterfaces(mlir::Type type, llvm::StringSet<> &found) + { + llvm::DenseSet seen; + anyInterfaces(type, found, seen); + } + + bool OwnershipSignaturePass::isNullish(mlir::Type type) + { + return mlir::isa(type); + } + + bool OwnershipSignaturePass::castMakesInterface(mlir::Type in, mlir::Type out, int depth) + { + // null seen as an interface value has no vtable: a call through it reaches nothing + if (in == out || depth > 16 || isNullish(in)) + { + return false; + } + + llvm::StringSet<> made; + valueInterfaces(out, made); + if (made.empty()) + { + return false; + } + + if (mlir::isa(out)) + { + return !holdsAsIs(in, out, /*onlyNullable=*/true); + } + + if (mlir::isa(out)) + { + if (isNullish(in) || holdsAsIs(out, in)) + { + return false; + } + + llvm::SmallVector members; + if (auto unionType = mlir::dyn_cast(in)) + { + members.append(unionType.getTypes().begin(), unionType.getTypes().end()); + } + else if (auto optionalType = mlir::dyn_cast(in)) + { + members.push_back(optionalType.getElementType()); + } + else + { + return true; + } + + return !llvm::all_of(members, [&](mlir::Type member) { return isNullish(member) || holdsAsIs(out, member); }); + } + + auto tupleFields = [](mlir::Type type) -> std::optional> { + if (auto tupleType = mlir::dyn_cast(type)) + { + return tupleType.getFields(); + } + + if (auto constTupleType = mlir::dyn_cast(type)) + { + return constTupleType.getFields(); + } + + return std::nullopt; + }; + + if (auto outFields = tupleFields(out)) + { + auto inFields = tupleFields(in); + if (!inFields || inFields->size() != outFields->size()) + { + return true; + } + + for (auto [inField, outField] : llvm::zip(*inFields, *outFields)) + { + if (castMakesInterface(inField.type, outField.type, depth + 1)) + { + return true; + } + } + + return false; + } + + auto elementOf = [](mlir::Type type) -> mlir::Type { + if (auto arrayType = mlir::dyn_cast(type)) + { + return arrayType.getElementType(); + } + + if (auto constArrayType = mlir::dyn_cast(type)) + { + return constArrayType.getElementType(); + } + + return {}; + }; + + if (auto outElement = elementOf(out)) + { + auto inElement = elementOf(in); + return !inElement || castMakesInterface(inElement, outElement, depth + 1); + } + + // a reference seen as another (`!ts.bound_ref` as `!ts.ref`): what it points to + auto pointee = [](mlir::Type type) -> mlir::Type { + if (auto refType = mlir::dyn_cast(type)) + { + return refType.getElementType(); + } + + if (auto valueRefType = mlir::dyn_cast(type)) + { + return valueRefType.getElementType(); + } + + if (auto boundRefType = mlir::dyn_cast(type)) + { + return boundRefType.getElementType(); + } + + return {}; + }; + + if (auto outPointee = pointee(out)) + { + auto inPointee = pointee(in); + return !inPointee || castMakesInterface(inPointee, outPointee, depth + 1); + } + + // anything else reinterpreted: what is read out of it was never checked + return true; + } + + mlir::StringAttr OwnershipSignaturePass::slotFunction(mlir::Value value) + { + while (auto castOp = value ? value.getDefiningOp() : mlir_ts::CastOp()) + { + value = castOp.getIn(); + } + + auto symbolRefOp = value ? value.getDefiningOp() : mlir_ts::SymbolRefOp(); + return symbolRefOp ? symbolRefOp.getIdentifierAttr().getAttr() : mlir::StringAttr(); + } + + mlir_ts::AddressOfOp OwnershipSignaturePass::addressOfThroughCasts(mlir::Value value) + { + while (auto castOp = value ? value.getDefiningOp() : mlir_ts::CastOp()) + { + value = castOp.getIn(); + } + + return value ? value.getDefiningOp() : mlir_ts::AddressOfOp(); + } + + llvm::StringSet<> OwnershipSignaturePass::exportedWords(mlir::ModuleOp module) + { + llvm::StringSet<> words; + module.walk([&](mlir_ts::GlobalOp globalOp) { + if (!globalOp.getSymName().starts_with("__decls")) + { + return; + } + + globalOp.getInitializerRegion().walk([&](mlir_ts::ConstantOp constantOp) { + auto text = mlir::dyn_cast(constantOp.getValue()); + if (!text) + { + return; + } + + llvm::StringRef rest = text.getValue(); + while (!rest.empty()) + { + auto isWordChar = [](char c) { return llvm::isAlnum(c) || c == '_' || c == '$'; }; + rest = rest.drop_while([&](char c) { return !isWordChar(c); }); + auto word = rest.take_while(isWordChar); + if (!word.empty()) + { + words.insert(word); + } + + rest = rest.drop_front(word.size()); + } + }); + }); + + return words; + } + + void OwnershipSignaturePass::collectInterfaceFamilies(mlir::ModuleOp module) + { + InterfaceScan scan; + readInterfaceVTables(module, scan); + traceInterfaceValues(module, scan); + openExposedInterfaces(module, scan); + openThroughMembers(scan); + openLooseVTables(scan); + buildInterfaceFamilies(scan); + collectFieldFunctions(module); + markInterfaceReached(scan); + } + + void OwnershipSignaturePass::readInterfaceVTables(mlir::ModuleOp module, InterfaceScan &scan) + { + module.walk([&](mlir_ts::GlobalOp globalOp) { + auto name = globalOp.getSymName(); + if (!name.ends_with("..vtbl")) + { + return; + } + + auto owner = name.drop_back(llvm::StringRef("..vtbl").size()); + llvm::DenseMap values; + globalOp.getInitializerRegion().walk([&](mlir_ts::InsertPropertyOp insertOp) { + if (insertOp.getPosition().size() == 1) + { + values[insertOp.getPosition()[0]] = insertOp.getValue(); + } + }); + + auto header = slotFunction(values.lookup(0)); + auto tupleType = mlir::dyn_cast(globalOp.getType()); + if (!header || !tupleType) + { + scan.unread.push_back(name); + return; + } + + if (header.getValue() == (owner + "..instanceOf").str()) + { + // an exported class (not in an executable, whose exports no module sees) + auto exposed = !executable && (!globalOp.isPrivate() || globalOp->hasAttr("export") || globalOp->hasAttr(DLL_EXPORT)); + ownClasses.insert(owner); + if (exposed) + { + nameableClasses.insert(owner); + } + + for (auto &[position, value] : values) + { + auto addressOfOp = addressOfThroughCasts(value); + auto id = position < static_cast(tupleType.size()) + ? mlir::dyn_cast_or_null(tupleType.getFieldInfo(position).id) + : mlir::StringAttr(); + if (!addressOfOp || !id) + { + continue; + } + + scan.classHeld[id.getValue()].insert(addressOfOp.getGlobalName()); + if (exposed) + { + scan.open.insert(id.getValue()); // another module may extend the class + } + } + + return; + } + + if (!header.getValue().ends_with("..instanceOf") && header.getValue() != ".instanceOf.none") + { + scan.unread.push_back(name); + return; + } + + auto &vtable = scan.vtables[name]; + vtable.type = tupleType; + for (auto &[position, value] : values) + { + if (position > 0) + { + vtable.slots[position] = slotFunction(value); + if (auto field = slotField(value); field.first) + { + vtable.fields[position] = field; + } + } + } + }); + } + + void OwnershipSignaturePass::traceInterfaceValues(mlir::ModuleOp module, InterfaceScan &scan) + { + auto vtableOf = [&](mlir::Value value) -> llvm::StringRef { + // the global itself + if (auto addressOfOp = addressOfThroughCasts(value)) + { + return scan.vtables.count(addressOfOp.getGlobalName()) ? addressOfOp.getGlobalName() : llvm::StringRef(); + } + + // a copy of it on the heap, its absent members' slots patched + while (auto castOp = value.getDefiningOp()) + { + value = castOp.getIn(); + } + + if (auto newOp = value.getDefiningOp()) + { + llvm::StringRef copied; + for (auto *user : newOp->getUsers()) + { + auto storeOp = mlir::dyn_cast(user); + auto loadOp = storeOp && storeOp.getReference() == newOp.getResult() + ? storeOp.getValue().getDefiningOp() + : mlir_ts::LoadOp(); + auto addressOfOp = loadOp ? addressOfThroughCasts(loadOp.getReference()) : mlir_ts::AddressOfOp(); + if (addressOfOp && scan.vtables.count(addressOfOp.getGlobalName())) + { + if (!copied.empty()) + { + return {}; + } + + copied = addressOfOp.getGlobalName(); + } + } + + return copied; + } + + return {}; + }; + + // an object's class vtable read at run time: `ts.VTableOffsetRef(ts.Load(ts.PropertyRef(object, 0)), k)` + auto readFromClass = [](mlir::Value value) { + auto offsetRefOp = value.getDefiningOp(); + auto loadOp = offsetRefOp ? offsetRefOp.getVtable().getDefiningOp() : mlir_ts::LoadOp(); + auto propertyRefOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); + return propertyRefOp && propertyRefOp.getPosition() == 0 && + mlir::isa(propertyRefOp.getObjectRef().getType()); + }; + + // what reads a vtable's address: making an interface value, a class vtable's slot, the copy + std::function onlyMakesValues = [&](mlir::Value address) { + return llvm::all_of(address.getUses(), [&](mlir::OpOperand &use) { + auto *user = use.getOwner(); + if (auto newInterfaceOp = mlir::dyn_cast(user)) + { + return newInterfaceOp.getInterfaceVTable() == use.get(); + } + + if (mlir::isa(user)) + { + return onlyMakesValues(user->getResult(0)); + } + + if (mlir::isa(user)) + { + return use.getOperandNumber() == 0 && user->getParentOfType() != nullptr; + } + + // the copy: read whole, and stored into a block `ts.New` made + if (auto loadOp = mlir::dyn_cast(user)) + { + return llvm::all_of(loadOp->getUsers(), [&](mlir::Operation *loadUser) { + auto storeOp = mlir::dyn_cast(loadUser); + return storeOp && storeOp.getValue() == loadOp.getResult() && + storeOp.getReference().getDefiningOp(); + }); + } + + return false; + }); + }; + + auto isOpaque = [](mlir::Type type) { return mlir::isa(type); }; + module.walk([&](mlir::Operation *op) { + if (auto newInterfaceOp = mlir::dyn_cast(op)) + { + auto name = mlir::cast(newInterfaceOp.getType()).getName().getValue(); + auto vtable = newInterfaceOp.getInterfaceVTable(); + if (auto found = vtableOf(vtable); !found.empty()) + { + scan.madeWith[name].insert(found); + } + else if (readFromClass(vtable)) + { + // any class's vtable for the interface (classHeld, all in its families below) + if (!scan.classHeld.count(name)) + { + scan.open.insert(name); + } + } + else if (auto extractOp = vtable.getDefiningOp()) + { + scan.castOpened.insert(name); + valueInterfaces(extractOp.getInterfaceVal().getType(), scan.castOpened); + } + else + { + scan.allOpen = true; + } + + return; + } + + if (auto castOp = mlir::dyn_cast(op)) + { + auto in = castOp.getIn().getType(); + auto out = castOp.getType(); + if (isOpaque(out)) + { + valueInterfaces(in, scan.boxed); + } + + if (castMakesInterface(in, out)) + { + valueInterfaces(out, scan.castOpened); + valueInterfaces(in, scan.castOpened); + if (isOpaque(in)) + { + valueInterfaces(out, scan.unboxedInto); + } + } + + // a function value reinterpreted carries the interface values of its parameters and + // results across unseen (review6 x1c, x1r) + llvm::StringSet<> signatures; + auto boxing = false; + if (castReinterpretsFunction(in, out, signatures, boxing)) + { + scan.openAll(signatures); + if (isOpaque(in)) + { + scan.unboxedFunction = true; + } + } + else if (boxing) + { + for (auto &entry : signatures) + { + scan.boxed.insert(entry.getKey()); + } + } + + return; + } + + if (auto interfaceRefOp = mlir::dyn_cast(op)) + { + if (!interfaceRefOp->use_empty()) + { + scan.open.insert(interfaceRefOp.getIdentifier()); + } + + return; + } + + if (auto symbolRefOp = mlir::dyn_cast(op)) + { + if (auto interfaceType = mlir::dyn_cast(symbolRefOp.getInterfaceVal().getType())) + { + memberTypeInterfaces(symbolRefOp.getType(), scan.memberInterfaces[interfaceType.getName().getValue()]); + } + + return; + } + + if (auto addressOfOp = mlir::dyn_cast(op)) + { + if (auto found = scan.vtables.find(addressOfOp.getGlobalName()); found != scan.vtables.end() && !onlyMakesValues(addressOfOp.getResult())) + { + found->second.written = true; + } + } + }); + } + + void OwnershipSignaturePass::openExposedInterfaces(mlir::ModuleOp module, InterfaceScan &scan) + { + // seen from another module: a word of its exported declarations, or in a reachable type. An + // executable's exports are seen by no other module (USER decision, spec 31): only what it + // imports - a declared function, an imported global - comes from outside. + if (!executable) + { + exportWords = exportedWords(module); + } + + llvm::StringSet<> exposed; + llvm::DenseSet seen; + for (auto &entry : functions) + { + // a `public` method of a class no other module can name is not one: only a declaration + // re-parsed from `__decls` names a function of another module + auto exported = !executable && (entry.second->hasAttr("export") || entry.second->hasAttr(DLL_EXPORT)); + if (entry.second.isDeclaration() || exported) + { + anyInterfaces(entry.second.getFunctionType(), exposed, seen, &nameableClasses); + } + } + + for (auto &entry : globals) + { + auto globalOp = entry.second; + // another module's global, declared here with no initializer (an import of a .ts module + // carries `export`) + auto imported = globalOp->hasAttr("import") || globalOp->hasAttr(DLL_IMPORT) || + globalOp.getInitializerRegion().empty() || globalOp.getInitializerRegion().front().empty(); + auto exported = !globalOp.isPrivate() || globalOp->hasAttr("export") || globalOp->hasAttr(DLL_EXPORT); + if (imported || (exported && !executable)) + { + anyInterfaces(globalOp.getType(), exposed, seen, &nameableClasses); + } + } + + scan.openAll(exposed); + if (!scan.unboxedInto.empty() || scan.unboxedFunction) + { + scan.openAll(scan.boxed); + for (auto &entry : scan.boxed) + { + scan.castOpened.insert(entry.getKey()); + } + } + } + + void OwnershipSignaturePass::openThroughMembers(InterfaceScan &scan) + { + // An interface value made out of another one (or out of `any`) is called with the other's + // members: `HasF2 { f: (o: Op2, ...) }` seen as `HasF { f: (o: Op, ...) }` hands an `Op` to a + // function that calls through `Op2` (review6r1 v5, v11f). So every interface either side's + // members mention is open too, and theirs, recursively (controller ruling, spec 31). + // + // A class's vtables for its interfaces are its members' families whether or not this module + // makes an interface value of the class (a method called directly then keeps its facts), and + // their slots' types are the interface's members' types: an interface whose values are all + // made from class objects is read through them (review6r2 d1-d3). + for (auto &[name, held] : scan.classHeld) + { + scan.madeWith[name].insert(held.begin(), held.end()); + } + + for (auto &[name, made] : scan.madeWith) + { + for (auto vtableName : made) + { + if (auto found = scan.vtables.find(vtableName); found != scan.vtables.end() && found->second.type) + { + for (auto [position, field] : llvm::enumerate(found->second.type.getFields())) + { + if (position > 0) + { + memberTypeInterfaces(field.type, scan.memberInterfaces[name]); + } + } + } + } + } + + llvm::SmallVector work; + for (auto &entry : scan.castOpened) + { + work.push_back(entry.getKey().str()); + } + + llvm::StringSet<> expanded; + while (!work.empty()) + { + auto name = work.pop_back_val(); + if (!expanded.insert(name).second) + { + continue; + } + + scan.open.insert(name); + for (auto &member : scan.memberInterfaces[name]) + { + work.push_back(member.getKey().str()); + } + } + } + + void OwnershipSignaturePass::openLooseVTables(InterfaceScan &scan) + { + // A vtable this pass could not read, or one no value of any interface is made with, that is + // named for an interface (`...vtbl`, `...vtbl`): the interface may have an + // implementation outside its families. Belt and braces - every way to make an interface + // value is traced above - but a missed member would get its family's facts. + llvm::DenseSet made; + for (auto &entry : scan.madeWith) + { + made.insert(entry.second.begin(), entry.second.end()); + } + + llvm::SmallVector loose(scan.unread.begin(), scan.unread.end()); + for (auto &entry : scan.vtables) + { + if (!made.contains(entry.getKey())) + { + loose.push_back(entry.getKey()); + } + } + + for (auto &entry : scan.madeWith) + { + auto name = entry.getKey().str(); + if (llvm::any_of(loose, [&](llvm::StringRef vtableName) { + return vtableName.ends_with("." + name + "..vtbl") || vtableName.starts_with(name + "."); + })) + { + scan.open.insert(entry.getKey()); + } + } + } + + void OwnershipSignaturePass::buildInterfaceFamilies(InterfaceScan &scan) + { + auto &words = exportWords; + auto isOpen = [&](llvm::StringRef name) { + // a namespaced interface is declared by its last name + auto last = name.rsplit('.').second; + return scan.allOpen || scan.open.contains(name) || words.contains(name) || (!last.empty() && words.contains(last)); + }; + + // the families + for (auto &[name, made] : scan.madeWith) + { + // a class vtable's slot holding a vtable this pass did not read (another module's) + auto allRead = llvm::all_of(made, [&](llvm::StringRef vtableName) { return scan.vtables.count(vtableName); }); + auto closed = !isOpen(name) && allRead; + for (auto vtableName : made) + { + if (!scan.vtables.count(vtableName)) + { + continue; + } + + if (allRead) + { + interfaceVTableTypes[name].push_back(scan.vtables[vtableName].type); + } + + scan.tied.insert(vtableName); + auto &vtable = scan.vtables[vtableName]; + for (auto [position, field] : llvm::enumerate(vtable.type.getFields())) + { + if (position == 0) + { + continue; + } + + auto &family = interfaceFamilies[{name.str(), static_cast(position)}]; + auto id = mlir::dyn_cast_or_null(field.id); + auto member = vtable.slots.lookup(position); + auto funcOp = member ? functions.lookup(member.getValue()) : mlir_ts::FuncOp(); + if (!family.field) + { + family.field = id; + } + + // a member that is not private (a `public` method) is open (findOpen): it has no + // facts to give, and gives the family none + family.closed = family.closed && closed && !vtable.written && id && family.field == id && funcOp && + !funcOp.isDeclaration() && !declaresCallerFacts(funcOp); + if (member && !llvm::is_contained(family.members, member)) + { + family.members.push_back(member); + } + + // the same slot as a field: an object literal's field of function type + auto &fieldFamily = interfaceFieldFamilies[{name.str(), static_cast(position)}]; + auto target = vtable.fields.lookup(position); + auto refType = mlir::dyn_cast(field.type); + if (!fieldFamily.field) + { + fieldFamily.field = id; + } + + fieldFamily.closed = fieldFamily.closed && closed && !vtable.written && id && fieldFamily.field == id && + target.first && refType && mlir::isa(refType.getElementType()); + if (target.first && !llvm::is_contained(fieldFamily.targets, target)) + { + fieldFamily.targets.push_back(target); + } + } + } + } + } + + void OwnershipSignaturePass::markInterfaceReached(InterfaceScan &scan) + { + // what an open family holds, and a vtable no interface value is made with, is reached + // through calls that are not resolved + auto reached = [&](mlir::StringAttr member) { + if (auto funcOp = member ? functions.lookup(member.getValue()) : mlir_ts::FuncOp()) + { + interfaceReached.insert(funcOp); + } + }; + + for (auto &[key, family] : interfaceFamilies) + { + if (!family.closed) + { + llvm::for_each(family.members, reached); + } + } + + for (auto &entry : scan.vtables) + { + if (!scan.tied.contains(entry.getKey())) + { + for (auto &slot : entry.second.slots) + { + reached(slot.second); + } + } + } + } + +} // namespace own_signature diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp index 527c93ff7..553ec9c36 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp @@ -1,414 +1,14 @@ -#include "mlir/Pass/Pass.h" -#include "mlir/IR/BuiltinOps.h" -#include "mlir/IR/SymbolTable.h" -#include "mlir/Interfaces/FunctionInterfaces.h" +// OwnershipSignaturePass (see OwnershipSignaturePass.h): the pass itself, the facts a signature says, +// the facts callers must know and their fixpoint, drops, and the facts a library exports. -#include "TypeScript/TypeScriptDialect.h" -#include "TypeScript/TypeScriptOps.h" -#include "TypeScript/Passes.h" -#include "TypeScript/Defines.h" -#include "TypeScript/MLIRLogic/MLIRTypeHelper.h" - -#include "OwnershipFacts.h" - -#include "llvm/ADT/DenseMap.h" -#include "llvm/ADT/DenseSet.h" -#include "llvm/ADT/MapVector.h" -#include "llvm/ADT/SetVector.h" -#include "llvm/ADT/SmallVector.h" -#include "llvm/ADT/StringMap.h" -#include "llvm/ADT/StringSet.h" - -#include +#include "OwnershipSignaturePass.h" #define DEBUG_TYPE "own" -namespace mlir_ts = mlir::typescript; - -namespace +namespace own_signature { -using namespace own_facts; - -// The callee facts of -mm=own (spec 2.4, 3.2), computed over the whole module before the -// per-function inference runs. Each call the pass can resolve - a direct call, a virtual call -// whose candidates are all defined here, the `.instanceOf` slot - gets the facts its candidates -// agree on, pinned on the call op, so the inference reads them without looking anything up. -// -// `__own_no_drops` goes one way: a call with no facts may destroy anything, which can only turn a -// program into an error. The facts a callee's own body relies on - a parameter it keeps, a result -// that borrows an argument - go the other way: a caller that does not know them borrows the -// argument and releases it, or releases a result it does not own, a double free either way. So a -// function has those only when every call that can reach it is one this pass resolved (the closed -// world, see `open`). -// -// `__own_no_drops` alone crosses a module boundary: a library built under own exports the names of -// its exported functions that have it (SHARED_LIB_OWN_FACTS), and a module importing it calls them -// knowing it. The other two cannot: a module linking the library statically re-parses its source -// and sees no facts at all, and a caller that does not know them frees twice. -class OwnershipSignaturePass : public mlir::PassWrapper> -{ - public: - MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(OwnershipSignaturePass) - - // `executable`: the module is built into a program (`--emit=exe`, or the JIT's entry module), - // whose exports no other module sees (USER decision, spec 31). Unknown is a library. - explicit OwnershipSignaturePass(bool executable = false) : executable(executable) - { - } - - bool executable = false; - - // A global is a root (rc 5ai): what its initializer makes moves into it, and nothing gives it - // back. rc retains a fresh value for its `ts.GlobalResult`, and that retain is the move: it - // goes. The per-function inference never sees a global's region, so any other retain there is - // reported here, by the global's name, rather than left to the lowering's "left a retain - // behind". - // Every use of `value` ends in the global's `ts.GlobalResult`: given to it, or an element of a - // fresh array literal that is (`const nested = [[1, 2], [3]]`), or the payload of a - // `new Shared(x)` (`let g = new Shared({...})`). Its retains are judged on their own. - static bool movesIntoResult(mlir::Value value) - { - return llvm::all_of(value.getUses(), [](mlir::OpOperand &use) { - auto *user = use.getOwner(); - if (mlir::isa(user)) - { - return true; - } - - // given to `new Shared(x)`: it moves into the handle's block, which owns it from then on - // whatever becomes of the handle - rc counts the handle (spec 23.2, 26) - if (auto sharedNewOp = mlir::dyn_cast(user)) - { - return sharedNewOp.getValue() == use.get(); - } - - if ((isView(user) && use.getOperandNumber() == 0) || mlir::isa(user)) - { - return movesIntoResult(user->getResult(0)); - } - - // copied into a block `ts.New` made, which itself moves in: an object literal's box - if (auto storeOp = mlir::dyn_cast(user); storeOp && use.getOperandNumber() == 0) - { - auto block = storeOp.getReference(); - return block.getDefiningOp() && - llvm::all_of(block.getUses(), [&](mlir::OpOperand &blockUse) { - return blockUse.getOwner() == storeOp.getOperation() || - (isView(blockUse.getOwner()) && movesIntoResult(blockUse.getOwner()->getResult(0))); - }); - } - - return false; - }); - } - - // A record copied out of a local a `ts.Constant` initializes, whose owning fields are given - // nothing that holds a block: it holds constants only - numbers, immortal string literals, - // functions - and nobody else's reference. - static bool holdsOnlyConstants(mlir::Value value) - { - auto loadOp = value.getDefiningOp(); - auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); - if (!varOp || !varOp.getInitializer() || !varOp.getInitializer().getDefiningOp()) - { - return false; - } - - return llvm::all_of(varOp->getUsers(), [](mlir::Operation *user) { - if (mlir::isa(user)) - { - return true; - } - - auto propertyRefOp = mlir::dyn_cast(user); - return propertyRefOp && llvm::all_of(propertyRefOp->getUsers(), [&](mlir::Operation *fieldUser) { - auto storeOp = mlir::dyn_cast(fieldUser); - return mlir::isa(fieldUser) || - (storeOp && storeOp.getReference() == propertyRefOp.getResult() && - holdsNoBlock(storeOp.getValue())); - }); - }); - } - - // Whether `op` may run more than once in one run of its function: it is inside a loop op, or its - // block - or the block of a region op holding it - is on a cycle of its region's CFG, a path from - // the block's successors back to it. (By this pass the affine lowering has made every loop a CFG - // cycle of the function body; the loop op is kept for safety.) - static bool mayRunAgain(mlir::Operation *op) - { - for (auto *at = op; at && !mlir::isa(at); at = at->getParentOp()) - { - if (mlir::isa(at)) - { - return true; - } - - auto *block = at->getBlock(); - if (!block) - { - return true; // not in a function: nothing is known of how often it runs - } - - llvm::SmallPtrSet seen; - llvm::SmallVector work(block->getSuccessors().begin(), block->getSuccessors().end()); - while (!work.empty()) - { - auto *next = work.pop_back_val(); - if (next == block) - { - return true; - } - - if (seen.insert(next).second) - { - work.append(next->getSuccessors().begin(), next->getSuccessors().end()); - } - } - } - - return false; - } - - // The immortal globals (OWN_IMMORTAL_GLOBAL_ATTR_NAME, spec 25). A global of a class type, - // not exported, is immortal when only its initializer writes it, once, and gives it a fresh - // object, nothing, or another immortal global's value. Its initializer is either its own region - // (`const c = new C()`) or, for a class static, the one store into it in the class's static - // constructor, which a `ts.GlobalConstructor` runs (the region then yields only the default). - // Top-level script code is such a constructor too, so a module-level `let c: C;` it stores - // into once is one as well. "Once" is how often the store runs, not how many stores there are: - // a store in a loop (`static { for (...) K.w = new V(i); }`) would mark every block it is given - // immortal, and each old one would leak, so a store that may run again leaves the global mortal, - // and each store releases the old value, as for any other global. - // - // The store - the region's, which the lowering makes - marks the block HEAP_BLOCK_IMMORTAL, so - // every block the global ever holds is one no release frees. The value test makes sure the block - // is the global's to keep: an object made right there and given back by nobody. A call's result - // is not one, whatever its callee: these facts are found before the callees' (computeFacts), and - // a global's region is no function the inference checks, so a result that borrows its argument - - // a temporary the region frees, an element of another global's array - would be marked, and - // stored, as the global's own. Such a global stays mortal, and a read of it keeps today's errors. - void findImmortalGlobals(mlir::ModuleOp module) - { - // the static constructors, which run once before the program - llvm::StringSet<> constructors; - module.walk([&](mlir_ts::GlobalConstructorOp constructorOp) { constructors.insert(constructorOp.getGlobalName()); }); - - // a static's address taken out of a class's vtable (`k.w = x`, `k: typeof K`) and written - // through: it may be any class's static the vtable slot holds, so no static is immortal - auto vtableWritten = false; - module.walk([&](mlir::Operation *op) { - if (mlir::isa(op) && - mlir::isa(op->getResult(0).getType()) && - !llvm::all_of(op->getUsers(), [](mlir::Operation *user) { return mlir::isa(user); })) - { - vtableWritten = true; - } - }); - - auto uses = mlir::SymbolTable::getSymbolUses(&module.getBodyRegion()); - if (!uses) - { - return; // an op this pass cannot read symbols through: nothing is known never written - } - - llvm::StringMap> usesOf; - for (auto &use : *uses) - { - usesOf[use.getSymbolRef().getRootReference().getValue()].push_back(use.getUser()); - } - - // each candidate with the value its initializer gives it, and the store that does, if any - llvm::MapVector> candidates; - MLIRTypeHelper mth(&getContext(), CompileOptions{}); - module.walk([&](mlir_ts::GlobalOp globalOp) { - if (!mlir::isa(globalOp.getType()) || !mth.ownsHeapMemory(globalOp.getLoc(), globalOp.getType()) || - globalOp->hasAttr("export") || globalOp->hasAttr("import") || globalOp->hasAttr(DLL_EXPORT) || - globalOp->hasAttr(DLL_IMPORT) || !globalOp.getInitializerRegion().hasOneBlock()) - { - return; - } - - auto resultOp = mlir::dyn_cast(globalOp.getInitializerBlock()->getTerminator()); - if (!resultOp || resultOp->getNumOperands() != 1) - { - return; - } - - mlir_ts::StoreOp initStore; - for (auto *user : usesOf.lookup(globalOp.getSymName())) - { - // its vtable's entry, which only a `ts.VirtualSymbolRef` reads (vtableWritten) - if (mlir::isa(user) && user->getParentOfType()) - { - if (vtableWritten) - { - return; - } - - continue; - } - - if (mlir::isa(user)) - { - continue; // only loaded, or vtableWritten - } - - auto addressOfOp = mlir::dyn_cast(user); - if (!addressOfOp) - { - return; - } - - auto released = false; - auto stored = false; - for (auto *addressUser : addressOfOp->getUsers()) - { - if (mlir::isa(addressUser)) - { - continue; - } - - // the old value, the default, given up before the store - if (mlir::isa(addressUser)) - { - released = true; - continue; - } - - // the one store, in a static constructor, run once - auto storeOp = mlir::dyn_cast(addressUser); - auto funcOp = storeOp ? storeOp->getParentOfType() : mlir_ts::FuncOp(); - if (initStore || !storeOp || storeOp.getReference() != addressOfOp.getResult() || - storeOp.getValue() == addressOfOp.getResult() || !funcOp || !constructors.contains(funcOp.getSymName()) || - mayRunAgain(storeOp)) - { - return; - } - - initStore = storeOp; - stored = true; - } - - if (released && !stored) - { - return; - } - } - - // a static's region yields only its default, which the store replaces - if (initStore && !regionYieldsDefault(globalOp)) - { - return; - } - - candidates[globalOp] = {initStore ? initStore.getValue() : resultOp->getOperand(0), initStore}; - }); - - // given a fresh object, nothing, or another immortal global's value: in module order, until - // no more are found - llvm::StringSet<> immortal; - auto madeHere = [&](mlir::Value value) { - auto root = rootOf(value); - if (!root.getDefiningOp() || isCall(root.getDefiningOp()) || !isFresh(root)) - { - return false; - } - - // and not given back before the global takes it - auto released = false; - forEachUse(root, [&](mlir::Operation *user, mlir::Value) { released = released || mlir::isa(user); }); - return !released; - }; - // isImmortalGlobalRead, of the globals found so far: none is marked yet - auto readsImmortal = [&](mlir::Value value) { - auto addressOfOp = globalReadOf(value); - return addressOfOp && immortal.contains(addressOfOp.getGlobalName()); - }; - - for (auto found = true; found;) - { - found = false; - for (auto &[globalOp, init] : candidates) - { - auto value = init.first; - if (!immortal.contains(globalOp.getSymName()) && (madeHere(value) || holdsNoBlock(value) || readsImmortal(value))) - { - immortal.insert(globalOp.getSymName()); - found = true; - } - } - } - - auto unit = mlir::UnitAttr::get(&getContext()); - for (auto &[globalOp, init] : candidates) - { - if (!immortal.contains(globalOp.getSymName())) - { - continue; - } - - globalOp->setAttr(OWN_IMMORTAL_GLOBAL_ATTR_NAME, unit); - if (init.second) - { - init.second->setAttr(OWN_IMMORTAL_GLOBAL_ATTR_NAME, unit); - } - - for (auto *user : usesOf.lookup(globalOp.getSymName())) - { - if (mlir::isa(user)) - { - user->setAttr(OWN_IMMORTAL_GLOBAL_ATTR_NAME, unit); - } - } - } - } - - void moveIntoGlobals(mlir::ModuleOp module) - { - module.walk([&](mlir_ts::GlobalOp globalOp) { - llvm::SmallVector retains; - globalOp.walk([&](mlir_ts::RetainOp retainOp) { retains.push_back(retainOp); }); - for (auto retainOp : retains) - { - // a handle's retain is rc's: the global's count, which own leaves in place (spec 23.2) - if (touchesHandle(retainOp)) - { - continue; - } - - auto value = retainOp.getReference(); - - // an immortal global's value (spec 25): owned by no one, held by any number - if (isImmortalGlobalRead(value)) - { - retainOp.erase(); - continue; - } - - // nothing in the region gives the block back, or takes a second reference to it: - // the global is its one owner - auto released = false; - auto retained = 0; - forEachUse(rootOf(value), [&](mlir::Operation *user, mlir::Value) { - released = released || mlir::isa(user); - retained += mlir::isa(user); - }); - - if (retained == 1 && !released && (isFresh(value) || holdsOnlyConstants(value) || isConstantData(value)) && movesIntoResult(value)) - { - retainOp.erase(); - continue; - } - - retainOp.emitError("the initializer of global '") - << globalOp.getSymName() << "' takes a second reference; -mm=own cannot prove a move or a borrow here yet"; - signalPassFailure(); - } - }); - } - - void runOnOperation() override + void OwnershipSignaturePass::runOnOperation() { auto module = getOperation(); module.walk([&](mlir_ts::FuncOp funcOp) { functions[funcOp.getSymName()] = funcOp; }); @@ -538,9 +138,7 @@ class OwnershipSignaturePass : public mlir::PassWrapper(); if (!symbolRefOp) @@ -552,10 +150,7 @@ class OwnershipSignaturePass : public mlir::PassWrappersecond.isDeclaration() ? found->second : mlir_ts::FuncOp(); } - // The fields of a closure body's capture box whose cell it assigns (OWN_ASSIGNS_CAPTURES_ATTR_NAME), - // directly or through a closure it builds over the cell. Every field, when the body is unknown or - // a cell goes anywhere this does not follow. - llvm::SetVector assignedCaptures(mlir_ts::FuncOp funcOp, int depth = 0) + llvm::SetVector OwnershipSignaturePass::assignedCaptures(mlir_ts::FuncOp funcOp, int depth) { llvm::SetVector assigned; auto boxType = funcOp && funcOp.getNumArguments() > 0 && !funcOp.getBody().empty() @@ -606,8 +201,7 @@ class OwnershipSignaturePass : public mlir::PassWrapper> assignedCache; - - // What a call may reach: known, when every candidate is a function defined in this module (or - // the call goes through the `.instanceOf` slot, or reaches a function another module says - // destroys nothing); else anything. - struct Callees + llvm::ArrayRef OwnershipSignaturePass::declared(mlir::Operation *funcOp, llvm::StringRef name) { - bool known = false; - bool instanceOf = false; - bool imported = false; - // a function read out of a field that only ever holds it (fieldFunctions) - bool fieldFunction = false; - // the slot of a closed interface (resolveInterfaceSlot) - bool viaInterface = false; - // a parameter of a closed function that callers pass only functions of this module - // (callbackFamilies), with each member's shift: a closure's body takes its capture box - // first, so its parameter k is the call's argument k - 1 - bool viaCallback = false; - llvm::SmallVector shifts; - // the name the imported function is exported under - llvm::StringRef importedName; - llvm::SmallVector funcs; - // the symbols the call names, whether or not it is known: a direct callee, or every - // member of a virtual slot's family (for the facts a declaration says, declaredFacts) - llvm::SmallVector named; - }; - - struct Call + if (auto attr = funcOp->getAttrOfType(name)) + { + return attr.asArrayRef(); + } + + return {}; + } + + bool OwnershipSignaturePass::declaresFacts(mlir::Operation *funcOp) { - mlir::Operation *op; - Callees callees; - }; - - llvm::StringMap functions; - llvm::StringMap globals; - // Globals whose address is used other than to load from it: what they hold may change. - llvm::StringSet<> writtenGlobals; - llvm::SmallVector calls; - - // Functions of imported libraries that destroy nothing a caller can reach, by the name they - // are exported under (SHARED_LIB_OWN_NO_DROPS_ATTR_NAME). - llvm::StringSet<> importedNoDrops; - - // (tuple type, field) -> the one function an object of that type holds in that field. A - // generator's state object holds its `next` so. - own_facts::FieldFunctions fieldFunctions; - - // Class names, from their vtables, to split a method's symbol into class and method name. - llvm::StringSet<> classNames; - // (vtable position, method name) -> the functions at that position under that name, in every - // class vtable of the module: what a virtual call through that slot may reach. - std::map, llvm::SmallVector> families; - - // Functions whose callers are not all known - a caller outside the module, or a reference to - // the symbol that is not a call this pass resolves - and why, for the error in their body. - llvm::DenseMap open; - - // Methods an interface's vtable holds (`...vtbl`, or `...vtbl` - // for an object literal) whose calls through the interface are not resolved: the slot's family - // is open (interfaceFamilies), or the vtable is one this pass cannot tie to an interface. Such - // a call (`ts.InterfaceSymbolRef`, then an indirect call) is given none of the method's facts: a - // caller that hands it an argument it keeps still releases it, and a caller of one whose result - // borrows an argument or `this` releases the borrow as its own (a double free either way). So a - // method reached this way that keeps a parameter or returns a borrow - by its body or by `@keeps` - // / `@lends` - loses its facts, and the body's error says why (`is a parameter, kept here, but - // -mm=own cannot make the callers of ... give it up`, `is a parameter, returned here, but -mm=own - // cannot tell the callers of ... that the result borrows it`). A method reached only through - // closed interfaces keeps its facts: their calls get them (spec 31). - llvm::DenseSet interfaceReached; - - // ---- Interfaces closed in this module (G9, spec 31) ---- - // - // A call through an interface reaches whatever function the slot of the interface value's vtable - // holds. When every such vtable is one this module defines and every interface value is made - // from one of them, the slot holds one of a known set of functions - the slot's family - and the - // call is resolved to it like a virtual call of a private class family: it gets the facts its - // members agree on (meetFacts demotes a family that disagrees), and `__own_no_drops` when every - // member destroys nothing. Anything this cannot see opens the interface, and then the call is - // what it was: no facts, and its members reached that way lose theirs (interfaceReached). - // - // Open, for an interface I: - // - another module can see it: its name in an exported declaration (`__decls`), or `!ts.iface<@I>` - // in the type of a function or global another module can reach - an exported or declared - // function, a global that is not private, or is exported or imported; - // - an I value made other than by `ts.NewInterface` over a vtable this pass reads (the global, a - // heap copy of it, or the one an object's class vtable holds for I): a cast from another - // interface (`J` to `I`, which opens `J` too: its members are reached through `I`), from - // `any` or an opaque pointer (which opens every interface put into one), from a tuple or array - // that does not hold I already (castMakesInterface), a `ts.InterfaceRef` used as a value; - // - a vtable of I written after its initializer, or read other than to make an interface value; - // - a class vtable that holds a vtable of I and is not private (an exported class: another - // module may extend it); - // - and, per slot, a member that is not a function this module defines, a slot that holds no - // function (a field's offset, a thunk's cell), or a member that says `@keeps` or `@lends` (its - // facts are its signature's, which a family cannot change). A member that is not private (a - // `public` method) is open (findOpen): it has no facts of its own to give the family. - struct InterfaceFamily + return funcOp->hasAttr(OWN_DECLARED_KEEPS_ATTR_NAME) || funcOp->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME) || + funcOp->hasAttr(OWN_DECLARED_LENDS_ATTR_NAME); + } + + bool OwnershipSignaturePass::declaresCallerFacts(mlir::Operation *funcOp) { - // the slot's name, which every vtable of the interface agrees on - mlir::StringAttr field; - llvm::SmallVector members; - bool closed = true; - }; - - // (interface, vtable position) -> the functions every vtable of the interface holds there - std::map, InterfaceFamily> interfaceFamilies; - - // The words of the module's exported declarations (none in an executable), the classes a class - // vtable is the own vtable of, and the classes another module can name: exported, or in the - // type of an exported or declared function or of an exported or imported global (findOpen). - llvm::StringSet<> exportWords; - llvm::StringSet<> ownClasses; - llvm::StringSet<> nameableClasses; - - // A field of function type (`f: (x: number) => number`), called through the interface: the slot - // holds the offset of a field of each implementing object literal, and the call goes to whatever - // function that field holds. When every vtable's slot is such an offset and every function ever - // stored into those fields is one this module defines - the literal's own (`{ f: function ... }`, - // in its `ts.Constant`), or one stored into the field later, through the object or through the - // interface - the call is resolved to them (spec 31). A store of anything else (a parameter, a - // call's result, a closure), a field reached other than to be read or stored, or an object of - // the literal's type made other than by the literal opens the slot. A class implementing the - // field holds a thunk's cell, which this does not follow: open. - struct InterfaceFieldFamily + return funcOp->hasAttr(OWN_DECLARED_KEEPS_ATTR_NAME) || funcOp->hasAttr(OWN_DECLARED_LENDS_ATTR_NAME); + } + + llvm::SmallVector OwnershipSignaturePass::declaredLends(mlir::Operation *funcOp) { - mlir::StringAttr field; - // (object literal's storage tuple, field index) of each vtable - llvm::SmallVector, 2> targets; - llvm::SmallVector members; - bool closed = true; - }; - - std::map, InterfaceFieldFamily> interfaceFieldFamilies; - - // interface name -> the tuple types of the vtables its values are made with, when every one of - // them is read here (holdsFunction: what a value of the interface carries) - llvm::StringMap> interfaceVTableTypes; - - // `ts.PropertyRef(ts.Cast(null), i)`, seen through casts: the offset of field `i` of an object of - // that type, as an object literal's interface vtable holds it. Null for anything else. - std::pair slotField(mlir::Value value) + auto lends = declared(funcOp, OWN_DECLARED_LENDS_ATTR_NAME); + llvm::SmallVector sorted(lends.begin(), lends.end()); + llvm::sort(sorted); + sorted.erase(std::unique(sorted.begin(), sorted.end()), sorted.end()); + return sorted; + } + + llvm::SmallVector OwnershipSignaturePass::declaredByCallees(const Callees &callees, llvm::StringRef name) { - while (auto castOp = value ? value.getDefiningOp() : mlir_ts::CastOp()) + llvm::SmallVector agreed; + for (auto [index, symbol] : llvm::enumerate(callees.named)) { - value = castOp.getIn(); - } + // a function, or the global holding the address of one a library exports (a dynamic + // import), which MLIRGen gave the declaration's markers + mlir::Operation *declaring = functions.lookup(symbol.getValue()); + if (!declaring) + { + declaring = globals.lookup(symbol.getValue()); + } - auto propertyRefOp = value ? value.getDefiningOp() : mlir_ts::PropertyRefOp(); - auto nullObject = propertyRefOp ? propertyRefOp.getObjectRef().getDefiningOp() : mlir_ts::CastOp(); - if (!nullObject || !nullObject.getIn().getDefiningOp() || - !mlir::isa(nullObject.getType())) - { - return {}; - } + if (!declaring || !declaring->hasAttr(name)) + { + return {}; + } - auto tupleType = storageTupleOf(nullObject.getType()); - if (!tupleType) - { - return {}; + auto indices = declared(declaring, name); + if (index == 0) + { + agreed.assign(indices.begin(), indices.end()); + } + else if (llvm::ArrayRef(agreed) != indices) + { + return {}; + } } - return {tupleType, static_cast(propertyRefOp.getPosition())}; + return agreed; } - // What every object-literal field an interface's field slot may be holds (InterfaceFieldFamily). - void collectFieldFunctions(mlir::ModuleOp module) + std::string OwnershipSignaturePass::describeParams(mlir_ts::FuncOp funcOp, llvm::ArrayRef indices) { - llvm::DenseMap, llvm::SetVector> stored; - llvm::DenseSet> unknownFields; - llvm::DenseSet unknownTypes; - // a function of unknown origin stored through an interface this pass cannot follow - auto unknownAnywhere = false; - - auto store = [&](std::pair key, mlir::Value value) { - auto symbol = slotFunction(value); - auto funcOp = symbol ? functions.lookup(symbol.getValue()) : mlir_ts::FuncOp(); - if (funcOp && !funcOp.isDeclaration()) + if (indices.empty()) + { + return "nothing"; + } + + auto names = funcOp->getAttrOfType(OWN_DECLARED_PARAM_NAMES_ATTR_NAME); + std::string text; + llvm::raw_string_ostream os(text); + for (auto [position, index] : llvm::enumerate(indices)) + { + os << (position ? ", " : "") << "'"; + if (names && static_cast(index) < names.size()) { - stored[key].insert(symbol); + os << mlir::cast(names[index]).getValue(); } else { - unknownFields.insert(key); + os << "#" << index; } - }; - auto isFunctionType = [](mlir::Type type) { - return mlir::isa(type); - }; + os << "'"; + } - module.walk([&](mlir::Operation *op) { - // the literal's own values - if (auto constantOp = mlir::dyn_cast(op)) + return os.str(); + } + + int32_t OwnershipSignaturePass::parameterOf(mlir::Value value, mlir_ts::FuncOp funcOp) + { + auto root = rootOf(value); + if (auto loadOp = root.getDefiningOp()) + { + if (auto propertyRefOp = loadOp.getReference().getDefiningOp()) { - auto constTupleType = mlir::dyn_cast(constantOp.getType()); - auto values = mlir::dyn_cast(constantOp.getValue()); - if (!constTupleType || !values) - { - return; - } + return parameterOf(propertyRefOp->getOperand(0), funcOp); + } - mlir::Type tupleType = mlir_ts::TupleType::get(&getContext(), constTupleType.getFields()); - for (auto [index, value] : llvm::enumerate(values)) - { - std::pair key{tupleType, static_cast(index)}; - if (auto symbol = mlir::dyn_cast(value)) - { - auto funcOp = functions.lookup(symbol.getValue()); - if (funcOp && !funcOp.isDeclaration()) - { - stored[key].insert(symbol.getAttr()); - } - else - { - unknownFields.insert(key); - } - } - else if (!mlir::isa(value) && index < constTupleType.getFields().size() && - isFunctionType(constTupleType.getFields()[index].type)) - { - unknownFields.insert(key); - } - } + auto varOp = loadOp.getReference().getDefiningOp(); + int index = -1; + if (varOp && isParameterSlot(varOp, index) && varOp->getParentOfType() == funcOp) + { + return index; + } - return; + return -1; + } + + auto argument = mlir::dyn_cast(root); + if (argument && argument.getOwner()->isEntryBlock() && argument.getOwner()->getParentOp() == funcOp.getOperation()) + { + return static_cast(argument.getArgNumber()); + } + + return -1; + } + + void OwnershipSignaturePass::checkDeclared() + { + for (auto &entry : functions) + { + auto funcOp = entry.second; + if (funcOp.isDeclaration()) + { + continue; } - // a store into the field through the object - if (auto propertyRefOp = mlir::dyn_cast(op)) + if (funcOp->hasAttr(OWN_DECLARED_LENDS_ATTR_NAME) && funcOp->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME)) { - auto tupleType = storageTupleOf(propertyRefOp.getObjectRef().getType()); - if (!tupleType) - { - return; - } + funcOp.emitError("'") << funcOp.getSymName() + << "' cannot say both '@" LENDS_DECORATOR "' and '@" BORROWS_DECORATOR + "': its caller owns a result that borrows, and nothing of one it is lent"; + signalPassFailure(); + continue; + } - std::pair key{tupleType, static_cast(propertyRefOp.getPosition())}; - for (auto &use : propertyRefOp->getUses()) + if (declaresCallerFacts(funcOp)) + { + auto local = factsLocal.lookup(funcOp); + auto keeps = declared(funcOp, OWN_DECLARED_KEEPS_ATTR_NAME); + auto lends = declaredLends(funcOp); + if (local.borrows != lends) { - auto *user = use.getOwner(); - if (mlir::isa(user)) + if (lends.empty()) { - continue; + funcOp.emitError("'") << funcOp.getSymName() << "' returns a borrow of " + << describeParams(funcOp, local.borrows) + << ", which it has to say with '@" LENDS_DECORATOR "'"; } - - auto storeOp = mlir::dyn_cast(user); - if (storeOp && storeOp.getReference() == propertyRefOp.getResult() && storeOp.getValue() != propertyRefOp.getResult()) + else if (local.borrows.empty()) { - store(key, storeOp.getValue()); - continue; + funcOp.emitError("'@" LENDS_DECORATOR "' of '") + << funcOp.getSymName() << "' names " << describeParams(funcOp, lends) + << ", but its body does not return a borrow of it"; } - - // an interface vtable's offset of the field (slotField) reads nothing - if (user->getParentOfType() && mlir::isa(user)) + else { - continue; + funcOp.emitError("'@" LENDS_DECORATOR "' of '") + << funcOp.getSymName() << "' names " << describeParams(funcOp, lends) + << ", but its body returns a borrow of " << describeParams(funcOp, local.borrows); } - unknownFields.insert(key); - } - - return; - } - - // a store into the field through an interface: into each literal its vtables reach - if (auto interfaceRefOp = mlir::dyn_cast(op)) - { - auto refType = mlir::dyn_cast(interfaceRefOp.getType()); - if (!refType || !isFunctionType(refType.getElementType())) - { - return; + signalPassFailure(); } - - auto interfaceType = mlir::dyn_cast(interfaceRefOp.getInterfaceVal().getType()); - auto found = interfaceType ? interfaceFieldFamilies.find({interfaceType.getName().getValue().str(), - static_cast(interfaceRefOp.getIndex()) + - INTERFACE_VTABLE_HEADER_SLOTS}) - : interfaceFieldFamilies.end(); - for (auto &use : interfaceRefOp->getUses()) + else if (llvm::ArrayRef(local.owned) != keeps) { - auto *user = use.getOwner(); - if (mlir::isa(user)) + if (funcOp->hasAttr(OWN_DECLARED_KEEPS_ATTR_NAME)) { - continue; + funcOp.emitError("'@" KEEPS_DECORATOR "' of '") + << funcOp.getSymName() << "' names " << describeParams(funcOp, keeps) << ", but its body keeps " + << describeParams(funcOp, local.owned); } - - auto storeOp = mlir::dyn_cast(user); - if (storeOp && storeOp.getReference() == interfaceRefOp.getResult() && found != interfaceFieldFamilies.end() && - found->second.closed) + else { - for (auto target : found->second.targets) - { - store(target, storeOp.getValue()); - } - - continue; + funcOp.emitError("'") << funcOp.getSymName() << "' keeps " << describeParams(funcOp, local.owned) + << ", which it has to say with '@" KEEPS_DECORATOR "'"; } - unknownAnywhere = true; + signalPassFailure(); } - - return; } - // an object of the literal's type made other than by the literal: its fields may hold - // anything. A cast keeps the type's own value (the literal's box seen as the object, an - // object, a reference to its storage) or makes the null object a vtable's offsets are - // taken from; from anything else it reinterprets. - if (auto castOp = mlir::dyn_cast(op)) + if (funcOp->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME)) { - auto tupleType = storageTupleOf(castOp.getType()); - if (tupleType && !mlir::isa(castOp.getIn().getType()) && - storageTupleOf(castOp.getIn().getType()) != tupleType) + auto says = declared(funcOp, OWN_DECLARED_BORROWS_ATTR_NAME); + auto found = borrowsFound.find(funcOp); + if (found == borrowsFound.end()) + { + funcOp.emitError("'@" BORROWS_DECORATOR "' of '") + << funcOp.getSymName() << "' names " << describeParams(funcOp, says) + << ", but it neither makes a generator whose state borrows its arguments nor returns one"; + signalPassFailure(); + } + else if (llvm::ArrayRef(found->second) != says) { - unknownTypes.insert(tupleType); + funcOp.emitError("'@" BORROWS_DECORATOR "' of '") + << funcOp.getSymName() << "' names " << describeParams(funcOp, says) + << ", but the state it makes borrows " << describeParams(funcOp, found->second); + signalPassFailure(); } - - return; - } - - if (mlir::isa(op) || isCall(op) || isView(op) || mlir::isa(op)) - { - return; } + } - for (auto result : op->getResults()) + // the members of a family a declaring method is in: each says the same + for (auto &[key, members] : families) + { + mlir_ts::FuncOp declaring; + for (auto member : members) { - if (mlir::isa(result.getType())) + if (auto funcOp = functions.lookup(member.getValue()); funcOp && declaresFacts(funcOp)) { - if (auto tupleType = storageTupleOf(result.getType())) - { - unknownTypes.insert(tupleType); - } + declaring = funcOp; + break; } } - }); - for (auto &[key, family] : interfaceFieldFamilies) - { - if (!family.closed) + if (!declaring) { continue; } - for (auto target : family.targets) + for (auto member : members) { - if (unknownAnywhere || unknownTypes.contains(target.first) || unknownFields.contains(target)) + auto funcOp = functions.lookup(member.getValue()); + if (!funcOp || funcOp == declaring) { - family.closed = false; - break; + continue; } - for (auto member : stored.lookup(target)) + for (auto *name : {OWN_DECLARED_KEEPS_ATTR_NAME, OWN_DECLARED_BORROWS_ATTR_NAME, OWN_DECLARED_LENDS_ATTR_NAME}) { - if (!llvm::is_contained(family.members, member)) + if (funcOp->hasAttr(name) != declaring->hasAttr(name) || declared(funcOp, name) != declared(declaring, name)) { - family.members.push_back(member); + funcOp.emitError("'") << funcOp.getSymName() << "' overrides '" << declaring.getSymName() + << "', and has to say the same '@" KEEPS_DECORATOR "', '@" BORROWS_DECORATOR + "' and '@" LENDS_DECORATOR "'"; + signalPassFailure(); + break; } } } - - family.closed = family.closed && !family.members.empty(); } } - // The interfaces mentioned by a type as values: the interface itself, a union or optional, a - // tuple's fields, an array's elements, what a reference points to. Not a class's or an object's - // fields (a reference to a block made elsewhere), nor a function's parameters and result. - static void valueInterfaces(mlir::Type type, llvm::StringSet<> &found, int depth = 0) + bool OwnershipSignaturePass::factsThroughInterface(mlir::Operation *funcOp, const Facts &facts) { - if (!type || depth > 16) - { - return; - } - - if (auto interfaceType = mlir::dyn_cast(type)) - { - found.insert(interfaceType.getName().getValue()); - return; - } + return !facts.empty() && interfaceReached.contains(funcOp); + } - auto fields = [&](llvm::ArrayRef infos) { - for (auto &info : infos) + llvm::SmallVector OwnershipSignaturePass::returnedValues(mlir_ts::FuncOp funcOp) + { + llvm::SmallVector values; + funcOp.walk([&](mlir_ts::ReturnInternalOp returnOp) { + for (auto operand : returnOp.getRetOperands()) { - valueInterfaces(info.type, found, depth + 1); - } - }; + auto loadOp = operand.getDefiningOp(); + auto slot = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + if (!slot || slot.getInitializer() || isOwningVariable(slot)) + { + values.push_back(operand); + continue; + } - if (auto tupleType = mlir::dyn_cast(type)) - { - fields(tupleType.getFields()); - } - else if (auto constTupleType = mlir::dyn_cast(type)) - { - fields(constTupleType.getFields()); - } - else if (auto unionType = mlir::dyn_cast(type)) - { - for (auto member : unionType.getTypes()) - { - valueInterfaces(member, found, depth + 1); + for (auto *user : slot.getResult().getUsers()) + { + auto storeOp = mlir::dyn_cast(user); + if (storeOp && storeOp.getReference() == slot.getResult()) + { + values.push_back(storeOp.getValue()); + } + } } - } - else if (auto optionalType = mlir::dyn_cast(type)) - { - valueInterfaces(optionalType.getElementType(), found, depth + 1); - } - else if (auto arrayType = mlir::dyn_cast(type)) - { - valueInterfaces(arrayType.getElementType(), found, depth + 1); - } - else if (auto constArrayType = mlir::dyn_cast(type)) - { - valueInterfaces(constArrayType.getElementType(), found, depth + 1); - } - else if (auto refType = mlir::dyn_cast(type)) - { - valueInterfaces(refType.getElementType(), found, depth + 1); - } - else if (auto valueRefType = mlir::dyn_cast(type)) - { - valueInterfaces(valueRefType.getElementType(), found, depth + 1); - } - else if (auto boundRefType = mlir::dyn_cast(type)) - { - valueInterfaces(boundRefType.getElementType(), found, depth + 1); - } - else if (auto sharedType = mlir::dyn_cast(type)) - { - valueInterfaces(sharedType.getElementType(), found, depth + 1); - } + }); + + return values; } - // Every interface a type mentions anywhere, a class's fields and a function's parameters - // included: what another module handed a value of this type could reach. With `classes`, the - // classes it mentions too (a base class is its derived class's first field). - static void anyInterfaces(mlir::Type type, llvm::StringSet<> &found, llvm::DenseSet &seen, - llvm::StringSet<> *classes = nullptr) + OwnershipSignaturePass::ResultBorrows OwnershipSignaturePass::localResultBorrows(mlir_ts::FuncOp funcOp) { - if (!type || !seen.insert(type).second) + auto found = -1; + auto record = false; + llvm::SmallVector params; + for (auto value : returnedValues(funcOp)) { - return; - } + if (holdsNoBlock(value)) + { + continue; + } - if (auto interfaceType = mlir::dyn_cast(type)) - { - found.insert(interfaceType.getName().getValue()); - return; - } + // a handle returned is a counted copy, never a borrow (spec 23.2) + if (isHandle(value)) + { + return {}; + } - auto fields = [&](llvm::ArrayRef infos) { - for (auto &info : infos) + switch (recordParamsOf(value, params)) { - anyInterfaces(info.type, found, seen, classes); + case RecordFlow::Bad: + return {}; + case RecordFlow::Record: + record = true; + continue; + case RecordFlow::None: + break; } - }; - // the types whose fields are not parameters of the type - if (auto classStorageType = mlir::dyn_cast(type)) - { - if (classes) + auto index = borrowedParam(value); + if (index < 0 || (found >= 0 && index != found)) { - classes->insert(classStorageType.getName().getValue()); + return {}; } - fields(classStorageType.getFields()); - return; + found = index; } - if (auto objectStorageType = mlir::dyn_cast(type)) + if (record && found >= 0) { - fields(objectStorageType.getFields()); - return; + return {}; } - if (auto tupleType = mlir::dyn_cast(type)) + if (record) { - fields(tupleType.getFields()); - return; + llvm::sort(params); + params.erase(std::unique(params.begin(), params.end()), params.end()); + return {params, true}; } - if (auto constTupleType = mlir::dyn_cast(type)) + if (found >= 0) { - fields(constTupleType.getFields()); - return; + return {{found}, false}; } - type.walkImmediateSubElements([](mlir::Attribute) {}, [&](mlir::Type sub) { anyInterfaces(sub, found, seen, classes); }); + return {}; } - // The function values a value of this type holds: the type itself, a union's or an optional's - // member, a tuple's field, an array's element. Not what a reference points to (a vtable's - // address seen as an opaque pointer is the compiler's own, not a value). - static void functionValues(mlir::Type type, llvm::SmallVectorImpl &found, int depth = 0) + int32_t OwnershipSignaturePass::mixedReturnParam(mlir_ts::FuncOp funcOp) { - if (!type || depth > 16) - { - return; - } - - if (mlir::isa(type)) + auto param = -1; + auto other = false; + auto returned = returnedValues(funcOp); + llvm::SmallVector work(returned.begin(), returned.end()); + llvm::DenseSet seen; + while (!work.empty()) { - found.push_back(type); - return; - } - - auto fields = [&](llvm::ArrayRef infos) { - for (auto &info : infos) + auto value = work.pop_back_val(); + if (!seen.insert(value).second || holdsNoBlock(value)) { - functionValues(info.type, found, depth + 1); + continue; } - }; - if (auto tupleType = mlir::dyn_cast(type)) - { - fields(tupleType.getFields()); - } - else if (auto constTupleType = mlir::dyn_cast(type)) - { - fields(constTupleType.getFields()); - } - else if (auto unionType = mlir::dyn_cast(type)) - { - for (auto member : unionType.getTypes()) + if (isHandle(value) || isCopyableString(value.getType())) { - functionValues(member, found, depth + 1); + return -1; } - } - else if (auto optionalType = mlir::dyn_cast(type)) - { - functionValues(optionalType.getElementType(), found, depth + 1); - } - else if (auto arrayType = mlir::dyn_cast(type)) - { - functionValues(arrayType.getElementType(), found, depth + 1); - } - else if (auto constArrayType = mlir::dyn_cast(type)) - { - functionValues(constArrayType.getElementType(), found, depth + 1); - } - else if (auto sharedType = mlir::dyn_cast(type)) - { - functionValues(sharedType.getElementType(), found, depth + 1); - } - } - - // A function type's parameters and results, whatever kind of function it is. - static std::pair, llvm::SmallVector> signatureOf(mlir::Type type) - { - llvm::SmallVector inputs; - llvm::SmallVector results; - if (auto functionType = mlir::dyn_cast(type)) - { - inputs.append(functionType.getInputs().begin(), functionType.getInputs().end()); - results.append(functionType.getResults().begin(), functionType.getResults().end()); - } - else if (auto boundType = mlir::dyn_cast(type)) - { - inputs.append(boundType.getInputs().begin(), boundType.getInputs().end()); - results.append(boundType.getResults().begin(), boundType.getResults().end()); - } - else if (auto hybridType = mlir::dyn_cast(type)) - { - inputs.append(hybridType.getInputs().begin(), hybridType.getInputs().end()); - results.append(hybridType.getResults().begin(), hybridType.getResults().end()); - } - else - { - // a kind this does not read: never the same signature as another - inputs.push_back(type); - } - - return {inputs, results}; - } - - // Does a cast reinterpret a function value - from `any` or `unknown`, or as a function of another - // signature? Then an interface value its parameters or - // results hold crosses unseen (a `(o: Op2) => ...` called as `(o: Op) => ...`): `interfaces` - // gets every interface either side's signatures mention, and `boxed` the input's when it is put - // into `any`. - static bool castReinterpretsFunction(mlir::Type in, mlir::Type out, llvm::StringSet<> &interfaces, bool &boxing) - { - // `!ts.opaque` is the compiler's own (a vtable slot read as the `.instanceOf` it holds), not - // a value a program can put a function into - auto isOpaque = [](mlir::Type type) { return mlir::isa(type); }; - llvm::SmallVector inFunctions; - llvm::SmallVector outFunctions; - functionValues(in, inFunctions); - functionValues(out, outFunctions); - boxing = false; - if ((inFunctions.empty() && outFunctions.empty()) || in == out) - { - return false; - } - auto mention = [&](llvm::ArrayRef types) { - for (auto type : types) + auto root = rootOf(value); + if (auto argument = mlir::dyn_cast(root); argument && !argument.getOwner()->isEntryBlock()) { - llvm::DenseSet seen; - anyInterfaces(type, interfaces, seen); - } - }; - - if (isOpaque(out)) - { - boxing = true; - mention(inFunctions); - return false; - } - - // the same signatures, but for `this` seen as an opaque pointer on one side, or bound on one - // side only (a closure's box, a bound function's object: the caller supplies neither) - auto sameSignature = [](mlir::Type a, mlir::Type b) { - auto [aInputs, aResults] = signatureOf(a); - auto [bInputs, bResults] = signatureOf(b); - auto dropBoundThis = [](mlir::Type type, llvm::SmallVector &inputs) { - if (!inputs.empty() && (mlir::isa(type) || mlir::isa(inputs.front()))) + llvm::SmallVector merged; + if (!mergedInto(argument, merged)) { - inputs.erase(inputs.begin()); + return -1; } - }; - - if (aInputs.size() == bInputs.size() + 1) - { - dropBoundThis(a, aInputs); - } - else if (bInputs.size() == aInputs.size() + 1) - { - dropBoundThis(b, bInputs); - } - if (aResults != bResults || aInputs.size() != bInputs.size()) - { - return false; + work.append(merged.begin(), merged.end()); + continue; } - for (auto [index, pair] : llvm::enumerate(llvm::zip(aInputs, bInputs))) + int32_t index = -1; + if (readsParameterSlot(root, funcOp, index)) { - auto [aInput, bInput] = pair; - if (aInput != bInput && - !(index == 0 && (mlir::isa(aInput) || mlir::isa(bInput)))) + if ((param >= 0 && param != index) || isHandle(root) || isCopyableString(root.getType())) { - return false; + return -1; } - } - return true; - }; + param = index; + continue; + } - // a function out of `any`, or one function seen as another; a function seen as something - // that is no function (a vtable slot's reference to it) is the compiler's own - auto reinterprets = isOpaque(in) && !outFunctions.empty(); - if (!inFunctions.empty() && !outFunctions.empty()) - { - reinterprets = reinterprets || inFunctions.size() != outFunctions.size(); - for (auto [inFunction, outFunction] : llvm::zip(inFunctions, outFunctions)) + llvm::SmallVector recordParams; + if (borrowedParam(root) >= 0 || recordParamsOf(root, recordParams) != RecordFlow::None) { - reinterprets = reinterprets || !sameSignature(inFunction, outFunction); + return -1; } - } - if (reinterprets) - { - mention(inFunctions); - mention(outFunctions); + other = true; } - return reinterprets; - } - - // The interfaces an interface member's type mentions: a method's or a function field's - // parameters and result, a field's type, anything they hold (a class's fields included). - static void memberTypeInterfaces(mlir::Type type, llvm::StringSet<> &found) - { - llvm::DenseSet seen; - anyInterfaces(type, found, seen); - } - - static bool isNullish(mlir::Type type) - { - return mlir::isa(type); + return other ? param : -1; } - // May a cast from `in` to `out` make an interface value out of something that was not that - // interface's value already? A union or optional narrowed to its interface member (whose other - // members are null or undefined) or widened from it, null given a nullable interface type, a - // class or object seen as another (the same block, its fields as they are) and a function seen as - // another are not; a cast from another interface, `any`, an opaque pointer, or a tuple or an - // array whose element is not the same interface is. - static bool castMakesInterface(mlir::Type in, mlir::Type out, int depth = 0) + bool OwnershipSignaturePass::keeps(mlir::Operation *user, mlir::Value used) { - // null seen as an interface value has no vtable: a call through it reaches nothing - if (in == out || depth > 16 || isNullish(in)) - { - return false; - } - - llvm::StringSet<> made; - valueInterfaces(out, made); - if (made.empty()) + // a number, a boolean, a string literal: nothing to keep + if (holdsNoBlock(used)) { return false; } - if (mlir::isa(out)) - { - return !holdsAsIs(in, out, /*onlyNullable=*/true); - } - - if (mlir::isa(out)) + if (auto storeOp = mlir::dyn_cast(user)) { - if (isNullish(in) || holdsAsIs(out, in)) + // a field a borrowing record borrows (spec 32): the record keeps nothing + if (isBorrowedRecordFieldStore(user)) { return false; } - llvm::SmallVector members; - if (auto unionType = mlir::dyn_cast(in)) - { - members.append(unionType.getTypes().begin(), unionType.getTypes().end()); - } - else if (auto optionalType = mlir::dyn_cast(in)) - { - members.push_back(optionalType.getElementType()); - } - else - { - return true; - } - - return !llvm::all_of(members, [&](mlir::Type member) { return isNullish(member) || holdsAsIs(out, member); }); + auto ref = storeOp.getReference(); + return storeOp.getValue() == used && (isPlace(ref) || ref.getDefiningOp()); } - auto tupleFields = [](mlir::Type type) -> std::optional> { - if (auto tupleType = mlir::dyn_cast(type)) - { - return tupleType.getFields(); - } - - if (auto constTupleType = mlir::dyn_cast(type)) - { - return constTupleType.getFields(); - } - - return std::nullopt; - }; - - if (auto outFields = tupleFields(out)) + if (auto pushOp = mlir::dyn_cast(user)) { - auto inFields = tupleFields(in); - if (!inFields || inFields->size() != outFields->size()) - { - return true; - } - - for (auto [inField, outField] : llvm::zip(*inFields, *outFields)) - { - if (castMakesInterface(inField.type, outField.type, depth + 1)) - { - return true; - } - } - - return false; + return llvm::is_contained(pushOp.getItems(), used); } - auto elementOf = [](mlir::Type type) -> mlir::Type { - if (auto arrayType = mlir::dyn_cast(type)) - { - return arrayType.getElementType(); - } - - if (auto constArrayType = mlir::dyn_cast(type)) - { - return constArrayType.getElementType(); - } - - return {}; - }; - - if (auto outElement = elementOf(out)) + // `new Shared(x)` moves `x` into the block, as a store into a place does (spec 23.1) + if (auto sharedNewOp = mlir::dyn_cast(user)) { - auto inElement = elementOf(in); - return !inElement || castMakesInterface(inElement, outElement, depth + 1); + return sharedNewOp.getValue() == used; } - // a reference seen as another (`!ts.bound_ref` as `!ts.ref`): what it points to - auto pointee = [](mlir::Type type) -> mlir::Type { - if (auto refType = mlir::dyn_cast(type)) - { - return refType.getElementType(); - } - - if (auto valueRefType = mlir::dyn_cast(type)) - { - return valueRefType.getElementType(); - } - - if (auto boundRefType = mlir::dyn_cast(type)) - { - return boundRefType.getElementType(); - } - - return {}; - }; + if (auto unshiftOp = mlir::dyn_cast(user)) + { + return llvm::is_contained(unshiftOp.getItems(), used); + } - if (auto outPointee = pointee(out)) + if (auto spliceOp = mlir::dyn_cast(user)) { - auto inPointee = pointee(in); - return !inPointee || castMakesInterface(inPointee, outPointee, depth + 1); + return llvm::is_contained(spliceOp.getItems(), used); } - // anything else reinterpreted: what is read out of it was never checked - return true; - } + if (isCall(user)) + { + auto args = callArgs(user); + return llvm::any_of(ownedParams(user), [&](int32_t index) { + return static_cast(index) < args.size() && args[index] == used; + }); + } - // The function a vtable slot holds: `ts.SymbolRef`, seen through casts. Null for anything else. - static mlir::StringAttr slotFunction(mlir::Value value) - { - while (auto castOp = value ? value.getDefiningOp() : mlir_ts::CastOp()) + // `let r = init; ... r = f(r, v)`: a local that owns what it is declared from and is given + // another value is no borrow of the parameter - the assignment releases what it holds - so + // the parameter moves into it (spec 33: `___array_reduce`'s accumulator). Not a string: the + // local copies it (spec 22) + if (auto varOp = mlir::dyn_cast(user)) { - value = castOp.getIn(); + return varOp.getInitializer() == used && isOwningVariable(varOp) && !varOp.getCaptured().value_or(false) && + isAssigned(varOp) && !isCopyableString(used.getType()); } - auto symbolRefOp = value ? value.getDefiningOp() : mlir_ts::SymbolRefOp(); - return symbolRefOp ? symbolRefOp.getIdentifierAttr().getAttr() : mlir::StringAttr(); + return false; } - static mlir_ts::AddressOfOp addressOfThroughCasts(mlir::Value value) + llvm::SmallVector OwnershipSignaturePass::localOwnedParams(mlir_ts::FuncOp funcOp) { - while (auto castOp = value ? value.getDefiningOp() : mlir_ts::CastOp()) + llvm::SmallVector owned; + if (funcOp.getBody().empty()) { - value = castOp.getIn(); + return owned; } - return value ? value.getDefiningOp() : mlir_ts::AddressOfOp(); - } - - // The words of the module's exported declarations (`__decls*` globals): what another module - // can name. - static llvm::StringSet<> exportedWords(mlir::ModuleOp module) - { - llvm::StringSet<> words; - module.walk([&](mlir_ts::GlobalOp globalOp) { - if (!globalOp.getSymName().starts_with("__decls")) + for (auto argument : funcOp.getBody().front().getArguments()) + { + // a handle kept is a counted copy: its caller gives nothing up (spec 23.2) + if (isHandle(argument)) { - return; + continue; } - globalOp.getInitializerRegion().walk([&](mlir_ts::ConstantOp constantOp) { - auto text = mlir::dyn_cast(constantOp.getValue()); - if (!text) + auto kept = false; + for (auto *user : argument.getUsers()) + { + auto varOp = mlir::dyn_cast(user); + auto index = -1; + if (!varOp || !isParameterSlot(varOp, index)) { - return; + continue; } - llvm::StringRef rest = text.getValue(); - while (!rest.empty()) + for (auto *slotUser : varOp.getResult().getUsers()) { - auto isWordChar = [](char c) { return llvm::isAlnum(c) || c == '_' || c == '$'; }; - rest = rest.drop_while([&](char c) { return !isWordChar(c); }); - auto word = rest.take_while(isWordChar); - if (!word.empty()) + if (auto loadOp = mlir::dyn_cast(slotUser)) { - words.insert(word); + forEachUse(loadOp.getResult(), [&](mlir::Operation *use, mlir::Value used) { + kept = kept || keeps(use, used); + }); } - - rest = rest.drop_front(word.size()); } - }); - }); - - return words; - } - - void collectInterfaceFamilies(mlir::ModuleOp module) - { - llvm::StringSet<> open; - auto openAll = [&](const llvm::StringSet<> &names) { - for (auto &entry : names) - { - open.insert(entry.getKey()); } - }; - // the vtables: an interface's (`ts.InsertProperty` of each slot, slot 0 `.instanceOf` of a - // class that is not the vtable's owner, or `.instanceOf.none` of an object literal), and the - // slots of a class's own vtable that hold the address of one of its interface vtables - struct InterfaceVTable - { - mlir_ts::TupleType type; - llvm::DenseMap slots; - // a field's slot: the object literal's field it is the offset of - llvm::DenseMap> fields; - bool written = false; - }; - - llvm::StringMap vtables; - // `..vtbl` globals that are neither (no `.instanceOf` this reads in slot 0) - llvm::SmallVector unread; - // interface name -> the interface vtables class vtables hold for it, read through an - // object's class at run time (`ts.VTableOffsetRef` of the object's vtable) - llvm::StringMap> classHeld; - module.walk([&](mlir_ts::GlobalOp globalOp) { - auto name = globalOp.getSymName(); - if (!name.ends_with("..vtbl")) + if (kept) { - return; + owned.push_back(argument.getArgNumber()); } + } - auto owner = name.drop_back(llvm::StringRef("..vtbl").size()); - llvm::DenseMap values; - globalOp.getInitializerRegion().walk([&](mlir_ts::InsertPropertyOp insertOp) { - if (insertOp.getPosition().size() == 1) - { - values[insertOp.getPosition()[0]] = insertOp.getValue(); - } - }); + return owned; + } - auto header = slotFunction(values.lookup(0)); - auto tupleType = mlir::dyn_cast(globalOp.getType()); - if (!header || !tupleType) - { - unread.push_back(name); - return; - } + void OwnershipSignaturePass::computeFacts() + { + llvm::DenseMap pinned; + for (auto round = 0; round < 16; ++round) + { + // which object literals borrow depends on what the calls stored into them return, and a + // function's facts on which literals borrow (spec 32) + classifyRecordBoxes(); - if (header.getValue() == (owner + "..instanceOf").str()) + for (auto &entry : functions) { - // an exported class (not in an executable, whose exports no module sees) - auto exposed = !executable && (!globalOp.isPrivate() || globalOp->hasAttr("export") || globalOp->hasAttr(DLL_EXPORT)); - ownClasses.insert(owner); - if (exposed) - { - nameableClasses.insert(owner); - } - - for (auto &[position, value] : values) + if (!entry.second.isDeclaration()) { - auto addressOfOp = addressOfThroughCasts(value); - auto id = position < static_cast(tupleType.size()) - ? mlir::dyn_cast_or_null(tupleType.getFieldInfo(position).id) - : mlir::StringAttr(); - if (!addressOfOp || !id) + auto result = localResultBorrows(entry.second); + Facts facts; + facts.borrows = std::move(result.borrows); + facts.record = result.record; + facts.owned = localOwnedParams(entry.second); + // what the body takes into something on every path; a parameter it returns on + // some paths only is kept too, and released on the others (spec 33) + takenParams[entry.second] = facts.owned; + if (auto mixed = mixedReturnParam(entry.second); mixed >= 0 && !llvm::is_contained(facts.owned, mixed)) { - continue; + facts.owned.insert(llvm::upper_bound(facts.owned, mixed), mixed); } - classHeld[id.getValue()].insert(addressOfOp.getGlobalName()); - if (exposed) - { - open.insert(id.getValue()); // another module may extend the class - } + factsLocal[entry.second] = std::move(facts); } - - return; } - if (!header.getValue().ends_with("..instanceOf") && header.getValue() != ".instanceOf.none") - { - unread.push_back(name); - return; - } + meetFacts(); - auto &vtable = vtables[name]; - vtable.type = tupleType; - for (auto &[position, value] : values) + llvm::DenseMap next; + for (auto &call : calls) { - if (position > 0) + auto facts = callFacts(call.callees); + if (facts.empty()) { - vtable.slots[position] = slotFunction(value); - if (auto field = slotField(value); field.first) - { - vtable.fields[position] = field; - } - } - } - }); - - // interface name -> the vtables its values are made with - llvm::StringMap> madeWith; - auto vtableOf = [&](mlir::Value value) -> llvm::StringRef { - // the global itself - if (auto addressOfOp = addressOfThroughCasts(value)) - { - return vtables.count(addressOfOp.getGlobalName()) ? addressOfOp.getGlobalName() : llvm::StringRef(); - } - - // a copy of it on the heap, its absent members' slots patched - while (auto castOp = value.getDefiningOp()) - { - value = castOp.getIn(); - } - - if (auto newOp = value.getDefiningOp()) - { - llvm::StringRef copied; - for (auto *user : newOp->getUsers()) - { - auto storeOp = mlir::dyn_cast(user); - auto loadOp = storeOp && storeOp.getReference() == newOp.getResult() - ? storeOp.getValue().getDefiningOp() - : mlir_ts::LoadOp(); - auto addressOfOp = loadOp ? addressOfThroughCasts(loadOp.getReference()) : mlir_ts::AddressOfOp(); - if (addressOfOp && vtables.count(addressOfOp.getGlobalName())) - { - if (!copied.empty()) - { - return {}; - } - - copied = addressOfOp.getGlobalName(); - } - } - - return copied; - } - - return {}; - }; - - // an object's class vtable read at run time: `ts.VTableOffsetRef(ts.Load(ts.PropertyRef(object, 0)), k)` - auto readFromClass = [](mlir::Value value) { - auto offsetRefOp = value.getDefiningOp(); - auto loadOp = offsetRefOp ? offsetRefOp.getVtable().getDefiningOp() : mlir_ts::LoadOp(); - auto propertyRefOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); - return propertyRefOp && propertyRefOp.getPosition() == 0 && - mlir::isa(propertyRefOp.getObjectRef().getType()); - }; - - // what reads a vtable's address: making an interface value, a class vtable's slot, the copy - std::function onlyMakesValues = [&](mlir::Value address) { - return llvm::all_of(address.getUses(), [&](mlir::OpOperand &use) { - auto *user = use.getOwner(); - if (auto newInterfaceOp = mlir::dyn_cast(user)) - { - return newInterfaceOp.getInterfaceVTable() == use.get(); - } - - if (mlir::isa(user)) - { - return onlyMakesValues(user->getResult(0)); - } - - if (mlir::isa(user)) - { - return use.getOperandNumber() == 0 && user->getParentOfType() != nullptr; - } - - // the copy: read whole, and stored into a block `ts.New` made - if (auto loadOp = mlir::dyn_cast(user)) - { - return llvm::all_of(loadOp->getUsers(), [&](mlir::Operation *loadUser) { - auto storeOp = mlir::dyn_cast(loadUser); - return storeOp && storeOp.getValue() == loadOp.getResult() && - storeOp.getReference().getDefiningOp(); - }); - } - - return false; - }); - }; - - // An interface value made out of another one reaches the other's members: both are open. One - // made out of `any` or an opaque pointer may be any interface value put into one: every - // interface so boxed is open with it, and every interface at all when it is untraceable. - auto allOpen = false; - llvm::StringSet<> boxed; - llvm::StringSet<> unboxedInto; - auto unboxedFunction = false; - // the interfaces a cast makes a value of out of another interface's or an opaque one, and - // the ones it came from: closed over their members' types below - llvm::StringSet<> castOpened; - // interface -> the interfaces its members' types mention: its vtables' slots and what is - // read out of its values (`ts.InterfaceSymbolRef`), function signatures and class fields - // included - llvm::StringMap> memberInterfaces; - auto isOpaque = [](mlir::Type type) { return mlir::isa(type); }; - module.walk([&](mlir::Operation *op) { - if (auto newInterfaceOp = mlir::dyn_cast(op)) - { - auto name = mlir::cast(newInterfaceOp.getType()).getName().getValue(); - auto vtable = newInterfaceOp.getInterfaceVTable(); - if (auto found = vtableOf(vtable); !found.empty()) - { - madeWith[name].insert(found); - } - else if (readFromClass(vtable)) - { - // any class's vtable for the interface (classHeld, all in its families below) - if (!classHeld.count(name)) - { - open.insert(name); - } - } - else if (auto extractOp = vtable.getDefiningOp()) - { - castOpened.insert(name); - valueInterfaces(extractOp.getInterfaceVal().getType(), castOpened); - } - else - { - allOpen = true; - } - - return; - } - - if (auto castOp = mlir::dyn_cast(op)) - { - auto in = castOp.getIn().getType(); - auto out = castOp.getType(); - if (isOpaque(out)) - { - valueInterfaces(in, boxed); - } - - if (castMakesInterface(in, out)) - { - valueInterfaces(out, castOpened); - valueInterfaces(in, castOpened); - if (isOpaque(in)) - { - valueInterfaces(out, unboxedInto); - } - } - - // a function value reinterpreted carries the interface values of its parameters and - // results across unseen (review6 x1c, x1r) - llvm::StringSet<> signatures; - auto boxing = false; - if (castReinterpretsFunction(in, out, signatures, boxing)) - { - openAll(signatures); - if (isOpaque(in)) - { - unboxedFunction = true; - } - } - else if (boxing) - { - for (auto &entry : signatures) - { - boxed.insert(entry.getKey()); - } - } - - return; - } - - if (auto interfaceRefOp = mlir::dyn_cast(op)) - { - if (!interfaceRefOp->use_empty()) - { - open.insert(interfaceRefOp.getIdentifier()); - } - - return; - } - - if (auto symbolRefOp = mlir::dyn_cast(op)) - { - if (auto interfaceType = mlir::dyn_cast(symbolRefOp.getInterfaceVal().getType())) - { - memberTypeInterfaces(symbolRefOp.getType(), memberInterfaces[interfaceType.getName().getValue()]); - } - - return; - } - - if (auto addressOfOp = mlir::dyn_cast(op)) - { - if (auto found = vtables.find(addressOfOp.getGlobalName()); found != vtables.end() && !onlyMakesValues(addressOfOp.getResult())) - { - found->second.written = true; - } - } - }); - - // seen from another module: a word of its exported declarations, or in a reachable type. An - // executable's exports are seen by no other module (USER decision, spec 31): only what it - // imports - a declared function, an imported global - comes from outside. - if (!executable) - { - exportWords = exportedWords(module); - } - - auto &words = exportWords; - llvm::StringSet<> exposed; - llvm::DenseSet seen; - for (auto &entry : functions) - { - // a `public` method of a class no other module can name is not one: only a declaration - // re-parsed from `__decls` names a function of another module - auto exported = !executable && (entry.second->hasAttr("export") || entry.second->hasAttr(DLL_EXPORT)); - if (entry.second.isDeclaration() || exported) - { - anyInterfaces(entry.second.getFunctionType(), exposed, seen, &nameableClasses); - } - } - - for (auto &entry : globals) - { - auto globalOp = entry.second; - // another module's global, declared here with no initializer (an import of a .ts module - // carries `export`) - auto imported = globalOp->hasAttr("import") || globalOp->hasAttr(DLL_IMPORT) || - globalOp.getInitializerRegion().empty() || globalOp.getInitializerRegion().front().empty(); - auto exported = !globalOp.isPrivate() || globalOp->hasAttr("export") || globalOp->hasAttr(DLL_EXPORT); - if (imported || (exported && !executable)) - { - anyInterfaces(globalOp.getType(), exposed, seen, &nameableClasses); - } - } - - openAll(exposed); - if (!unboxedInto.empty() || unboxedFunction) - { - openAll(boxed); - for (auto &entry : boxed) - { - castOpened.insert(entry.getKey()); - } - } - - // An interface value made out of another one (or out of `any`) is called with the other's - // members: `HasF2 { f: (o: Op2, ...) }` seen as `HasF { f: (o: Op, ...) }` hands an `Op` to a - // function that calls through `Op2` (review6r1 v5, v11f). So every interface either side's - // members mention is open too, and theirs, recursively (controller ruling, spec 31). - // - // A class's vtables for its interfaces are its members' families whether or not this module - // makes an interface value of the class (a method called directly then keeps its facts), and - // their slots' types are the interface's members' types: an interface whose values are all - // made from class objects is read through them (review6r2 d1-d3). - for (auto &[name, held] : classHeld) - { - madeWith[name].insert(held.begin(), held.end()); - } - - for (auto &[name, made] : madeWith) - { - for (auto vtableName : made) - { - if (auto found = vtables.find(vtableName); found != vtables.end() && found->second.type) - { - for (auto [position, field] : llvm::enumerate(found->second.type.getFields())) - { - if (position > 0) - { - memberTypeInterfaces(field.type, memberInterfaces[name]); - } - } - } - } - } - - llvm::SmallVector work; - for (auto &entry : castOpened) - { - work.push_back(entry.getKey().str()); - } - - llvm::StringSet<> expanded; - while (!work.empty()) - { - auto name = work.pop_back_val(); - if (!expanded.insert(name).second) - { - continue; - } - - open.insert(name); - for (auto &member : memberInterfaces[name]) - { - work.push_back(member.getKey().str()); - } - } - - auto isOpen = [&](llvm::StringRef name) { - // a namespaced interface is declared by its last name - auto last = name.rsplit('.').second; - return allOpen || open.contains(name) || words.contains(name) || (!last.empty() && words.contains(last)); - }; - - // A vtable this pass could not read, or one no value of any interface is made with, that is - // named for an interface (`...vtbl`, `...vtbl`): the interface may have an - // implementation outside its families. Belt and braces - every way to make an interface - // value is traced above - but a missed member would get its family's facts. - llvm::DenseSet made; - for (auto &entry : madeWith) - { - made.insert(entry.second.begin(), entry.second.end()); - } - - llvm::SmallVector loose(unread.begin(), unread.end()); - for (auto &entry : vtables) - { - if (!made.contains(entry.getKey())) - { - loose.push_back(entry.getKey()); - } - } - - for (auto &entry : madeWith) - { - auto name = entry.getKey().str(); - if (llvm::any_of(loose, [&](llvm::StringRef vtableName) { - return vtableName.ends_with("." + name + "..vtbl") || vtableName.starts_with(name + "."); - })) - { - open.insert(entry.getKey()); - } - } - - // the families - llvm::DenseSet tied; - for (auto &[name, made] : madeWith) - { - // a class vtable's slot holding a vtable this pass did not read (another module's) - auto allRead = llvm::all_of(made, [&](llvm::StringRef vtableName) { return vtables.count(vtableName); }); - auto closed = !isOpen(name) && allRead; - for (auto vtableName : made) - { - if (!vtables.count(vtableName)) - { - continue; - } - - if (allRead) - { - interfaceVTableTypes[name].push_back(vtables[vtableName].type); - } - - tied.insert(vtableName); - auto &vtable = vtables[vtableName]; - for (auto [position, field] : llvm::enumerate(vtable.type.getFields())) - { - if (position == 0) - { - continue; - } - - auto &family = interfaceFamilies[{name.str(), static_cast(position)}]; - auto id = mlir::dyn_cast_or_null(field.id); - auto member = vtable.slots.lookup(position); - auto funcOp = member ? functions.lookup(member.getValue()) : mlir_ts::FuncOp(); - if (!family.field) - { - family.field = id; - } - - // a member that is not private (a `public` method) is open (findOpen): it has no - // facts to give, and gives the family none - family.closed = family.closed && closed && !vtable.written && id && family.field == id && funcOp && - !funcOp.isDeclaration() && !declaresCallerFacts(funcOp); - if (member && !llvm::is_contained(family.members, member)) - { - family.members.push_back(member); - } - - // the same slot as a field: an object literal's field of function type - auto &fieldFamily = interfaceFieldFamilies[{name.str(), static_cast(position)}]; - auto target = vtable.fields.lookup(position); - auto refType = mlir::dyn_cast(field.type); - if (!fieldFamily.field) - { - fieldFamily.field = id; - } - - fieldFamily.closed = fieldFamily.closed && closed && !vtable.written && id && fieldFamily.field == id && - target.first && refType && mlir::isa(refType.getElementType()); - if (target.first && !llvm::is_contained(fieldFamily.targets, target)) - { - fieldFamily.targets.push_back(target); - } - } - } - } - - collectFieldFunctions(module); - - // what an open family holds, and a vtable no interface value is made with, is reached - // through calls that are not resolved - auto reached = [&](mlir::StringAttr member) { - if (auto funcOp = member ? functions.lookup(member.getValue()) : mlir_ts::FuncOp()) - { - interfaceReached.insert(funcOp); - } - }; - - for (auto &[key, family] : interfaceFamilies) - { - if (!family.closed) - { - llvm::for_each(family.members, reached); - } - } - - for (auto &entry : vtables) - { - if (!tied.contains(entry.getKey())) - { - for (auto &slot : entry.second.slots) - { - reached(slot.second); - } - } - } - } - static constexpr const char *INTERFACE_REACHED_WHY = - "it is reached through an interface, whose calls are not given its facts"; - - // The parameters a function's result borrows and the parameters it keeps (spec 2.4); absent is - // rc's convention. A plain borrow names one parameter; a borrowing record (`record`, spec 32) - // any number, and its caller owns the box. - struct Facts - { - llvm::SmallVector borrows; - llvm::SmallVector owned; - bool record = false; - - bool empty() const - { - return borrows.empty() && owned.empty(); - } - - bool operator==(const Facts &other) const - { - return borrows == other.borrows && owned == other.owned && record == other.record; - } - - bool operator!=(const Facts &other) const - { - return !(*this == other); - } - }; - - // Each function's facts, from its body (local) and as its callers see them (effective: closed, - // and agreed by every family it is in). - llvm::DenseMap factsLocal; - llvm::DenseMap factsEffective; - // The parameters each body takes into something - a field, an element, a global, a keeping call, - // an owning local it assigns (localOwnedParams). A kept parameter that is not one of them is - // released on the paths that do not move it (OWN_PARAMS_RELEASED_ATTR_NAME, spec 33). - llvm::DenseMap> takenParams; - - // ---- Facts a signature says (`@keeps`, `@borrows`, `@lends`) ---- - // - // A function that says them has them whoever calls it: a caller in another module reads them - // from the declaration it re-parses. Its body is checked against them instead (checkDeclared). - - static llvm::ArrayRef declared(mlir::Operation *funcOp, llvm::StringRef name) - { - if (auto attr = funcOp->getAttrOfType(name)) - { - return attr.asArrayRef(); - } - - return {}; - } - - static bool declaresFacts(mlir::Operation *funcOp) - { - return funcOp->hasAttr(OWN_DECLARED_KEEPS_ATTR_NAME) || funcOp->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME) || - funcOp->hasAttr(OWN_DECLARED_LENDS_ATTR_NAME); - } - - // The facts `@keeps` and `@lends` say (Facts): what callers move in, and what the result is a - // borrow of. - static bool declaresCallerFacts(mlir::Operation *funcOp) - { - return funcOp->hasAttr(OWN_DECLARED_KEEPS_ATTR_NAME) || funcOp->hasAttr(OWN_DECLARED_LENDS_ATTR_NAME); - } - - // The parameters `@lends` names: one for a plain borrow, any number for a borrowing record - // (spec 32). - static llvm::SmallVector declaredLends(mlir::Operation *funcOp) - { - auto lends = declared(funcOp, OWN_DECLARED_LENDS_ATTR_NAME); - llvm::SmallVector sorted(lends.begin(), lends.end()); - llvm::sort(sorted); - sorted.erase(std::unique(sorted.begin(), sorted.end()), sorted.end()); - return sorted; - } - - // What every function a call names declares under `name`, when they all declare the same; - // nothing when one of them does not. - llvm::SmallVector declaredByCallees(const Callees &callees, llvm::StringRef name) - { - llvm::SmallVector agreed; - for (auto [index, symbol] : llvm::enumerate(callees.named)) - { - // a function, or the global holding the address of one a library exports (a dynamic - // import), which MLIRGen gave the declaration's markers - mlir::Operation *declaring = functions.lookup(symbol.getValue()); - if (!declaring) - { - declaring = globals.lookup(symbol.getValue()); - } - - if (!declaring || !declaring->hasAttr(name)) - { - return {}; - } - - auto indices = declared(declaring, name); - if (index == 0) - { - agreed.assign(indices.begin(), indices.end()); - } - else if (llvm::ArrayRef(agreed) != indices) - { - return {}; - } - } - - return agreed; - } - - // The parameters, for an error, by the names MLIRGen recorded beside the marker. - static std::string describeParams(mlir_ts::FuncOp funcOp, llvm::ArrayRef indices) - { - if (indices.empty()) - { - return "nothing"; - } - - auto names = funcOp->getAttrOfType(OWN_DECLARED_PARAM_NAMES_ATTR_NAME); - std::string text; - llvm::raw_string_ostream os(text); - for (auto [position, index] : llvm::enumerate(indices)) - { - os << (position ? ", " : "") << "'"; - if (names && static_cast(index) < names.size()) - { - os << mlir::cast(names[index]).getValue(); - } - else - { - os << "#" << index; - } - - os << "'"; - } - - return os.str(); - } - - // Functions this module defines that `@borrows`, and those whose state was found to borrow. - llvm::DenseMap> borrowsFound; - - // The argument of `funcOp` that `value` is, or a field of: its own value, its slot's, or a - // read out of either. -1 for anything else. - static int32_t parameterOf(mlir::Value value, mlir_ts::FuncOp funcOp) - { - auto root = rootOf(value); - if (auto loadOp = root.getDefiningOp()) - { - if (auto propertyRefOp = loadOp.getReference().getDefiningOp()) - { - return parameterOf(propertyRefOp->getOperand(0), funcOp); - } - - auto varOp = loadOp.getReference().getDefiningOp(); - int index = -1; - if (varOp && isParameterSlot(varOp, index) && varOp->getParentOfType() == funcOp) - { - return index; - } - - return -1; - } - - auto argument = mlir::dyn_cast(root); - if (argument && argument.getOwner()->isEntryBlock() && argument.getOwner()->getParentOp() == funcOp.getOperation()) - { - return static_cast(argument.getArgNumber()); - } - - return -1; - } - - // ---- The iterator protocol ---- - // - // `it.next()` returns a `{value, done}` record, and under own the caller owns what it holds, - // whoever implements it: a generator's `next` moves what it yields into the record (a yield of - // a borrow is an error in its body), and any other `next` is a function whose callers this pass - // cannot all see, which may not return a borrow. So the result is fresh even when the call goes - // through the `Iterator` interface, or into another module - the default library's `matchAll`. - - // A call of a function named `next` - read through an interface, out of a field, or by its - // symbol - whose one result is a `{value, done}` record. - static bool isIteratorNext(mlir::Operation *op) - { - if (op->getNumResults() != 1) - { - return false; - } - - auto tupleType = mlir::dyn_cast(op->getResult(0).getType()); - if (!tupleType || tupleType.getFields().size() != 2) - { - return false; - } - - auto fieldName = [&](int index) { - auto id = mlir::dyn_cast_or_null(tupleType.getFields()[index].id); - return id ? id.getValue() : llvm::StringRef(); - }; - if (fieldName(0) != "value" || fieldName(1) != "done") - { - return false; - } - - auto isNext = [](llvm::StringRef name) { return name == "next" || name.ends_with(".next"); }; - if (auto callee = directCallee(op)) - { - return isNext(callee.getValue()); - } - - auto function = calleeValue(op); - if (!function) - { - return false; - } - - if (auto getMethodOp = function.getDefiningOp()) - { - function = getMethodOp.getBoundFunc(); - } - - if (auto boundOp = function.getDefiningOp()) - { - function = boundOp.getFunc(); - } - - if (auto loadOp = function.getDefiningOp()) - { - if (auto refOp = loadOp.getReference().getDefiningOp()) - { - auto identifier = mlir::dyn_cast(refOp.getIdentifier()); - return identifier && isNext(identifier.getValue()); - } - - if (auto propertyRefOp = loadOp.getReference().getDefiningOp()) - { - auto storageType = storageTupleOfType(propertyRefOp->getOperand(0).getType()); - if (!storageType || propertyRefOp.getPosition() >= storageType.getFields().size()) - { - return false; - } - - auto id = mlir::dyn_cast_or_null(storageType.getFields()[propertyRefOp.getPosition()].id); - return id && isNext(id.getValue()); - } - - return false; - } - - if (auto refOp = function.getDefiningOp()) - { - return isNext(refOp.getIdentifier()); - } - - if (auto refOp = function.getDefiningOp()) - { - return isNext(refOp.getIdentifier()); - } - - if (auto refOp = function.getDefiningOp()) - { - return isNext(refOp.getIdentifier()); - } - - if (auto refOp = function.getDefiningOp()) - { - return isNext(refOp.getIdentifier()); - } - - return false; - } - - // A generator another module made: its state object's type is only its shape here, and what each - // field owns - a local, a yielded value now the caller's, what it borrows from its arguments - is - // known in that module alone. A release routine made here from the shape would give back what is - // not the state's. So this module's gives back none of it: only the `.captured` box's cells, and - // their values too unless the maker says it borrows them (`@borrows`). What the generator's own - // locals still hold when it is given up early leaks rather than being freed twice. - void findImportedStates(mlir::ModuleOp module) - { - llvm::MapVector states; - for (auto &call : calls) - { - if (!makesGeneratorState(call.op) || (call.callees.known && !call.callees.funcs.empty())) - { - continue; - } - - auto tupleType = storageTupleOfType(call.op->getResult(0).getType()); - auto borrows = !declaredByCallees(call.callees, OWN_DECLARED_BORROWS_ATTR_NAME).empty(); - states[tupleType] = states.lookup(tupleType) || borrows; - } - - if (states.empty()) - { - return; - } - - auto *context = &getContext(); - llvm::SmallVector entries; - if (auto existing = module->getAttrOfType(OWN_BORROWING_FIELDS_ATTR_NAME)) - { - entries.append(existing.begin(), existing.end()); - } - - for (auto &[type, borrows] : states) - { - auto tupleType = mlir::cast(type); - auto captured = fieldIndex(tupleType, CAPTURED_NAME); - // the box first, when it borrows; -1 names no field, so a box that owns is released - llvm::SmallVector fields{borrows && captured >= 0 ? captured : -1}; - for (auto [index, field] : llvm::enumerate(tupleType.getFields())) - { - if (static_cast(index) != captured && ownsHeap(module.getLoc(), field.type)) - { - fields.push_back(static_cast(index)); - } - } - - entries.push_back(mlir::ArrayAttr::get(context, {mlir::TypeAttr::get(type), mlir::DenseI32ArrayAttr::get(context, fields)})); - } - - module->setAttr(OWN_BORROWING_FIELDS_ATTR_NAME, mlir::ArrayAttr::get(context, entries)); - } - - // A call whose result is a generator's state object (an object whose fields are `.step` and - // `next`): a generator function made it, a `ts.New` block it hands over (spec 18.1), which the - // caller owns - in another module too, as the iteration that follows needs. - static bool makesGeneratorState(mlir::Operation *op) - { - if (op->getNumResults() != 1 || !mlir::isa(op->getResult(0).getType())) - { - return false; - } - - auto tupleType = storageTupleOfType(op->getResult(0).getType()); - if (!tupleType) - { - return false; - } - - auto hasStep = false; - auto hasNext = false; - for (auto field : tupleType.getFields()) - { - if (auto id = mlir::dyn_cast_or_null(field.id)) - { - hasStep = hasStep || id.getValue() == ".step"; - hasNext = hasNext || id.getValue() == "next"; - } - } - - return hasStep && hasNext; - } - - // storageTupleOf, without the pass's context: an object's storage, a reference's or a tuple - static mlir_ts::TupleType storageTupleOfType(mlir::Type type) - { - if (auto objectType = mlir::dyn_cast(type)) - { - if (auto storageType = mlir::dyn_cast(objectType.getStorageType())) - { - return mlir_ts::TupleType::get(type.getContext(), storageType.getFields()); - } - - // an object a declaration names by its shape (another module's generator) - return mlir::dyn_cast(objectType.getStorageType()); - } - - if (auto refType = mlir::dyn_cast(type)) - { - type = refType.getElementType(); - } - else if (auto valueRefType = mlir::dyn_cast(type)) - { - type = valueRefType.getElementType(); - } - - return mlir::dyn_cast(type); - } - - // ---- Fields a generator yields ---- - // - // A generator's locals are fields of its state object, and `yield groups` puts what one holds into - // the `{value, done}` record its `next` returns: a borrow of the state, which the caller cannot - // keep. When the field is read for nothing but yields, and each value stored into it is yielded - // before the generator can stop (later in the same block), the yield moves the value instead: - // the read is fresh (OWN_FRESH_RESULT_ATTR_NAME) and the field owns nothing - // (OWN_YIELDED_FIELDS_ATTR_NAME). - - // `ts.Store(value, ts.PropertyRef(record, i))`, where `record` is a local a `ts.Constant` - // initializes: a field of a record built to be returned, a yield's. - static bool isRecordFieldStore(mlir::Operation *user, mlir::Value value) - { - auto storeOp = mlir::dyn_cast(user); - auto propertyRefOp = storeOp && storeOp.getValue() == value - ? storeOp.getReference().getDefiningOp() - : mlir_ts::PropertyRefOp(); - auto recordOp = propertyRefOp ? propertyRefOp->getOperand(0).getDefiningOp() : mlir_ts::VariableOp(); - return recordOp && recordOp.getInitializer() && recordOp.getInitializer().getDefiningOp(); - } - - void findYieldedFields(mlir::ModuleOp module) - { - // (state type, field) -> its reads and its stores - struct FieldUses - { - llvm::SmallVector reads; - llvm::SmallVector stores; - bool other = false; - }; - - llvm::MapVector, FieldUses> fields; - module.walk([&](mlir_ts::PropertyRefOp propertyRefOp) { - auto tupleType = storageTupleOf(propertyRefOp->getOperand(0).getType()); - auto position = static_cast(propertyRefOp.getPosition()); - if (!tupleType || fieldIndex(tupleType, ".step") < 0 || position == fieldIndex(tupleType, CAPTURED_NAME) || - !mlir::isa(propertyRefOp.getType()) || - !ownsHeap(propertyRefOp.getLoc(), mlir::cast(propertyRefOp.getType()).getElementType())) - { - return; - } - - auto &uses = fields[{tupleType, position}]; - for (auto *user : propertyRefOp->getUsers()) - { - if (auto loadOp = mlir::dyn_cast(user)) - { - uses.reads.push_back(loadOp); - } - else if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getReference() == propertyRefOp.getResult()) - { - uses.stores.push_back(storeOp); - } - else if (!mlir::isa(user)) - { - uses.other = true; - } - } - }); - - // a field the signature pass already found borrowing is the caller's, not the generator's - llvm::DenseSet> borrowing; - if (auto entries = module->getAttrOfType(OWN_BORROWING_FIELDS_ATTR_NAME)) - { - for (auto entry : entries.getAsRange()) - { - for (auto index : mlir::cast(entry[1]).asArrayRef()) - { - borrowing.insert({mlir::cast(entry[0]).getValue(), index}); - } - } - } - - llvm::MapVector> yielded; - for (auto &[key, uses] : fields) - { - if (uses.other || uses.reads.empty() || uses.stores.empty() || borrowing.contains(key)) - { - continue; - } - - // read only to be put into a yield's record - auto onlyYielded = llvm::all_of(uses.reads, [](mlir_ts::LoadOp loadOp) { - auto users = llvm::make_filter_range(loadOp->getUsers(), [](mlir::Operation *user) { - return !mlir::isa(user); - }); - return llvm::hasSingleElement(users) && isRecordFieldStore(*users.begin(), loadOp.getResult()); - }); - - // each value stored is yielded later in its block: nothing between stops the generator - auto eachYielded = llvm::all_of(uses.stores, [&](mlir_ts::StoreOp storeOp) { - return llvm::any_of(uses.reads, [&](mlir_ts::LoadOp loadOp) { - return loadOp->getBlock() == storeOp->getBlock() && storeOp->isBeforeInBlock(loadOp); - }); - }); - - if (!onlyYielded || !eachYielded) - { - continue; - } - - yielded[key.first].push_back(key.second); - for (auto loadOp : uses.reads) - { - loadOp->setAttr(OWN_FRESH_RESULT_ATTR_NAME, mlir::UnitAttr::get(&getContext())); - } - } - - if (yielded.empty()) - { - return; - } - - auto *context = &getContext(); - llvm::SmallVector entries; - for (auto &[type, indices] : yielded) - { - entries.push_back(mlir::ArrayAttr::get(context, {mlir::TypeAttr::get(type), mlir::DenseI32ArrayAttr::get(context, indices)})); - } - - module->setAttr(OWN_YIELDED_FIELDS_ATTR_NAME, mlir::ArrayAttr::get(context, entries)); - } - - // A function that says `@borrows` and makes no state itself, but returns what a call that has - // a bounded result gives it (`return this.value.matchAll(expr)`): it borrows the parameters - // the call's bounded arguments are, or are read out of. - void findBoundedForwarders() - { - llvm::DenseMap calleesOf; - for (auto &call : calls) - { - calleesOf[call.op] = &call.callees; - } - - for (auto &entry : functions) - { - auto funcOp = entry.second; - if (funcOp.isDeclaration() || !funcOp->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME) || borrowsFound.count(funcOp)) - { - continue; - } - - llvm::SmallVector found; - auto forwards = true; - for (auto value : returnedValues(funcOp)) - { - if (holdsNoBlock(value)) - { - continue; - } - - auto *call = rootOf(value).getDefiningOp(); - auto known = call ? calleesOf.find(call) : calleesOf.end(); - if (known == calleesOf.end()) - { - forwards = false; - break; - } - - llvm::SmallVector bounded = declaredByCallees(*known->second, OWN_DECLARED_BORROWS_ATTR_NAME); - if (bounded.empty() && known->second->funcs.size() == 1 && !known->second->viaCallback) - { - auto made = declared(known->second->funcs.front(), OWN_RESULT_BOUNDED_ATTR_NAME); - bounded.assign(made.begin(), made.end()); - } - - if (bounded.empty()) - { - forwards = false; - break; - } - - auto args = callArgs(call); - for (auto index : bounded) - { - auto param = static_cast(index) < args.size() ? parameterOf(args[index], funcOp) : -1; - if (param < 0) - { - forwards = false; - break; - } - - if (!llvm::is_contained(found, param)) - { - found.push_back(param); - } - } - } - - if (forwards && !found.empty()) - { - llvm::sort(found); - borrowsFound[funcOp] = found; - } - } - } - - // A body that does other than its signature says, and an override that says other than the - // method it overrides: either way a caller that trusts the signature frees twice or leaks. - void checkDeclared() - { - for (auto &entry : functions) - { - auto funcOp = entry.second; - if (funcOp.isDeclaration()) - { - continue; - } - - if (funcOp->hasAttr(OWN_DECLARED_LENDS_ATTR_NAME) && funcOp->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME)) - { - funcOp.emitError("'") << funcOp.getSymName() - << "' cannot say both '@" LENDS_DECORATOR "' and '@" BORROWS_DECORATOR - "': its caller owns a result that borrows, and nothing of one it is lent"; - signalPassFailure(); - continue; - } - - if (declaresCallerFacts(funcOp)) - { - auto local = factsLocal.lookup(funcOp); - auto keeps = declared(funcOp, OWN_DECLARED_KEEPS_ATTR_NAME); - auto lends = declaredLends(funcOp); - if (local.borrows != lends) - { - if (lends.empty()) - { - funcOp.emitError("'") << funcOp.getSymName() << "' returns a borrow of " - << describeParams(funcOp, local.borrows) - << ", which it has to say with '@" LENDS_DECORATOR "'"; - } - else if (local.borrows.empty()) - { - funcOp.emitError("'@" LENDS_DECORATOR "' of '") - << funcOp.getSymName() << "' names " << describeParams(funcOp, lends) - << ", but its body does not return a borrow of it"; - } - else - { - funcOp.emitError("'@" LENDS_DECORATOR "' of '") - << funcOp.getSymName() << "' names " << describeParams(funcOp, lends) - << ", but its body returns a borrow of " << describeParams(funcOp, local.borrows); - } - - signalPassFailure(); - } - else if (llvm::ArrayRef(local.owned) != keeps) - { - if (funcOp->hasAttr(OWN_DECLARED_KEEPS_ATTR_NAME)) - { - funcOp.emitError("'@" KEEPS_DECORATOR "' of '") - << funcOp.getSymName() << "' names " << describeParams(funcOp, keeps) << ", but its body keeps " - << describeParams(funcOp, local.owned); - } - else - { - funcOp.emitError("'") << funcOp.getSymName() << "' keeps " << describeParams(funcOp, local.owned) - << ", which it has to say with '@" KEEPS_DECORATOR "'"; - } - - signalPassFailure(); - } - } - - if (funcOp->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME)) - { - auto says = declared(funcOp, OWN_DECLARED_BORROWS_ATTR_NAME); - auto found = borrowsFound.find(funcOp); - if (found == borrowsFound.end()) - { - funcOp.emitError("'@" BORROWS_DECORATOR "' of '") - << funcOp.getSymName() << "' names " << describeParams(funcOp, says) - << ", but it neither makes a generator whose state borrows its arguments nor returns one"; - signalPassFailure(); - } - else if (llvm::ArrayRef(found->second) != says) - { - funcOp.emitError("'@" BORROWS_DECORATOR "' of '") - << funcOp.getSymName() << "' names " << describeParams(funcOp, says) - << ", but the state it makes borrows " << describeParams(funcOp, found->second); - signalPassFailure(); - } - } - } - - // the members of a family a declaring method is in: each says the same - for (auto &[key, members] : families) - { - mlir_ts::FuncOp declaring; - for (auto member : members) - { - if (auto funcOp = functions.lookup(member.getValue()); funcOp && declaresFacts(funcOp)) - { - declaring = funcOp; - break; - } - } - - if (!declaring) - { - continue; - } - - for (auto member : members) - { - auto funcOp = functions.lookup(member.getValue()); - if (!funcOp || funcOp == declaring) - { - continue; - } - - for (auto *name : {OWN_DECLARED_KEEPS_ATTR_NAME, OWN_DECLARED_BORROWS_ATTR_NAME, OWN_DECLARED_LENDS_ATTR_NAME}) - { - if (funcOp->hasAttr(name) != declaring->hasAttr(name) || declared(funcOp, name) != declared(declaring, name)) - { - funcOp.emitError("'") << funcOp.getSymName() << "' overrides '" << declaring.getSymName() - << "', and has to say the same '@" KEEPS_DECORATOR "', '@" BORROWS_DECORATOR - "' and '@" LENDS_DECORATOR "'"; - signalPassFailure(); - break; - } - } - } - } - } - - // Functions that destroy nothing their caller can reach. - llvm::DenseSet noDrops; - - // A class's vtable is a global named `..vtbl` that holds its `..instanceOf`. An - // interface's vtable for a class (`B.I..vtbl`) holds only the methods and is not one: a call - // through an interface does not name what it reaches. - void collectClassVTables(mlir::ModuleOp module) - { - llvm::SmallVector>>> vtables; - module.walk([&](mlir_ts::GlobalOp globalOp) { - auto entries = classVTableEntries(globalOp); - if (!entries.empty()) - { - auto name = globalOp.getSymName(); - classNames.insert(name.drop_back(llvm::StringRef("..vtbl").size())); - vtables.push_back({name, std::move(entries)}); - } - }); - - for (auto &[name, entries] : vtables) - { - for (auto [position, symbol] : entries) - { - families[{position, methodName(symbol.getValue())}].push_back(symbol); - } - } - } - - // Does a method reached through an open interface keep a parameter or return a borrow - // (interfaceReached)? Then its callers through the interface cannot be told, and it has no facts - // at all. - bool factsThroughInterface(mlir::Operation *funcOp, const Facts &facts) - { - return !facts.empty() && interfaceReached.contains(funcOp); - } - - mlir_ts::TupleType storageTupleOf(mlir::Type type) - { - return own_facts::storageTupleOf(&getContext(), type); - } - - // The functions a class vtable holds, by position; empty for any other global. - static llvm::SmallVector> classVTableEntries(mlir_ts::GlobalOp globalOp) - { - llvm::SmallVector> entries; - if (!globalOp.getSymName().ends_with("..vtbl")) - { - return entries; - } - - auto isClass = false; - globalOp.getInitializerRegion().walk([&](mlir_ts::InsertPropertyOp insertOp) { - auto symbolRefOp = insertOp.getValue().getDefiningOp(); - if (!symbolRefOp || insertOp.getPosition().size() != 1) - { - return; - } - - auto symbol = symbolRefOp.getIdentifierAttr().getAttr(); - isClass = isClass || symbol.getValue().ends_with("..instanceOf"); - entries.push_back({insertOp.getPosition()[0], symbol}); - }); - - if (!isClass) - { - entries.clear(); - } - - return entries; - } - - static bool isClassVTableEntry(mlir::Operation *op) - { - auto globalOp = op->getParentOfType(); - return globalOp && !classVTableEntries(globalOp).empty(); - } - - // `B.get` -> `get`: the symbol with the longest class name that prefixes it taken off. An - // override keeps its base's method name, which is what puts both in one family. - std::string methodName(llvm::StringRef symbol) - { - size_t best = 0; - for (auto &entry : classNames) - { - auto name = entry.getKey(); - if (name.size() > best && symbol.size() > name.size() && symbol.starts_with(name) && - symbol[name.size()] == '.') - { - best = name.size() + 1; - } - } - - return symbol.drop_front(best).str(); - } - - llvm::SmallVector familyOf(int64_t index, mlir::StringAttr identifier) - { - llvm::SmallVector members; - if (auto found = families.find({index, methodName(identifier.getValue())}); found != families.end()) - { - members = found->second; - } - - if (!llvm::is_contained(members, identifier)) - { - members.push_back(identifier); - } - - return members; - } - - // Adds `symbol`'s function to `callees`; false when it is not defined in this module. - bool addDefined(mlir::StringAttr symbol, Callees &callees) - { - auto funcOp = functions.lookup(symbol.getValue()); - if (!funcOp || funcOp.isDeclaration()) - { - return false; - } - - if (!llvm::is_contained(callees.funcs, funcOp)) - { - callees.funcs.push_back(funcOp); - } - - return true; - } - - // A function declared here and defined in a library that exported its `__own_no_drops`: one - // linked with this module, through its import library. Or a C function declared - // `@nodrops` (OWN_DECLARED_NO_DROPS_ATTR_NAME), or one whose parameters and result hold no - // heap block (scalarDeclaration). - bool addImported(mlir::StringAttr symbol, Callees &callees) - { - auto funcOp = functions.lookup(symbol.getValue()); - if (!funcOp || !funcOp.isDeclaration() || - !(importedNoDrops.contains(symbol.getValue()) || funcOp->hasAttr(OWN_DECLARED_NO_DROPS_ATTR_NAME) || - (scalarDeclaration(funcOp) && !functionsEscape))) - { - return false; - } - - callees.imported = true; - callees.importedName = symbol.getValue(); - return true; - } - - // A number, a boolean, a character, or an enum or a literal of one: a value that holds no block - // and no function of this module, so given to foreign code it hands it neither. Not an opaque - // pointer, a reference, a string, a class, an interface, an object, a tuple, an array, a union - // with such a member, a function, `any` or `unknown`. - static bool scalarType(mlir::Type type) - { - if (auto literalType = mlir::dyn_cast(type)) - { - return scalarType(literalType.getElementType()); - } - - if (auto enumType = mlir::dyn_cast(type)) - { - return scalarType(enumType.getElementType()); - } - - return mlir::isa(type); - } - - // Every parameter and result scalar (a void result too), and no rest parameter. - static bool scalarSignature(mlir_ts::FunctionType functionType) - { - return !functionType.isVarArg() && llvm::all_of(functionType.getInputs(), scalarType) && - llvm::all_of(functionType.getResults(), - [](mlir::Type type) { return mlir::isa(type) || scalarType(type); }); - } - - // A body-less `declare function` of C (no `export` or import marker: not another TypeScript - // module's, whose declarations carry `export`) whose parameters and result are scalar - // (scalarType): given no block of this module, it can destroy none (controller ruling, spec 31: - // `sqrt`, `floor`, `pow`, `fabs`). The risk is a C function that calls back into the module - // (`signal(15, f as Opaque)` then `raise(15)`, review6r1 c3), which it can only do with a - // function it was given - so the rule is off in a module whose foreign surface could hand it - // one (functionsEscape) - or an exported function reached by name, which a `@nodrops` - // declaration takes on as well. - static bool scalarDeclaration(mlir_ts::FuncOp funcOp) - { - return funcOp.isDeclaration() && !funcOp->hasAttr("export") && !funcOp->hasAttr("import") && - !funcOp->hasAttr(DLL_EXPORT) && !funcOp->hasAttr(DLL_IMPORT) && scalarSignature(funcOp.getFunctionType()); - } - - // `ts.Load(ts.AddressOf @f)`, where the global `f` holds the address of a function exported - // by a library loaded at run time, looked up by name: that function, if the library exported - // its `__own_no_drops`. - bool addImportedByAddress(mlir::Value callee, Callees &callees) - { - auto loadOp = callee.getDefiningOp(); - auto addressOfOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::AddressOfOp(); - if (!addressOfOp) - { - return false; - } - - auto globalOp = globals.lookup(addressOfOp.getGlobalName()); - if (!globalOp || writtenGlobals.contains(addressOfOp.getGlobalName())) - { - return false; - } - - // the global is never anything but that address: `Cast(SearchForAddressOfSymbol(name))` - auto ®ion = globalOp.getInitializerRegion(); - if (!region.hasOneBlock()) - { - return false; - } - - auto resultOp = mlir::dyn_cast(region.front().getTerminator()); - auto castOp = resultOp && resultOp->getNumOperands() == 1 - ? resultOp->getOperand(0).getDefiningOp() - : mlir_ts::CastOp(); - auto searchOp = castOp ? castOp.getIn().getDefiningOp() - : mlir_ts::SearchForAddressOfSymbolOp(); - auto nameOp = searchOp ? searchOp->getOperand(0).getDefiningOp() : mlir_ts::ConstantOp(); - auto name = nameOp ? mlir::dyn_cast(nameOp.getValue()) : mlir::StringAttr(); - if (!name || !importedNoDrops.contains(name.getValue())) - { - return false; - } - - callees.imported = true; - callees.importedName = name.getValue(); - return true; - } - - Callees resolve(mlir::Operation *op) - { - Callees callees; - if (auto callee = directCallee(op)) - { - callees.named.push_back(callee.getAttr()); - callees.known = addDefined(callee.getAttr(), callees) || addImported(callee.getAttr(), callees); - } - else if (mlir::isa(op)) - { - resolveValue(calleeValue(op), callees); - } - - if (!callees.known) - { - callees.funcs.clear(); - } - - return callees; - } - - // The function value a call goes through: a method of a class, virtual or not, a symbol, or - // the `.instanceOf` slot. Anything else - a closure, a function read from a field, an - // interface's method - is unknown. - void resolveValue(mlir::Value callee, Callees &callees) - { - // `ts.Load(ts.AddressOf @g)`: a dynamic import's global, which may say what the function - // keeps; a method of a class imported so is bound to its object first - auto function = callee; - if (auto getMethodOp = function.getDefiningOp()) - { - function = getMethodOp.getBoundFunc(); - } - - if (auto boundOp = function.getDefiningOp()) - { - function = boundOp.getFunc(); - } - - auto loadOp = function.getDefiningOp(); - if (auto addressOfOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::AddressOfOp(); - addressOfOp && !writtenGlobals.contains(addressOfOp.getGlobalName())) - { - callees.named.push_back(addressOfOp.getGlobalNameAttr().getAttr()); - } - - if (addImportedByAddress(callee, callees)) - { - callees.known = true; - return; - } - - if (auto getMethodOp = callee.getDefiningOp()) - { - callee = getMethodOp.getBoundFunc(); - } - - auto *def = callee.getDefiningOp(); - mlir::StringAttr identifier; - int64_t index = -1; - if (auto refOp = mlir::dyn_cast_or_null(def)) - { - identifier = refOp.getIdentifierAttr().getAttr(); - index = refOp.getIndex(); - } - else if (auto refOp = mlir::dyn_cast_or_null(def)) - { - identifier = refOp.getIdentifierAttr().getAttr(); - index = refOp.getIndex(); - } - else if (auto refOp = mlir::dyn_cast_or_null(def)) - { - identifier = refOp.getIdentifierAttr().getAttr(); - } - else if (auto refOp = mlir::dyn_cast_or_null(def)) - { - identifier = refOp.getIdentifierAttr().getAttr(); - } - else if (isInstanceOfSlot(def)) - { - callees.known = true; - callees.instanceOf = true; - return; - } - else if (auto symbol = fieldFunctions.of(def)) - { - callees.known = addDefined(symbol, callees); - callees.fieldFunction = callees.known; - return; - } - else if (auto interfaceRefOp = mlir::dyn_cast_or_null(def)) - { - resolveInterfaceSlot(interfaceRefOp, interfaceFamilies, callees); - return; - } - else if (auto loadOp = mlir::dyn_cast_or_null(def)) - { - resolveCallback(loadOp, callees); - return; - } - else if (auto boundOp = mlir::dyn_cast_or_null(def)) - { - // a function-typed field of an interface, called: `ts.CreateBoundFunction( - // ts.ExtractInterfaceThis(i), ts.Load(ts.InterfaceSymbolRef(i, f)))` - auto loadOp = boundOp.getFunc().getDefiningOp(); - auto refOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::InterfaceSymbolRefOp(); - auto thisOp = boundOp.getThisVal().getDefiningOp(); - if (refOp && isInterfaceField(refOp.getResult()) && thisOp && thisOp.getInterfaceVal() == refOp.getInterfaceVal()) - { - resolveInterfaceSlot(refOp, interfaceFieldFamilies, callees); - } - - return; - } - - if (!identifier) - { - return; - } - - if (index < 0) - { - callees.named.push_back(identifier); - callees.known = addDefined(identifier, callees) || addImported(identifier, callees); - return; - } - - callees.named = familyOf(index, identifier); - resolveFamily(index, identifier, callees); - } - - // A method of a closed interface (interfaceFamilies), `ts.InterfaceSymbolRef(value, index)`, or a - // function-typed field of one (interfaceFieldFamilies), read and called: the family of the slot - // after the vtable's header - every function the vtables hold there, or every function the - // implementing literals' fields are ever given. Open, the call is unknown, as before. - template - void resolveInterfaceSlot(mlir_ts::InterfaceSymbolRefOp refOp, const std::map, Family> &families, - Callees &callees) - { - auto interfaceType = mlir::dyn_cast(refOp.getInterfaceVal().getType()); - auto identifier = mlir::dyn_cast(refOp.getIdentifier()); - if (!interfaceType || !identifier) - { - return; - } - - auto position = static_cast(refOp.getIndex()) + INTERFACE_VTABLE_HEADER_SLOTS; - auto found = families.find({interfaceType.getName().getValue().str(), position}); - if (found == families.end() || !found->second.closed || found->second.field != identifier || - found->second.members.empty()) - { - return; - } - - callees.named.assign(found->second.members.begin(), found->second.members.end()); - callees.viaInterface = true; - callees.known = true; - for (auto member : found->second.members) - { - callees.known = addDefined(member, callees) && callees.known; - } - } - - // Can a module other than this one call the function? A `public` method (not private, not - // exported itself) of a class no other module can name - not exported, not a word of the - // exported declarations, not in the type of an exported or declared function or of an exported - // or imported global (nameableClasses) - no: another module calls a function of this one through - // the declaration `__decls` gives it (controller ruling, spec 31). But a method of a class that - // extends a class another module can name, or one another module defines, yes, private or not: - // that module calls it through the base's vtable. - bool callableFromOutside(mlir_ts::FuncOp funcOp) - { - if (!funcOp.isPrivate() && (funcOp.isDeclaration() || funcOp->hasAttr("export") || funcOp->hasAttr(DLL_EXPORT))) - { - return true; - } - - // its class: the longest own class name that prefixes its symbol - auto symbol = funcOp.getSymName(); - llvm::StringRef owner; - for (auto &entry : ownClasses) - { - auto name = entry.getKey(); - if (name.size() > owner.size() && symbol.size() > name.size() && symbol.starts_with(name) && symbol[name.size()] == '.') - { - owner = name; - } - } - - if (owner.empty()) - { - return !funcOp.isPrivate(); - } - - auto nameable = [&](llvm::StringRef name) { - auto last = name.rsplit('.').second; - return nameableClasses.contains(name) || exportWords.contains(name) || (!last.empty() && exportWords.contains(last)); - }; - - if (!funcOp.isPrivate() && nameable(owner)) - { - return true; - } - - // An override of a method of a nameable class, or of one another module defines (no own - // vtable here), fills a slot of that class's vtable: another module calls it through the - // base (review6r1 p5, p6, p7; controller ruling: any base of that kind on the chain). The - // base is the first field of a derived class's storage. - auto storage = classStorages.lookup(owner); - if (!storage) - { - // its storage is not known here: no telling what it extends - return !funcOp.isPrivate(); - } - - for (auto depth = 0; depth < 64; ++depth) - { - auto fields = storage.getFields(); - auto base = fields.empty() ? mlir_ts::ClassStorageType() : mlir::dyn_cast(fields.front().type); - if (!base) - { - return false; - } - - auto name = base.getName().getValue(); - if (nameable(name) || !ownClasses.contains(name)) - { - return true; - } - - storage = base; - } - - return true; - } - - // Can foreign code learn of a function of this module, to call it back during a call of a - // scalar declaration (scalarDeclaration)? It can only through the module's foreign surface, or - // through a function turned into data (controller ruling, spec 31, fix round 5: a closed-world - // test, in place of listing the routes a function value takes out). So the rule is on only when: - // - every foreign symbol is scalar (scalarType): every body-less function (C's, another - // module's - `import`, a DLL's - a `declare class`'s, a `@cpp` class's slot; a rest parameter - // is not scalar), every global declared with no initializer or imported (a `declare var`, a - // `declare class`'s vtable and type name), every `@cpp` class's fields and its bases', with - // no method of its own (C++ calls one through the C++ vtable), every function found in a - // library at run time (`SearchForAddressOfSymbol`, seen through its cast); an opaque pointer - // or a reference anywhere in such a type is a value of foreign memory, not scalar; - // - no cast makes an opaque pointer or a reference out of a number (`0x1234`, then a - // store through it reaches foreign memory); - // - no cast makes a value of a type that holds no function (`any`, `unknown`, an opaque - // pointer, an integer, a reference; a boolean cannot carry one and does not count) out of one - // that holds one (`onSig as Opaque`, `k.m as u64`, `>k`; a class or object - // takes its fields along unless seen as a class or an interface); - // - no `inline_asm` or `call_intrinsic` is given a value whose type holds a function. - bool functionsEscape = false; - - // The types a holdsFunction walk has been through, with or without their fields. - using SeenTypes = llvm::DenseSet>; - - // Does a value of this type hold a function: the type itself, a union's or an optional's - // member, a tuple's field, an array's element, what a reference points to, an interface whose - // vtable holds one past its header - a method, a function-typed field, or a field whose type - // holds one, recursively (an interface with methods, a class with a function field, an array of - // them) - or whose vtable this module does not make values with (a foreign one: `declare - // function makeW(): W`), and with `fields` a class's or an object's fields. The header slot - // (`.instanceOf`, the compiler's own, which destroys nothing) does not count. - bool holdsFunction(mlir::Type type, bool fields, SeenTypes &seen) - { - if (!type || !seen.insert({type, fields}).second) - { - return false; - } - - if (mlir::isa(type)) - { - return true; - } - - if (auto interfaceType = mlir::dyn_cast(type)) - { - auto found = interfaceVTableTypes.find(interfaceType.getName().getValue()); - if (found == interfaceVTableTypes.end()) - { - return true; - } - - return llvm::any_of(found->second, [&](mlir_ts::TupleType vtableType) { - return llvm::any_of(llvm::drop_begin(vtableType.getFields(), 1), - [&](const mlir_ts::FieldInfo &info) { return holdsFunction(info.type, /*fields=*/true, seen); }); - }); - } - - auto any = [&](llvm::ArrayRef infos) { - return llvm::any_of(infos, [&](const mlir_ts::FieldInfo &info) { return holdsFunction(info.type, fields, seen); }); - }; - - if (auto tupleType = mlir::dyn_cast(type)) - { - return any(tupleType.getFields()); - } - - if (auto constTupleType = mlir::dyn_cast(type)) - { - return any(constTupleType.getFields()); - } - - if (auto classStorageType = mlir::dyn_cast(type)) - { - return fields && any(classStorageType.getFields()); - } - - if (auto objectStorageType = mlir::dyn_cast(type)) - { - return fields && any(objectStorageType.getFields()); - } - - if (mlir::isa(type) && !fields) - { - return false; - } - - auto found = false; - type.walkImmediateSubElements([](mlir::Attribute) {}, [&](mlir::Type sub) { found = found || holdsFunction(sub, fields, seen); }); - return found; - } - - // The `@cpp` class storage a method's `this` is of, if any: the storage of its first - // parameter's class, when the method's symbol is `Class.name`. - mlir_ts::ClassStorageType cppClassOfMethod(mlir_ts::FuncOp funcOp) - { - auto inputs = funcOp.getFunctionType().getInputs(); - auto classType = inputs.empty() ? mlir_ts::ClassType() : mlir::dyn_cast(inputs.front()); - auto storage = classType ? mlir::dyn_cast(classType.getStorageType()) : mlir_ts::ClassStorageType(); - if (!storage || !storage.isCpp()) - { - return {}; - } - - auto name = storage.getName().getValue(); - auto symbol = funcOp.getSymName(); - return symbol.size() > name.size() && symbol.starts_with(name) && symbol[name.size()] == '.' ? storage : mlir_ts::ClassStorageType(); - } - - // Does a value of this type carry an address of memory this module made - so a cast of it to - // an opaque pointer or a reference points into this module, not into foreign memory? - static bool addressType(mlir::Type type) - { - return mlir::isa(type); - } - - void findEscapingFunctions(mlir::ModuleOp module) - { - // every body-less function: C's, another module's, a `declare class`'s (its methods, its - // `..instanceOf`, its `..new`), a `@cpp` class's slot - for (auto &entry : functions) - { - auto funcOp = entry.second; - if (funcOp.isDeclaration() && !scalarSignature(funcOp.getFunctionType())) - { - functionsEscape = true; - return; - } - - // a method of a `@cpp` class: C++ calls it through the class's C++ vtable - if (cppClassOfMethod(funcOp)) - { - functionsEscape = true; - return; - } - } - - // every global declared with no initializer or imported: a `declare var`, a `declare - // class`'s vtable (functions) and type name (a string) - for (auto &entry : globals) - { - auto globalOp = entry.second; - auto foreign = globalOp->hasAttr("import") || globalOp->hasAttr(DLL_IMPORT) || globalOp.getInitializerRegion().empty() || - globalOp.getInitializerRegion().front().empty(); - if (foreign && !scalarType(globalOp.getType())) - { - functionsEscape = true; - return; - } - } - - // every `@cpp` class's fields; a base (the first field, a storage) is a `@cpp` class of its - // own, judged on its own - for (auto &entry : classStorages) - { - auto storage = entry.second; - if (!storage.isCpp()) - { - continue; - } - - for (auto [index, field] : llvm::enumerate(storage.getFields())) - { - auto base = index == 0 ? mlir::dyn_cast(field.type) : mlir_ts::ClassStorageType(); - if (base ? !base.isCpp() : !scalarType(field.type)) - { - functionsEscape = true; - return; - } - } - } - - module.walk([&](mlir::Operation *op) { - if (functionsEscape) - { - return; - } - - // a function found in a library at run time: scalar through every cast of its address - if (mlir::isa(op)) - { - functionsEscape = !llvm::all_of(op->getResult(0).getUsers(), [](mlir::Operation *user) { - auto castOp = mlir::dyn_cast(user); - auto functionType = castOp ? mlir::dyn_cast(castOp.getType()) : mlir_ts::FunctionType(); - return functionType && scalarSignature(functionType); - }); - return; - } - - // `inline_asm(...)` and `call_intrinsic(...)`: a function given to either is data - // of no type (review6r5 h1, h2: `inline_asm("mov $1, $0", "=r,r", onSig)`) - if (mlir::isa(op)) - { - functionsEscape = llvm::any_of(op->getOperands(), [&](mlir::Value operand) { - SeenTypes seen; - return holdsFunction(operand.getType(), /*fields=*/true, seen); - }); - return; - } - - auto castOp = mlir::dyn_cast(op); - if (!castOp) - { - return; - } - - // a vtable's address (a class's, an interface's, a thunk's cell) seen as an opaque - // pointer or an integer: the compiler's own, inside a vtable or storing an object's - // vtable, never a value a program gives C - auto vtableOp = op->getParentOfType(); - auto addressOfOp = addressOfThroughCasts(castOp.getIn()); - auto isVTable = [](llvm::StringRef name) { return name.ends_with("..vtbl") || name.contains("..vtbl."); }; - if ((vtableOp && isVTable(vtableOp.getSymName())) || (addressOfOp && isVTable(addressOfOp.getGlobalName()))) - { - return; - } - - // an opaque pointer or a reference made out of a number: an address of foreign memory - if (mlir::isa(castOp.getType()) && !addressType(castOp.getIn().getType())) - { - functionsEscape = true; - return; - } - - // a function seen as something that holds none; a boolean cannot carry one. A class or - // object takes its fields along, seen as anything (`any`, an opaque pointer, an integer, - // a reference: `>k`, then a load of the field, review6r5 g2) but a class - // (its base) or an interface (a view of it, read through its vtable only) - if (mlir::isa(castOp.getType())) - { - return; - } - - auto fields = !mlir::isa(castOp.getType()); - SeenTypes seenIn; - SeenTypes seenOut; - functionsEscape = holdsFunction(castOp.getIn().getType(), fields, seenIn) && - !holdsFunction(castOp.getType(), /*fields=*/true, seenOut); - }); - } - - // class name -> its storage, from every class type a function's signature or a global mentions - llvm::StringMap classStorages; - - void collectClassStorages(mlir::ModuleOp module) - { - llvm::DenseSet seen; - std::function visit = [&](mlir::Type type) { - if (!type || !seen.insert(type).second) - { - return; - } - - if (auto storage = mlir::dyn_cast(type)) - { - classStorages[storage.getName().getValue()] = storage; - for (auto &field : storage.getFields()) - { - visit(field.type); - } - - return; - } - - type.walkImmediateSubElements([](mlir::Attribute) {}, visit); - }; - - for (auto &entry : functions) - { - visit(entry.second.getFunctionType()); - } - - module.walk([&](mlir::Operation *op) { - for (auto result : op->getResults()) - { - if (mlir::isa(result.getType())) - { - visit(result.getType()); - } - } - }); - } - - // A class another module can see may be extended there, and its override is a candidate this - // module never sees: every member has to be private and defined here. - void resolveFamily(int64_t index, mlir::StringAttr identifier, Callees &callees) - { - callees.known = true; - for (auto member : familyOf(index, identifier)) - { - callees.known = addDefined(member, callees) && callees.known; - } - - callees.known = callees.known && llvm::all_of(callees.funcs, [&](mlir_ts::FuncOp funcOp) { return !callableFromOutside(funcOp); }); - } - - // `ts.Cast(ts.VTableOffsetRef(vtable, 0))`, where `vtable` is the one an object's first word - // points to or an interface value's own: slot 0 of either is `.instanceOf` - a class's - // generated one, a string compare, or `.instanceOf.none` for an object literal behind an - // interface. Only mlirGenInstanceOfThroughVTable builds a call through it. Neither destroys - // anything. - static bool isInstanceOfSlot(mlir::Operation *def) - { - auto castOp = mlir::dyn_cast_or_null(def); - auto slot = castOp ? castOp.getIn().getDefiningOp() : mlir_ts::VTableOffsetRefOp(); - if (!slot || slot.getIndex() != INTERFACE_VTABLE_INSTANCEOF_SLOT) - { - return false; - } - - // a class vtable, read out of an object's first word, or an interface's own vtable, - // whose slot 0 is its implementer's `.instanceOf` (INTERFACE_VTABLE_HEADER_SLOTS) - auto *vtable = slot.getVtable().getDefiningOp(); - if (auto classVTable = mlir::dyn_cast_or_null(vtable)) - { - return classVTable.getIndex() == 0; - } - - return mlir::isa_and_nonnull(vtable); - } - - // Is every use of this function value the callee of a call - through `ts.GetMethod`, with - // `ts.GetThis` reading the object beside it? - static bool onlyCalled(mlir::Value value) - { - for (auto &use : value.getUses()) - { - auto *user = use.getOwner(); - if (mlir::isa(user) && - calleeValue(user) == use.get() && use.getOperandNumber() == 0) - { - continue; - } - - if (mlir::isa(user)) - { - continue; - } - - if (mlir::isa(user) && onlyCalled(user->getResult(0))) - { - continue; - } - - return false; - } - - return true; - } - - // The closed world. A function is open when a caller this pass cannot see may reach it: it is - // not private (another module may call it), or its symbol is used other than by a direct call, - // a class vtable's entry, or a method reference used only as a callee. A virtual reference - // that escapes opens its whole family. - void findOpen(mlir::ModuleOp module) - { - for (auto &entry : functions) - { - if (callableFromOutside(entry.second)) - { - open.try_emplace(entry.second, "it can be called from another module; '@" LENDS_DECORATOR "' or '@" KEEPS_DECORATOR - "' would say it in its signature"); - } - } - - // the module is a symbol table, which getSymbolUses does not look inside: walk its body - auto uses = mlir::SymbolTable::getSymbolUses(&module.getBodyRegion()); - if (!uses) - { - // an op this pass cannot read symbols through: nothing is closed - for (auto &entry : functions) - { - open.try_emplace(entry.second, "its callers cannot all be found"); - } - - return; - } - - for (auto &use : *uses) - { - auto symbol = use.getSymbolRef().getRootReference(); - auto funcOp = functions.lookup(symbol.getValue()); - if (!funcOp) - { - continue; - } - - auto *user = use.getUser(); - if (mlir::isa(user)) - { - continue; - } - - if (mlir::isa(user) && isClassVTableEntry(user)) - { - continue; - } - - // passed only to a parameter whose family is closed: its calls are resolved (spec 33) - if (callbackPassing.contains(user)) - { - continue; - } - - int64_t index = -1; - if (auto refOp = mlir::dyn_cast(user)) - { - index = refOp.getIndex(); - } - else if (auto refOp = mlir::dyn_cast(user)) - { - index = refOp.getIndex(); - } - - auto inFunction = !!user->getParentOfType(); - if (inFunction && mlir::isa(user) && - onlyCalled(user->getResult(0))) - { - // a virtual call this pass cannot resolve reaches its family without the facts - if (index >= 0) - { - Callees callees; - resolveFamily(index, symbol, callees); - if (!callees.known) - { - openFamily(index, symbol, "an override may be defined in another module"); - } - } - - continue; - } - - open.try_emplace(funcOp, "it is used other than by a call"); - if (index >= 0) - { - openFamily(index, symbol, "it is used other than by a call"); - } - } - } - - void openFamily(int64_t index, mlir::StringAttr symbol, const char *why) - { - for (auto member : familyOf(index, symbol)) - { - if (auto memberOp = functions.lookup(member.getValue())) - { - open.try_emplace(memberOp, why); - } - } - } - - // ---- Callbacks (spec 33) ---- - // - // `___array_reduce(arr, f, init)` calls its parameter `f`: an indirect call, which reaches - // whatever function its callers pass. When the function is closed - private, and every use of its - // symbol a direct call - the parameter is only ever called, and every caller passes there a - // function this module defines, by its symbol or as a closure over a capture box made right - // there, a call through the parameter reaches exactly those functions: the parameter's family. - // Its members are closed too, when their symbols are used only to be called directly or passed - // so; then the call gets the facts the members agree on (callFacts), and `__own_no_drops` when - // none of them destroys anything. A closure's body takes its capture box first: its parameter k - // is the call's argument k - 1 (CallbackMember::shift). - // - // A kept parameter is agreed on the other way round from a class family's (meetCallbacks): a - // member that only borrows a parameter another member keeps is given it to keep too, and releases - // it on every path (OWN_PARAMS_RELEASED_ATTR_NAME) - `reduce` with a callback that returns its - // accumulator on some paths and one that never does, in one `___array_reduce`. Anything - // else the members disagree on demotes them all, as a class family's members are. - - struct CallbackMember - { - mlir::StringAttr symbol; - int32_t shift = 0; - }; - - struct CallbackFamily - { - llvm::SmallVector members; - // the ops that name a member's symbol to pass it here - llvm::SmallVector passing; - bool closed = true; - }; - - // (function, parameter index) -> the functions its callers pass there - std::map, CallbackFamily> callbackFamilies; - // the members whose families disagreed (meetFacts), for the error in their bodies - llvm::DenseSet demotedCallbacks; - // uses of a member's symbol that only pass it to a closed family: no escape (findOpen) - llvm::DenseSet callbackPassing; - - static bool isFunctionValueType(mlir::Type type) - { - return mlir::isa(type); - } - - // Is every use of this parameter - through its slot, never assigned or captured - the function - // a call goes through? - static bool onlyCalledParam(mlir::BlockArgument argument) - { - for (auto *user : argument.getUsers()) - { - auto varOp = mlir::dyn_cast(user); - auto index = -1; - if (!varOp || !isParameterSlot(varOp, index)) - { - return false; - } - - for (auto *slotUser : varOp->getUsers()) - { - if (mlir::isa(slotUser)) - { - continue; - } - - auto loadOp = mlir::dyn_cast(slotUser); - if (!loadOp) - { - return false; - } - - for (auto &use : loadOp->getUses()) - { - auto *call = use.getOwner(); - if (!isCall(call) || calleeValue(call) != use.get() || - llvm::is_contained(callArgs(call), use.get())) - { - return false; - } - } - } - } - - return true; - } - - // The function a caller passes as `value`: its symbol, seen as a function value through casts - // between function types, or a closure over a capture box built of it. Null for anything else. - static mlir_ts::SymbolRefOp passedFunction(mlir::Value value, int32_t &shift) - { - while (auto castOp = value.getDefiningOp()) - { - if (!isFunctionValueType(castOp.getIn().getType()) || !isFunctionValueType(castOp.getType())) - { - return {}; - } - - value = castOp.getIn(); - } - - if (auto refOp = value.getDefiningOp()) - { - shift = 0; - return refOp; - } - - auto boundOp = value.getDefiningOp(); - if (!boundOp || closureOfBox(boundOp.getThisVal()) != boundOp) - { - return {}; - } - - shift = 1; - return boundOp.getFunc().getDefiningOp(); - } - - // Does the function value `value` go only where `passes` allows - as an argument of a call - and - // through casts between function types, a closure built of it, and that closure's own retains - // and releases? - static bool passedOnly(mlir::Value value, llvm::function_ref passes, int depth = 0) - { - if (depth > 8) - { - return false; - } - - for (auto &use : value.getUses()) - { - auto *user = use.getOwner(); - if (mlir::isa(user) || passes(use)) - { - continue; - } - - auto castOp = mlir::dyn_cast(user); - auto boundOp = mlir::dyn_cast(user); - if (castOp && isFunctionValueType(castOp.getType()) && passedOnly(castOp.getResult(), passes, depth + 1)) - { - continue; - } - - if (boundOp && boundOp.getFunc() == use.get() && closureOfBox(boundOp.getThisVal()) == boundOp && - passedOnly(boundOp.getResult(), passes, depth + 1)) - { - continue; - } - - return false; - } - - return true; - } - - void collectCallbackFamilies(mlir::ModuleOp module) - { - auto uses = mlir::SymbolTable::getSymbolUses(&module.getBodyRegion()); - if (!uses) - { - return; - } - - llvm::DenseMap> usersOf; - for (auto &use : *uses) - { - if (auto funcOp = functions.lookup(use.getSymbolRef().getRootReference().getValue())) - { - usersOf[funcOp].push_back(use.getUser()); - } - } - - auto callsOnly = [&](mlir_ts::FuncOp funcOp) { - auto &users = usersOf[funcOp]; - return !users.empty() && llvm::all_of(users, [&](mlir::Operation *user) { - auto callee = directCallee(user); - return callee && callee.getValue() == funcOp.getSymName(); - }); - }; - - for (auto &entry : functions) - { - auto funcOp = entry.second; - if (funcOp.isDeclaration() || callableFromOutside(funcOp) || declaresFacts(funcOp) || !callsOnly(funcOp)) - { - continue; - } - - for (auto argument : funcOp.getBody().front().getArguments()) - { - if (!isFunctionValueType(argument.getType()) || !onlyCalledParam(argument)) - { - continue; - } - - auto index = static_cast(argument.getArgNumber()); - CallbackFamily family; - for (auto *user : usersOf[funcOp]) - { - auto args = callArgs(user); - auto shift = 0; - auto refOp = static_cast(index) < args.size() ? passedFunction(args[index], shift) - : mlir_ts::SymbolRefOp(); - auto member = refOp ? functions.lookup(refOp.getIdentifier()) : mlir_ts::FuncOp(); - if (!member || member.isDeclaration() || callableFromOutside(member) || declaresFacts(member)) - { - family.closed = false; - break; - } - - auto symbol = member.getSymNameAttr(); - auto same = llvm::find_if(family.members, [&](const CallbackMember &known) { return known.symbol == symbol; }); - if (same != family.members.end() && same->shift != shift) - { - family.closed = false; - break; - } - - if (same == family.members.end()) - { - family.members.push_back({symbol, shift}); - } - - family.passing.push_back(refOp); - } - - callbackFamilies[{funcOp, index}] = std::move(family); - } - } - - // A member's symbol used anywhere else - a call through another value, a field, a vtable, a - // global - reaches callers this cannot see; so does one passed to a family that is not closed. - // Each family that has such a member is open, until nothing changes. - auto passesToClosed = [&](mlir::OpOperand &use) { - auto *call = use.getOwner(); - auto callee = directCallee(call); - auto calleeOp = callee ? functions.lookup(callee.getValue()) : mlir_ts::FuncOp(); - if (!calleeOp) - { - return false; - } - - // the argument this use is, by its operand's position - auto args = callArgs(call); - if (args.empty() || use.getOperandNumber() < args.getBeginOperandIndex() || - use.getOperandNumber() >= args.getBeginOperandIndex() + args.size()) - { - return false; - } - - auto index = static_cast(use.getOperandNumber() - args.getBeginOperandIndex()); - auto found = callbackFamilies.find({calleeOp, index}); - return found != callbackFamilies.end() && found->second.closed; - }; - - for (auto changed = true; changed;) - { - changed = false; - for (auto &[key, family] : callbackFamilies) - { - if (!family.closed) - { - continue; - } - - for (auto &member : family.members) - { - auto memberOp = functions.lookup(member.symbol.getValue()); - auto escapes = llvm::any_of(usersOf[memberOp], [&](mlir::Operation *user) { - if (auto callee = directCallee(user); callee && callee.getValue() == member.symbol.getValue()) - { - return false; - } - - auto refOp = mlir::dyn_cast(user); - return !refOp || !user->getParentOfType() || - !passedOnly(refOp.getResult(), passesToClosed); - }); - - if (escapes) - { - family.closed = false; - changed = true; - break; - } - } - } - } - - for (auto &[key, family] : callbackFamilies) - { - if (family.closed) - { - callbackPassing.insert(family.passing.begin(), family.passing.end()); - } - } - } - - // A call through a parameter whose family is closed: its members. - void resolveCallback(mlir_ts::LoadOp loadOp, Callees &callees) - { - auto varOp = loadOp.getReference().getDefiningOp(); - auto funcOp = loadOp->getParentOfType(); - auto index = -1; - if (!varOp || !funcOp || !isParameterSlot(varOp, index)) - { - return; - } - - auto found = callbackFamilies.find({funcOp.getOperation(), index}); - if (found == callbackFamilies.end() || !found->second.closed || found->second.members.empty()) - { - return; - } - - callees.viaCallback = true; - callees.known = true; - for (auto &member : found->second.members) - { - callees.named.push_back(member.symbol); - callees.known = addDefined(member.symbol, callees) && callees.known; - callees.shifts.push_back(member.shift); - } - - callees.known = callees.known && callees.shifts.size() == callees.funcs.size(); - } - - // A member's facts as a call through its family sees them: each parameter k as argument - // k - shift. False when a fact is about the capture box itself, which the call does not pass. - static bool shiftFacts(const Facts &facts, int32_t shift, Facts &shifted) - { - shifted = Facts(); - shifted.record = facts.record; - for (auto [from, to] : {std::make_pair(&facts.owned, &shifted.owned), std::make_pair(&facts.borrows, &shifted.borrows)}) - { - for (auto index : *from) - { - if (index < shift) - { - return false; - } - - to->push_back(index - shift); - } - } - - return true; - } - - // Can the body of `funcOp` be given parameter `index` to keep and release it where it does not - // move it: a plain slot (isParameterSlot), a block, not a handle, not a string? - bool canReleaseParam(mlir_ts::FuncOp funcOp, int32_t index) - { - if (funcOp.getBody().empty() || static_cast(index) >= funcOp.getNumArguments()) - { - return false; - } - - auto argument = funcOp.getBody().front().getArgument(index); - if (isHandle(argument) || isCopyableString(argument.getType()) || holdsNoBlock(argument)) - { - return false; - } - - return llvm::any_of(argument.getUsers(), [&](mlir::Operation *user) { - auto varOp = mlir::dyn_cast(user); - auto position = -1; - return varOp && isParameterSlot(varOp, position) && position == index; - }); - } - - // The members of each closed callback family agree (the section above), by promotion of a kept - // parameter or by demotion. True when any member's facts changed. - bool meetCallbacks(llvm::DenseSet &demoted) - { - auto changed = false; - for (auto &[key, family] : callbackFamilies) - { - if (!family.closed) - { - continue; - } - - llvm::SmallVector members; - llvm::SmallVector seen; - auto shiftable = true; - for (auto &member : family.members) - { - auto funcOp = functions.lookup(member.symbol.getValue()); - Facts shifted; - shiftable = shiftable && funcOp && shiftFacts(factsEffective.lookup(funcOp), member.shift, shifted); - members.push_back(funcOp); - seen.push_back(shifted); - } - - if (shiftable && llvm::all_equal(seen)) - { - continue; - } - - // a parameter some member keeps, and each other one only borrows: kept by all - llvm::SmallVector kept; - for (auto &facts : seen) - { - kept.append(facts.owned.begin(), facts.owned.end()); - } - - llvm::sort(kept); - kept.erase(std::unique(kept.begin(), kept.end()), kept.end()); - auto promotable = shiftable && llvm::all_of(seen, [&](const Facts &facts) { - return facts.borrows == seen.front().borrows && facts.record == seen.front().record && - llvm::none_of(kept, [&](int32_t index) { return llvm::is_contained(facts.borrows, index); }); - }); - - for (auto [position, member] : llvm::enumerate(family.members)) - { - auto funcOp = members[position]; - // a member another caller reaches without the facts is given none (findOpen) - for (auto index : kept) - { - promotable = promotable && !demoted.contains(funcOp) && !open.contains(funcOp) && - !interfaceReached.contains(funcOp) && - (llvm::is_contained(seen[position].owned, index) || - canReleaseParam(funcOp, index + member.shift)); - } - } - - if (promotable) - { - for (auto [position, member] : llvm::enumerate(family.members)) - { - auto funcOp = members[position]; - auto facts = factsEffective.lookup(funcOp); - for (auto index : kept) - { - auto param = index + member.shift; - if (!llvm::is_contained(facts.owned, param)) - { - facts.owned.insert(llvm::upper_bound(facts.owned, param), param); - } - } - - if (facts != factsEffective.lookup(funcOp)) - { - factsEffective[funcOp] = facts; - changed = true; - } - } - - continue; - } - - for (auto funcOp : members) - { - if (funcOp && !declaresCallerFacts(funcOp)) - { - demoted.insert(funcOp); - if (factsEffective.erase(funcOp)) - { - changed = true; - } - } - } - } - - return changed; - } - - // ---- Results that borrow an argument ---- - - // The values a function returns: what is stored into the local its returns read (MLIRGen's - // result slot), or what a return hands back directly. - static llvm::SmallVector returnedValues(mlir_ts::FuncOp funcOp) - { - llvm::SmallVector values; - funcOp.walk([&](mlir_ts::ReturnInternalOp returnOp) { - for (auto operand : returnOp.getRetOperands()) - { - auto loadOp = operand.getDefiningOp(); - auto slot = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); - if (!slot || slot.getInitializer() || isOwningVariable(slot)) - { - values.push_back(operand); - continue; - } - - for (auto *user : slot.getResult().getUsers()) - { - auto storeOp = mlir::dyn_cast(user); - if (storeOp && storeOp.getReference() == slot.getResult()) - { - values.push_back(storeOp.getValue()); - } - } - } - }); - - return values; - } - - struct ResultBorrows - { - llvm::SmallVector borrows; - bool record = false; - }; - - // What the function's result borrows (spec 2.4, 32): the one parameter every heap value it returns - // is a borrow of, or, when every such value is a borrowing record, the parameters the records - // borrow. A result that holds no block (`null`, a number) agrees with any; a record on one path - // and anything else on another is nothing a caller can be told. - ResultBorrows localResultBorrows(mlir_ts::FuncOp funcOp) - { - auto found = -1; - auto record = false; - llvm::SmallVector params; - for (auto value : returnedValues(funcOp)) - { - if (holdsNoBlock(value)) - { - continue; - } - - // a handle returned is a counted copy, never a borrow (spec 23.2) - if (isHandle(value)) - { - return {}; - } - - switch (recordParamsOf(value, params)) - { - case RecordFlow::Bad: - return {}; - case RecordFlow::Record: - record = true; - continue; - case RecordFlow::None: - break; - } - - auto index = borrowedParam(value); - if (index < 0 || (found >= 0 && index != found)) - { - return {}; - } - - found = index; - } - - if (record && found >= 0) - { - return {}; - } - - if (record) - { - llvm::sort(params); - params.erase(std::unique(params.begin(), params.end()), params.end()); - return {params, true}; - } - - if (found >= 0) - { - return {{found}, false}; - } - - return {}; - } - - // The parameter a function returns as it is on some paths, where every other value it returns - // that holds a block is one it owns (spec 33): `return v.x > 2 ? acc : V.plus(acc, v)`. Such a - // parameter is kept: the callers move the argument in, the path that returns it moves it out, - // and every other path releases it. -1 for none - a parameter returned on every path is a - // borrowed result; two parameters returned, or a part of one (`acc.next`), or a borrowing - // record, have no such answer; a string is copied instead (spec 22). - int32_t mixedReturnParam(mlir_ts::FuncOp funcOp) - { - auto param = -1; - auto other = false; - auto returned = returnedValues(funcOp); - llvm::SmallVector work(returned.begin(), returned.end()); - llvm::DenseSet seen; - while (!work.empty()) - { - auto value = work.pop_back_val(); - if (!seen.insert(value).second || holdsNoBlock(value)) - { - continue; - } - - if (isHandle(value) || isCopyableString(value.getType())) - { - return -1; - } - - auto root = rootOf(value); - if (auto argument = mlir::dyn_cast(root); argument && !argument.getOwner()->isEntryBlock()) - { - llvm::SmallVector merged; - if (!mergedInto(argument, merged)) - { - return -1; - } - - work.append(merged.begin(), merged.end()); - continue; - } - - int32_t index = -1; - if (readsParameterSlot(root, funcOp, index)) - { - if ((param >= 0 && param != index) || isHandle(root) || isCopyableString(root.getType())) - { - return -1; - } - - param = index; - continue; - } - - llvm::SmallVector recordParams; - if (borrowedParam(root) >= 0 || recordParamsOf(root, recordParams) != RecordFlow::None) - { - return -1; - } - - other = true; - } - - return other ? param : -1; - } - - // ---- Borrowing records (spec 32) ---- - // - // `{ thing: this, ray: ray, dist: d }` given as an interface: an object literal's box holding values - // it does not own beside values it does. Rust's `struct Hit<'a> { thing: &'a Th, .. }`: the box is - // its owner's - the caller's, when it is returned - and it may not outlive what it borrows. Each box - // is classified where it is made: the fields it borrows (OWN_RECORD_BORROWED_FIELDS_ATTR_NAME, on the - // box and on every interface made of it, whose tag then names a release routine that leaves them), - // and, when every one of them is a parameter, `this`, or a read out of one, those parameters - what - // a function returning it lends (OWN_RESULT_RECORD_ATTR_NAME). The inference bounds every use. - - struct RecordBox - { - llvm::SmallVector params; - // every borrowed field is a parameter's, `this`'s, a read out of one, or immortal - bool returnable = true; - }; - - llvm::DenseMap recordBoxes; - - void classifyRecordBoxes() - { - recordBoxes.clear(); - getOperation().walk([&](mlir_ts::NewOp newOp) { - newOp->removeAttr(OWN_RECORD_BORROWED_FIELDS_ATTR_NAME); - for (auto *user : newOp->getUsers()) - { - for (auto *viewUser : user->getUsers()) - { - if (mlir::isa(viewUser)) - { - viewUser->removeAttr(OWN_RECORD_BORROWED_FIELDS_ATTR_NAME); - } - } - } - - classifyRecordBox(newOp); - }); - } - - void classifyRecordBox(mlir_ts::NewOp newOp) - { - auto boxOp = recordBoxOf(newOp.getResult()); - auto varOp = boxOp == newOp ? recordLocalOf(newOp.getResult()) : mlir_ts::VariableOp(); - if (!varOp) - { - return; - } - - // the record read once, and only into its box (rc's retain of it beside) - mlir_ts::LoadOp read; - for (auto *user : varOp->getUsers()) - { - if (auto loadOp = mlir::dyn_cast(user)) - { - if (read) - { - return; - } - - read = loadOp; - } - } - - for (auto *user : read->getUsers()) - { - auto storeOp = mlir::dyn_cast(user); - if (!mlir::isa(user) && !(storeOp && storeOp.getReference() == newOp.getResult())) - { - return; - } - } - - // the box seen only as interfaces: every release of it reads their tag - llvm::SmallVector interfaces; - for (auto &use : newOp->getUses()) - { - auto *user = use.getOwner(); - if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getReference() == newOp.getResult()) - { - continue; - } - - auto castOp = mlir::dyn_cast(user); - if (!castOp || !isView(castOp)) - { - return; - } - - for (auto *castUser : castOp->getUsers()) - { - auto newInterfaceOp = mlir::dyn_cast(castUser); - if (!newInterfaceOp || newInterfaceOp.getThisVal() != castOp.getResult()) - { - return; - } - - interfaces.push_back(castUser); - } - } - - if (interfaces.empty()) - { - return; - } - - // each field that holds a block: owned (made here, or nothing) or borrowed, never both - RecordBox info; - llvm::SmallVector borrowed; - MLIRTypeHelper mth(&getContext(), CompileOptions{}); - for (auto *user : varOp->getUsers()) - { - auto propertyRefOp = mlir::dyn_cast(user); - if (!propertyRefOp) - { - continue; - } - - auto field = propertyRefOp.getResult(); - auto refType = mlir::dyn_cast(field.getType()); - if (!refType || !mth.ownsHeapMemory(field.getLoc(), refType.getElementType())) - { - continue; - } - - auto owns = false; - auto borrows = false; - for (auto *fieldUser : field.getUsers()) - { - if (mlir::isa(fieldUser)) - { - continue; - } - - auto storeOp = mlir::dyn_cast(fieldUser); - if (!storeOp || storeOp.getReference() != field) - { - return; - } - - auto value = storeOp.getValue(); - switch (recordFieldSource(value)) - { - case RecordFieldSource::NoBlock: - continue; - case RecordFieldSource::Fresh: - owns = true; - continue; - case RecordFieldSource::Unknown: - return; - case RecordFieldSource::Immortal: - borrows = true; - continue; - case RecordFieldSource::Param: - case RecordFieldSource::Place: - if (auto param = borrowedParam(value); param >= 0) - { - info.params.push_back(param); - } - else - { - info.returnable = false; - } - - borrows = true; - continue; - case RecordFieldSource::Slot: - case RecordFieldSource::Cell: - info.returnable = false; - borrows = true; - continue; - } - } - - // a string is copied, not borrowed (spec 22) - if ((owns && borrows) || (borrows && isCopyableString(refType.getElementType()))) - { - return; - } - - if (borrows) - { - borrowed.push_back(propertyRefOp.getPosition()); - } - } - - if (borrowed.empty()) - { - return; - } - - llvm::sort(borrowed); - borrowed.erase(std::unique(borrowed.begin(), borrowed.end()), borrowed.end()); - llvm::sort(info.params); - info.params.erase(std::unique(info.params.begin(), info.params.end()), info.params.end()); - - auto attr = mlir::DenseI32ArrayAttr::get(&getContext(), borrowed); - newOp->setAttr(OWN_RECORD_BORROWED_FIELDS_ATTR_NAME, attr); - for (auto *newInterfaceOp : interfaces) - { - newInterfaceOp->setAttr(OWN_RECORD_BORROWED_FIELDS_ATTR_NAME, attr); - } - - recordBoxes[newOp.getOperation()] = std::move(info); - } - - enum class RecordFlow - { - None, // no borrowing record - Record, // borrowing records only (or values that hold no block) - Bad // a record that borrows what is not a parameter, or one beside anything else - }; - - // Whether `value` - through views, merges and the owning locals that hold it - is a borrowing - // record, and the parameters it borrows: a box made here, or a call's result that is one, whose - // arguments are this function's parameters or reads out of them. - RecordFlow recordParamsOf(mlir::Value value, llvm::SmallVectorImpl ¶ms) - { - llvm::SmallVector work{value}; - llvm::SmallPtrSet seen; - auto record = false; - auto other = false; - while (!work.empty()) - { - // what holds no block, by its own type (a box's `value_ref` owns nothing by type, but - // the object its view shows does) - auto next = work.pop_back_val(); - auto current = rootOf(next); - if (!seen.insert(current).second || holdsNoBlock(next)) - { - continue; - } - - auto *def = current.getDefiningOp(); - if (auto found = def ? recordBoxes.find(def) : recordBoxes.end(); found != recordBoxes.end()) - { - if (!found->second.returnable) - { - return RecordFlow::Bad; - } - - params.append(found->second.params.begin(), found->second.params.end()); - record = true; - continue; - } - - if (def && isCall(def) && isRecordResult(def)) - { - auto args = callArgs(def); - for (auto index : resultBorrowsAll(def)) - { - if (static_cast(index) >= args.size()) - { - return RecordFlow::Bad; - } - - auto arg = args[index]; - if (argumentHoldsNoBlock(arg) || isImmortalGlobalRead(arg)) - { - continue; - } - - auto param = borrowedParam(arg); - if (param < 0) - { - return RecordFlow::Bad; - } - - params.push_back(param); - } - - record = true; - continue; - } - - if (auto argument = mlir::dyn_cast(current)) - { - llvm::SmallVector merged; - if (mergedInto(argument, merged)) - { - work.append(merged.begin(), merged.end()); - continue; - } - - other = true; - continue; - } - - // what an owning local holds: its initializer and every value stored into it - auto loadOp = mlir::dyn_cast_or_null(def); - auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); - if (varOp && isOwningVariable(varOp) && !varOp.getCaptured().value_or(false)) - { - if (varOp.getInitializer()) - { - work.push_back(varOp.getInitializer()); - } - - for (auto *user : varOp->getUsers()) - { - if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getReference() == varOp.getResult()) - { - work.push_back(storeOp.getValue()); - } - } - - continue; - } - - other = true; - } - - if (!record) - { - return RecordFlow::None; - } - - return other ? RecordFlow::Bad : RecordFlow::Record; - } - - // ---- Parameters the callee keeps ---- - - // Does `user` take `used` - a read of a parameter - into something that outlives the call: a - // store into a field, an element or a global, an insertion into an array, or a call that keeps - // it in turn? A local that holds it, and the result it is returned as, do not count: the first - // is a borrow, the second a borrowed result. - static bool keeps(mlir::Operation *user, mlir::Value used) - { - // a number, a boolean, a string literal: nothing to keep - if (holdsNoBlock(used)) - { - return false; - } - - if (auto storeOp = mlir::dyn_cast(user)) - { - // a field a borrowing record borrows (spec 32): the record keeps nothing - if (isBorrowedRecordFieldStore(user)) - { - return false; - } - - auto ref = storeOp.getReference(); - return storeOp.getValue() == used && (isPlace(ref) || ref.getDefiningOp()); - } - - if (auto pushOp = mlir::dyn_cast(user)) - { - return llvm::is_contained(pushOp.getItems(), used); - } - - // `new Shared(x)` moves `x` into the block, as a store into a place does (spec 23.1) - if (auto sharedNewOp = mlir::dyn_cast(user)) - { - return sharedNewOp.getValue() == used; - } - - if (auto unshiftOp = mlir::dyn_cast(user)) - { - return llvm::is_contained(unshiftOp.getItems(), used); - } - - if (auto spliceOp = mlir::dyn_cast(user)) - { - return llvm::is_contained(spliceOp.getItems(), used); - } - - if (isCall(user)) - { - auto args = callArgs(user); - return llvm::any_of(ownedParams(user), [&](int32_t index) { - return static_cast(index) < args.size() && args[index] == used; - }); - } - - // `let r = init; ... r = f(r, v)`: a local that owns what it is declared from and is given - // another value is no borrow of the parameter - the assignment releases what it holds - so - // the parameter moves into it (spec 33: `___array_reduce`'s accumulator). Not a string: the - // local copies it (spec 22) - if (auto varOp = mlir::dyn_cast(user)) - { - return varOp.getInitializer() == used && isOwningVariable(varOp) && !varOp.getCaptured().value_or(false) && - isAssigned(varOp) && !isCopyableString(used.getType()); - } - - return false; - } - - // The parameters the body keeps (spec 2.4, owned-by-callee): a read of the parameter's slot - - // never assigned - that something keeps. - static llvm::SmallVector localOwnedParams(mlir_ts::FuncOp funcOp) - { - llvm::SmallVector owned; - if (funcOp.getBody().empty()) - { - return owned; - } - - for (auto argument : funcOp.getBody().front().getArguments()) - { - // a handle kept is a counted copy: its caller gives nothing up (spec 23.2) - if (isHandle(argument)) - { - continue; - } - - auto kept = false; - for (auto *user : argument.getUsers()) - { - auto varOp = mlir::dyn_cast(user); - auto index = -1; - if (!varOp || !isParameterSlot(varOp, index)) - { - continue; - } - - for (auto *slotUser : varOp.getResult().getUsers()) - { - if (auto loadOp = mlir::dyn_cast(slotUser)) - { - forEachUse(loadOp.getResult(), [&](mlir::Operation *use, mlir::Value used) { - kept = kept || keeps(use, used); - }); - } - } - } - - if (kept) - { - owned.push_back(argument.getArgNumber()); - } - } - - return owned; - } - - // ---- The facts callers must know ---- - - // Owned parameters and a result that borrows only hold where every caller knows them, so the - // facts survive only on a closed function whose every family agrees, and families are - // demoted until they do: a member that loses its facts can make another family disagree. A - // caller that returns what such a call returns, or passes a parameter on to a kept one, gets - // the fact too, so the facts are recomputed with the calls pinned until nothing changes; if - // that does not settle, nobody gets any. - void computeFacts() - { - llvm::DenseMap pinned; - for (auto round = 0; round < 16; ++round) - { - // which object literals borrow depends on what the calls stored into them return, and a - // function's facts on which literals borrow (spec 32) - classifyRecordBoxes(); - - for (auto &entry : functions) - { - if (!entry.second.isDeclaration()) - { - auto result = localResultBorrows(entry.second); - Facts facts; - facts.borrows = std::move(result.borrows); - facts.record = result.record; - facts.owned = localOwnedParams(entry.second); - // what the body takes into something on every path; a parameter it returns on - // some paths only is kept too, and released on the others (spec 33) - takenParams[entry.second] = facts.owned; - if (auto mixed = mixedReturnParam(entry.second); mixed >= 0 && !llvm::is_contained(facts.owned, mixed)) - { - facts.owned.insert(llvm::upper_bound(facts.owned, mixed), mixed); - } - - factsLocal[entry.second] = std::move(facts); - } - } - - meetFacts(); - - llvm::DenseMap next; - for (auto &call : calls) - { - auto facts = callFacts(call.callees); - if (facts.empty()) - { - // a call this pass cannot resolve still has what its callees' signatures say - // A declaration's `@lends` is a plain borrow to its callers: its body, which would - // say whether the result is a record the caller owns (spec 32), is not here. A - // record's box then leaks, and nothing is freed twice. - auto keeps = declaredByCallees(call.callees, OWN_DECLARED_KEEPS_ATTR_NAME); - facts.owned.assign(keeps.begin(), keeps.end()); - if (auto lends = declaredByCallees(call.callees, OWN_DECLARED_LENDS_ATTR_NAME); !lends.empty()) - { - llvm::sort(lends); - lends.erase(std::unique(lends.begin(), lends.end()), lends.end()); - facts.borrows = lends; - } - } - - if (!facts.empty()) - { - next[call.op] = facts; - } - } - - auto settled = next == pinned; - pinned = std::move(next); - pinFacts(pinned); - if (settled) - { - return; - } - } - - factsEffective.clear(); - pinFacts(llvm::DenseMap()); - } - - void meetFacts() - { - factsEffective.clear(); - for (auto &[funcOp, facts] : factsLocal) - { - if (!facts.empty() && !open.contains(funcOp) && !factsThroughInterface(funcOp, facts)) - { - factsEffective[funcOp] = facts; - } - } - - // what a signature says stands, open or not; checkDeclared holds the body to it. But not a - // `@keeps` or `@lends` through an interface: the interface's callers are not told - // (interfaceReached). - for (auto &entry : functions) - { - if (declaresCallerFacts(entry.second)) - { - auto keeps = declared(entry.second, OWN_DECLARED_KEEPS_ATTR_NAME); - Facts said; - said.borrows = declaredLends(entry.second); - said.owned.assign(keeps.begin(), keeps.end()); - // a body here that returns a borrowing record of what `@lends` names: callers in this - // module own the box (spec 32); checkDeclared holds the body to the parameters - auto local = factsLocal.lookup(entry.second); - said.record = !said.borrows.empty() && local.record && local.borrows == said.borrows; - if (factsThroughInterface(entry.second, said)) - { - continue; - } - - factsEffective[entry.second] = said; - } - } - - // the members of a family that disagree lose their facts: a call through the family is - // given only what all of them say. A closed interface's slot is such a family too. - auto demote = [&](llvm::ArrayRef members) { - llvm::SmallVector seen; - for (auto member : members) - { - auto funcOp = functions.lookup(member.getValue()); - seen.push_back(funcOp ? factsEffective.lookup(funcOp) : Facts()); - } - - auto changed = false; - if (llvm::all_equal(seen)) - { - return changed; - } - - for (auto member : members) - { - if (auto funcOp = functions.lookup(member.getValue()); - funcOp && !declaresCallerFacts(funcOp) && factsEffective.erase(funcOp)) - { - changed = true; - } - } - - return changed; - }; - - // a member a callback family demoted is never promoted again: each step only adds or only - // takes away, so this settles - demotedCallbacks.clear(); - for (auto changed = true; changed;) - { - changed = false; - for (auto &[key, members] : families) - { - changed = demote(members) || changed; - } - - changed = meetCallbacks(demotedCallbacks) || changed; - - for (auto &[key, family] : interfaceFamilies) - { - if (family.closed) - { - changed = demote(family.members) || changed; - } - } - - for (auto &[key, family] : interfaceFieldFamilies) - { - if (family.closed) - { - changed = demote(family.members) || changed; - } - } - } - } - - Facts callFacts(const Callees &callees) - { - if (!callees.known || callees.instanceOf || callees.funcs.empty()) - { - return {}; - } - - // each member's facts as the call's arguments see them (a closure's body takes its box first) - if (callees.viaCallback) - { - std::optional agreed; - for (auto [funcOp, shift] : llvm::zip(callees.funcs, callees.shifts)) - { - Facts shifted; - if (!shiftFacts(factsEffective.lookup(funcOp), shift, shifted) || (agreed && *agreed != shifted)) - { - return {}; - } - - agreed = shifted; - } - - return agreed.value_or(Facts()); - } - - auto facts = factsEffective.lookup(callees.funcs.front()); - auto agree = llvm::all_of(callees.funcs, [&](mlir_ts::FuncOp callee) { - return factsEffective.lookup(callee) == facts; - }); - return agree ? facts : Facts(); - } - - static void setFacts(mlir::Operation *op, const Facts &facts) - { - auto *context = op->getContext(); - op->removeAttr(OWN_RESULT_BORROWS_ATTR_NAME); - op->removeAttr(OWN_RESULT_RECORD_ATTR_NAME); - op->removeAttr(OWN_PARAMS_ATTR_NAME); - if (!facts.borrows.empty()) - { - op->setAttr(OWN_RESULT_BORROWS_ATTR_NAME, mlir::DenseI32ArrayAttr::get(context, facts.borrows)); - if (facts.record) - { - op->setAttr(OWN_RESULT_RECORD_ATTR_NAME, mlir::UnitAttr::get(context)); - } - } - - if (!facts.owned.empty()) - { - op->setAttr(OWN_PARAMS_ATTR_NAME, mlir::DenseI32ArrayAttr::get(context, facts.owned)); - } - } - - // The calls get the facts their callees agree on; each function gets its own, or the reason it - // lost those its body has, for the error there. - void pinFacts(const llvm::DenseMap &pinned) - { - for (auto &call : calls) - { - setFacts(call.op, pinned.lookup(call.op)); - } - - for (auto &[funcOp, local] : factsLocal) - { - auto effective = factsEffective.lookup(funcOp); - setFacts(funcOp, effective); - funcOp->removeAttr(OWN_FACTS_LOST_ATTR_NAME); - funcOp->removeAttr(OWN_PARAMS_RELEASED_ATTR_NAME); - // a signature's `@keeps` is checked against the body as it is (checkDeclared) - llvm::SmallVector released; - for (auto index : effective.owned) - { - if (!declaresCallerFacts(funcOp) && !llvm::is_contained(takenParams.lookup(funcOp), index)) - { - released.push_back(index); - } - } - - if (!released.empty()) - { - funcOp->setAttr(OWN_PARAMS_RELEASED_ATTR_NAME, mlir::DenseI32ArrayAttr::get(&getContext(), released)); - } - - if (effective.empty() && !local.empty()) - { - // through an interface first: a marker would not help there (interfaceReached) - auto *why = factsThroughInterface(funcOp, local) ? INTERFACE_REACHED_WHY : open.lookup(funcOp); - if (!why && demotedCallbacks.contains(funcOp)) - { - why = "a function passed in its place disagrees"; - } - - funcOp->setAttr(OWN_FACTS_LOST_ATTR_NAME, - mlir::StringAttr::get(&getContext(), why ? why : "an override in its class family disagrees")); - } - } - } - - // ---- Generators that borrow (phase 7b) ---- - // - // A generator over a parameter that holds a block - `function* each(a: number[])` - keeps the - // parameter's cell in a box in its state object's `.captured` field. Under 5b's rule that is an - // escaping closure over a value that is the caller's, an error. Instead the state object - // borrows: the caller owns it, it may not outlive the argument (OWN_RESULT_BOUNDED_ATTR_NAME), - // and its release gives back neither the parameter's value nor any field that only ever holds - // what was read out of it (OWN_BORROWING_FIELDS_ATTR_NAME). A state type is unique to its - // generator - its `next` field's type names a per-site `object_storage` - so a fact per type is - // a fact per generator, which is what the per-type release routine needs. - - struct BorrowingState - { - mlir_ts::FuncOp maker; - int32_t captured = -1; - llvm::SmallVector bounded; - llvm::SetVector fields; - }; - - static int32_t fieldIndex(mlir_ts::TupleType tupleType, llvm::StringRef name) - { - for (auto [index, field] : llvm::enumerate(tupleType.getFields())) - { - if (auto id = mlir::dyn_cast_or_null(field.id); id && id.getValue() == name) - { - return static_cast(index); - } - } - - return -1; - } - - // The box the maker stores into the `.captured` field of the initial state it copies into - // `newOp`'s block: `ts.Store(box, ts.PropertyRef(state, captured))`, `state` a local whose only - // read fills the block. - static mlir::Value boxStoredIntoState(mlir_ts::NewOp newOp, int32_t captured) - { - mlir_ts::StoreOp fill; - for (auto *user : newOp->getUsers()) - { - if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getReference() == newOp.getResult()) - { - fill = storeOp; - } - } - - auto loadOp = fill ? fill.getValue().getDefiningOp() : mlir_ts::LoadOp(); - auto stateOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); - if (!stateOp) - { - return {}; - } - - mlir::Value box; - for (auto *user : stateOp->getUsers()) - { - auto propertyRefOp = mlir::dyn_cast(user); - if (!propertyRefOp || propertyRefOp.getPosition() != captured) - { - continue; - } - - for (auto *fieldUser : propertyRefOp->getUsers()) - { - auto storeOp = mlir::dyn_cast(fieldUser); - if (!storeOp || box) - { - return {}; - } - - box = storeOp.getValue(); - } - } - - auto boxOp = box ? box.getDefiningOp() : mlir_ts::VariableOp(); - return boxOp && boxOp->hasAttr(CAPTURE_BOX_ATTR_NAME) ? box : mlir::Value(); - } - - // Is every caller of `funcOp` a direct call this pass sees? It is private, and each use of its - // symbol is a call, or a closure over it that is only retained and released - `.map(f)` builds - // one and calls its body straight through the box. - bool calledOnlyDirectly(mlir::ModuleOp module, mlir_ts::FuncOp funcOp, bool publicToo = false) - { - if (!funcOp.isPrivate() && !publicToo) - { - return false; - } - - auto uses = mlir::SymbolTable::getSymbolUses(funcOp.getSymNameAttr(), &module.getBodyRegion()); - if (!uses) - { - return false; - } - - for (auto &use : *uses) - { - auto *user = use.getUser(); - if (mlir::isa(user)) - { - continue; - } - - // a method whose signature gives its callers the bound: called through its class's - // vtable or a reference to it, which the facts reach too (declaredByCallees) - if (publicToo && - (isClassVTableEntry(user) || - (mlir::isa(user) && - onlyCalled(user->getResult(0))))) - { - continue; - } - - auto symbolRefOp = mlir::dyn_cast(user); - if (!symbolRefOp) - { - return false; - } - - for (auto *refUser : symbolRefOp->getUsers()) - { - auto boundOp = mlir::dyn_cast(refUser); - if (!boundOp || !llvm::all_of(boundOp->getUsers(), [](mlir::Operation *boundUser) { - return mlir::isa(boundUser); - })) - { - return false; - } - } - } - - return true; - } - - // A parameter's cell: a captured local declared from an argument of `maker`'s entry block, that - // owns nothing (its value is the caller's). - static bool isParameterCell(mlir_ts::VariableOp cellOp, mlir_ts::FuncOp maker, int32_t &index) - { - auto argument = cellOp.getInitializer() ? mlir::dyn_cast(cellOp.getInitializer()) - : mlir::BlockArgument(); - if (!cellOp.getCaptured().value_or(false) || isOwningVariable(cellOp) || !argument || - !argument.getOwner()->isEntryBlock() || argument.getOwner()->getParentOp() != maker.getOperation()) - { - return false; - } - - index = static_cast(argument.getArgNumber()); - return true; - } - - // `ts.Load` of one of `maker`'s parameter slots (isParameterSlot): `index` is its position. - static bool readsParameterSlot(mlir::Value value, mlir_ts::FuncOp maker, int32_t &index) - { - auto loadOp = rootOf(value).getDefiningOp(); - auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); - int position = -1; - if (!varOp || !isParameterSlot(varOp, position) || varOp->getParentOfType() != maker) - { - return false; - } - - index = static_cast(position); - return true; - } - - // `ts.Load(ts.PropertyRef(argument, i))` of one of `maker`'s parameters: `index` is its position. - static bool readsParameterField(mlir::Value value, mlir_ts::FuncOp maker, int32_t &index) - { - auto loadOp = rootOf(value).getDefiningOp(); - auto propertyRefOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); - auto argument = propertyRefOp ? mlir::dyn_cast(propertyRefOp->getOperand(0)) : mlir::BlockArgument(); - if (!argument || !argument.getOwner()->isEntryBlock() || argument.getOwner()->getParentOp() != maker.getOperation()) - { - return false; - } - - index = static_cast(argument.getArgNumber()); - return true; - } - - bool ownsHeap(mlir::Location location, mlir::Type type) - { - MLIRTypeHelper mth(&getContext(), CompileOptions{}); - return mth.ownsHeapMemory(location, type); - } - - void findBorrowingStates(mlir::ModuleOp module) - { - llvm::MapVector states; - llvm::DenseMap made; - module.walk([&](mlir_ts::NewOp newOp) { - auto valueRefType = mlir::dyn_cast(newOp.getType()); - auto tupleType = valueRefType ? mlir::dyn_cast(valueRefType.getElementType()) - : mlir_ts::TupleType(); - if (!tupleType) - { - return; - } - - ++made[tupleType]; - auto captured = fieldIndex(tupleType, CAPTURED_NAME); - auto maker = newOp->getParentOfType(); - // an open maker has callers this pass cannot give the bound to, unless its signature - // gives it to them (`@borrows`) - if (captured < 0 || !maker || - !calledOnlyDirectly(module, maker, /*publicToo=*/maker->hasAttr(OWN_DECLARED_BORROWS_ATTR_NAME))) - { - return; - } - - auto box = boxStoredIntoState(newOp, captured); - if (!box) - { - return; - } - - BorrowingState state{maker, captured}; - for (auto *user : box.getUsers()) - { - auto propertyRefOp = mlir::dyn_cast(user); - if (!propertyRefOp) - { - continue; - } - - for (auto *fieldUser : propertyRefOp->getUsers()) - { - auto storeOp = mlir::dyn_cast(fieldUser); - if (!storeOp) - { - return; - } - - auto value = storeOp.getValue(); - int32_t index = -1; - if (auto cellOp = value.getDefiningOp(); cellOp && isParameterCell(cellOp, maker, index)) - { - auto cellType = mlir::cast(cellOp.getType()); - // a handle parameter's cell holds a count of its own (spec 23.2), which only - // the box's release gives back: a box holding one owns what it holds - if (MLIRTypeHelper::isSharedHandleType(cellType.getElementType())) - { - return; - } - - if (ownsHeap(cellOp.getLoc(), cellType.getElementType())) - { - state.bounded.push_back(index); - } - - continue; - } - - // a copy of what a parameter holds in a field: `.map`'s maker is a closure body - // that copies its own box's fields into the generator's - if (!ownsHeap(value.getLoc(), value.getType())) - { - continue; - } - - // `this`, which the box holds by value (`.this`), not in a cell: read out of - // the parameter's slot - if (readsParameterSlot(value, maker, index)) - { - if (MLIRTypeHelper::isSharedHandleType(value.getType())) - { - return; - } - - if (!llvm::is_contained(state.bounded, index)) - { - state.bounded.push_back(index); - } - - continue; - } - - if (!readsParameterField(value, maker, index)) - { - return; // a box with anything but what the parameters hold owns what it holds - } - - if (!llvm::is_contained(state.bounded, index)) - { - state.bounded.push_back(index); - } - } - } - - if (!state.bounded.empty()) - { - states[tupleType] = state; - } - }); - - // made in one place only: the maker is the one function a bound can come from - states.remove_if([&](auto &entry) { return made[entry.first] != 1; }); - for (auto &[type, state] : states) - { - auto bounded = state.bounded; - llvm::sort(bounded); - borrowsFound[state.maker] = bounded; - } - if (states.empty()) - { - return; - } - - // Each store into a field of a state type, by field: what the field may be given. - llvm::DenseMap, llvm::SmallVector> stored; - module.walk([&](mlir_ts::StoreOp storeOp) { - auto propertyRefOp = storeOp.getReference().getDefiningOp(); - auto tupleType = propertyRefOp ? storageTupleOf(propertyRefOp->getOperand(0).getType()) : mlir_ts::TupleType(); - if (tupleType && states.count(tupleType)) - { - stored[{tupleType, propertyRefOp.getPosition()}].push_back(storeOp.getValue()); - } - }); - - for (auto &[type, state] : states) - { - auto tupleType = mlir::cast(type); - // least fixpoint: a field borrows once everything stored into it is a read of a borrowing - // cell or field - for (auto changed = true; changed;) - { - changed = false; - for (auto [index, field] : llvm::enumerate(tupleType.getFields())) - { - auto position = static_cast(index); - auto found = stored.find({type, position}); - if (position == state.captured || state.fields.contains(position) || found == stored.end() || - !ownsHeap(state.maker.getLoc(), field.type)) - { - continue; - } - - if (llvm::all_of(found->second, [&](mlir::Value value) { return readsBorrowed(value, tupleType, state); })) + // a call this pass cannot resolve still has what its callees' signatures say + // A declaration's `@lends` is a plain borrow to its callers: its body, which would + // say whether the result is a record the caller owns (spec 32), is not here. A + // record's box then leaks, and nothing is freed twice. + auto keeps = declaredByCallees(call.callees, OWN_DECLARED_KEEPS_ATTR_NAME); + facts.owned.assign(keeps.begin(), keeps.end()); + if (auto lends = declaredByCallees(call.callees, OWN_DECLARED_LENDS_ATTR_NAME); !lends.empty()) { - state.fields.insert(position); - changed = true; + llvm::sort(lends); + lends.erase(std::unique(lends.begin(), lends.end()), lends.end()); + facts.borrows = lends; } } - } - } - - auto *context = &getContext(); - llvm::SmallVector entries; - for (auto &[type, state] : states) - { - llvm::SmallVector fields{state.captured}; - fields.append(state.fields.begin(), state.fields.end()); - entries.push_back(mlir::ArrayAttr::get( - context, {mlir::TypeAttr::get(type), mlir::DenseI32ArrayAttr::get(context, fields)})); - state.maker->setAttr(OWN_RESULT_BOUNDED_ATTR_NAME, mlir::DenseI32ArrayAttr::get(context, state.bounded)); - } - - module->setAttr(OWN_BORROWING_FIELDS_ATTR_NAME, mlir::ArrayAttr::get(context, entries)); - } - - // Is `value` a read of what a state of `tupleType` borrows: the value in one of its box's cells - // (`ts.Load` of `ts.Load(ts.PropertyRef(ts.Load(ts.PropertyRef(state, captured)), i))`), or a - // borrowing field's (`ts.Load(ts.PropertyRef(state, p))`)? - bool readsBorrowed(mlir::Value value, mlir_ts::TupleType tupleType, const BorrowingState &state) - { - auto loadOp = rootOf(value).getDefiningOp(); - if (!loadOp) - { - return false; - } - - auto isStateField = [&](mlir::Value ref, int32_t &position) { - auto propertyRefOp = ref.getDefiningOp(); - if (!propertyRefOp || storageTupleOf(propertyRefOp->getOperand(0).getType()) != tupleType) - { - return false; - } - - position = propertyRefOp.getPosition(); - return true; - }; - - int32_t position = -1; - if (isStateField(loadOp.getReference(), position)) - { - return state.fields.contains(position); - } - - // a field of the box the state holds, copied in by value, or a cell read out of it - auto isBoxField = [&](mlir::Value ref) { - auto fieldRef = ref.getDefiningOp(); - auto boxLoad = fieldRef ? fieldRef->getOperand(0).getDefiningOp() : mlir_ts::LoadOp(); - return boxLoad && isStateField(boxLoad.getReference(), position) && position == state.captured; - }; - - if (isBoxField(loadOp.getReference())) - { - return true; - } - - auto cellLoad = loadOp.getReference().getDefiningOp(); - return cellLoad && isBoxField(cellLoad.getReference()); - } - - // ---- Globals given a borrow ---- - - // A global initialized with a read of another global, a field or an element (`let k = a`, - // `let k = h.c`) would be a second owner of a block another owner keeps: the first overwrite - // of either frees what the other still reads, and both give it back at exit. rc counts the - // global's own reference (OwnedReturnConsumptionPass, claimGlobalInitializers); own cannot - // move the value out of its owner, and a global cannot hold a borrow. A handle is counted - // under own too. - void rejectBorrowedGlobalInitializers(mlir::ModuleOp module) - { - module.walk([&](mlir_ts::GlobalOp globalOp) { - auto *block = globalOp.getInitializerBlock(); - auto resultOp = block && !block->empty() ? mlir::dyn_cast(block->getTerminator()) - : mlir_ts::GlobalResultOp(); - if (!resultOp || resultOp->getNumOperands() != 1) - { - return; - } - - auto value = resultOp->getOperand(0); - while (value.getDefiningOp() && isView(value.getDefiningOp())) - { - value = value.getDefiningOp()->getOperand(0); - } - - auto loadOp = value.getDefiningOp(); - auto *place = loadOp ? loadOp.getReference().getDefiningOp() : nullptr; - // an immortal global's value (spec 25) is owned by no one: any number of globals hold it - if (!place || !mlir::isa(place) || - isHandle(value) || isConstantData(value) || isImmortalGlobalRead(value) || - !ownsHeap(value.getLoc(), value.getType())) - { - return; - } - - resultOp.emitError("the initializer of global '") - << globalOp.getSymName() - << "' reads a value another owner keeps; -mm=own cannot make a global a second owner"; - signalPassFailure(); - }); - } - - // ---- Drops ---- - - // Least fixpoint: a function drops nothing until its body or a call it makes shows it may. - void computeDrops() - { - llvm::DenseMap> callsIn; - for (auto &call : calls) - { - callsIn[call.op->getParentOfType()].push_back(&call); - } - - for (auto &entry : functions) - { - auto funcOp = entry.second; - if (!funcOp.isDeclaration() && !dropsInBody(funcOp)) - { - noDrops.insert(funcOp); - } - } - - for (auto changed = true; changed;) - { - changed = false; - for (auto &entry : functions) - { - auto funcOp = entry.second; - if (!noDrops.contains(funcOp)) - { - continue; - } - - for (auto *call : callsIn[funcOp]) - { - if (!callMayDrop(call->callees)) - { - continue; - } - noDrops.erase(funcOp); - changed = true; - break; + if (!facts.empty()) + { + next[call.op] = facts; } } - } - } - bool callMayDrop(const Callees &callees) - { - if (!callees.known) - { - return true; + auto settled = next == pinned; + pinned = std::move(next); + pinFacts(pinned); + if (settled) + { + return; + } } - return !callees.instanceOf && !callees.imported && - llvm::any_of(callees.funcs, [&](mlir_ts::FuncOp callee) { return !noDrops.contains(callee); }); + factsEffective.clear(); + pinFacts(llvm::DenseMap()); } - // ---- Globals a call may assign ---- - - // The globals each call may give a new value, pinned on the call for the inference - // (OWN_ASSIGNS_GLOBALS_ATTR_NAME, OWN_ASSIGNS_ANY_GLOBAL_ATTR_NAME): a store to a global, or its - // release, in a callee's body, or in a call it makes. A call that may drop and whose callees are - // not known may assign any; one that drops nothing assigns none. A read of a global ends where - // the global is given a new value (checkGlobalRead), and a call that does so ends it as surely as - // a store in the same function: `const c = G; reset(); print(c.x)`. - void pinAssignedGlobals() + void OwnershipSignaturePass::meetFacts() { - llvm::DenseMap> callsIn; - for (auto &call : calls) + factsEffective.clear(); + for (auto &[funcOp, facts] : factsLocal) { - callsIn[call.op->getParentOfType()].push_back(&call); + if (!facts.empty() && !open.contains(funcOp) && !factsThroughInterface(funcOp, facts)) + { + factsEffective[funcOp] = facts; + } } - llvm::DenseMap> assigns; - llvm::DenseSet assignsAny; + // what a signature says stands, open or not; checkDeclared holds the body to it. But not a + // `@keeps` or `@lends` through an interface: the interface's callers are not told + // (interfaceReached). for (auto &entry : functions) { - auto funcOp = entry.second; - funcOp.walk([&](mlir::Operation *op) { - mlir::Value slot; - if (auto storeOp = mlir::dyn_cast(op)) - { - slot = storeOp.getReference(); - } - else if (auto releaseSlotOp = mlir::dyn_cast(op)) + if (declaresCallerFacts(entry.second)) + { + auto keeps = declared(entry.second, OWN_DECLARED_KEEPS_ATTR_NAME); + Facts said; + said.borrows = declaredLends(entry.second); + said.owned.assign(keeps.begin(), keeps.end()); + // a body here that returns a borrowing record of what `@lends` names: callers in this + // module own the box (spec 32); checkDeclared holds the body to the parameters + auto local = factsLocal.lookup(entry.second); + said.record = !said.borrows.empty() && local.record && local.borrows == said.borrows; + if (factsThroughInterface(entry.second, said)) { - slot = releaseSlotOp.getSlot(); + continue; } - if (auto addressOfOp = slot ? slot.getDefiningOp() : mlir_ts::AddressOfOp()) - { - assigns[funcOp].insert(addressOfOp.getGlobalName()); - } - }); + factsEffective[entry.second] = said; + } } - // what a call may assign, from what its callees do so far - auto assignedBy = [&](Call &call, llvm::SetVector &globals) { - if (!callMayDrop(call.callees)) + // the members of a family that disagree lose their facts: a call through the family is + // given only what all of them say. A closed interface's slot is such a family too. + auto demote = [&](llvm::ArrayRef members) { + llvm::SmallVector seen; + for (auto member : members) { - return false; + auto funcOp = functions.lookup(member.getValue()); + seen.push_back(funcOp ? factsEffective.lookup(funcOp) : Facts()); } - if (!call.callees.known) + auto changed = false; + if (llvm::all_equal(seen)) { - return true; + return changed; } - auto any = false; - for (auto callee : call.callees.funcs) + for (auto member : members) { - any |= assignsAny.contains(callee); - if (auto found = assigns.find(callee); found != assigns.end()) + if (auto funcOp = functions.lookup(member.getValue()); + funcOp && !declaresCallerFacts(funcOp) && factsEffective.erase(funcOp)) { - globals.insert(found->second.begin(), found->second.end()); + changed = true; } } - return any; + return changed; }; + // a member a callback family demoted is never promoted again: each step only adds or only + // takes away, so this settles + demotedCallbacks.clear(); for (auto changed = true; changed;) { changed = false; - for (auto &entry : functions) + for (auto &[key, members] : families) { - auto funcOp = entry.second; - for (auto *call : callsIn[funcOp]) + changed = demote(members) || changed; + } + + changed = meetCallbacks(demotedCallbacks) || changed; + + for (auto &[key, family] : interfaceFamilies) + { + if (family.closed) { - llvm::SetVector globals; - if (assignedBy(*call, globals)) - { - changed |= assignsAny.insert(funcOp).second; - } + changed = demote(family.members) || changed; + } + } - for (auto global : globals) - { - changed |= assigns[funcOp].insert(global); - } + for (auto &[key, family] : interfaceFieldFamilies) + { + if (family.closed) + { + changed = demote(family.members) || changed; } } } + } - for (auto &call : calls) + OwnershipSignaturePass::Facts OwnershipSignaturePass::callFacts(const Callees &callees) + { + if (!callees.known || callees.instanceOf || callees.funcs.empty()) { - llvm::SetVector globals; - if (assignedBy(call, globals)) - { - call.op->setAttr(OWN_ASSIGNS_ANY_GLOBAL_ATTR_NAME, mlir::UnitAttr::get(&getContext())); - continue; - } + return {}; + } - if (globals.empty()) + // each member's facts as the call's arguments see them (a closure's body takes its box first) + if (callees.viaCallback) + { + std::optional agreed; + for (auto [funcOp, shift] : llvm::zip(callees.funcs, callees.shifts)) { - continue; + Facts shifted; + if (!shiftFacts(factsEffective.lookup(funcOp), shift, shifted) || (agreed && *agreed != shifted)) + { + return {}; + } + + agreed = shifted; } - llvm::SmallVector names; - for (auto global : globals) + return agreed.value_or(Facts()); + } + + auto facts = factsEffective.lookup(callees.funcs.front()); + auto agree = llvm::all_of(callees.funcs, [&](mlir_ts::FuncOp callee) { + return factsEffective.lookup(callee) == facts; + }); + return agree ? facts : Facts(); + } + + void OwnershipSignaturePass::setFacts(mlir::Operation *op, const Facts &facts) + { + auto *context = op->getContext(); + op->removeAttr(OWN_RESULT_BORROWS_ATTR_NAME); + op->removeAttr(OWN_RESULT_RECORD_ATTR_NAME); + op->removeAttr(OWN_PARAMS_ATTR_NAME); + if (!facts.borrows.empty()) + { + op->setAttr(OWN_RESULT_BORROWS_ATTR_NAME, mlir::DenseI32ArrayAttr::get(context, facts.borrows)); + if (facts.record) { - names.push_back(mlir::StringAttr::get(&getContext(), global)); + op->setAttr(OWN_RESULT_RECORD_ATTR_NAME, mlir::UnitAttr::get(context)); } + } - call.op->setAttr(OWN_ASSIGNS_GLOBALS_ATTR_NAME, mlir::ArrayAttr::get(&getContext(), names)); + if (!facts.owned.empty()) + { + op->setAttr(OWN_PARAMS_ATTR_NAME, mlir::DenseI32ArrayAttr::get(context, facts.owned)); } } - // An argument read out of a global (`run(G)`, `G.m()`) is lent to the callee, and the callee may - // free it under its own parameter: `function run(h: C) { G = mk(); print(h.x); }`. The caller's - // borrow ends at the call that takes it, so nothing in the caller sees that, and in the callee - // the parameter is not known to be the global's. A call that may drop is an error for such an - // argument, when something assigns that global. (A field of a global given that way is a - // borrow of a place, which the inference already bounds by the call.) - void rejectGlobalArgumentsDropped() + void OwnershipSignaturePass::pinFacts(const llvm::DenseMap &pinned) { for (auto &call : calls) { - if (!callMayDrop(call.callees)) - { - continue; - } + setFacts(call.op, pinned.lookup(call.op)); + } - for (auto argument : callArgs(call.op)) + for (auto &[funcOp, local] : factsLocal) + { + auto effective = factsEffective.lookup(funcOp); + setFacts(funcOp, effective); + funcOp->removeAttr(OWN_FACTS_LOST_ATTR_NAME); + funcOp->removeAttr(OWN_PARAMS_RELEASED_ATTR_NAME); + // a signature's `@keeps` is checked against the body as it is (checkDeclared) + llvm::SmallVector released; + for (auto index : effective.owned) { - // a method's object: `this` of the method bound to it - if (auto getThisOp = argument.getDefiningOp()) + if (!declaresCallerFacts(funcOp) && !llvm::is_contained(takenParams.lookup(funcOp), index)) { - auto *boundOp = getThisOp->getOperand(0).getDefiningOp(); - if (mlir::isa_and_nonnull(boundOp)) - { - argument = boundOp->getOperand(0); - } + released.push_back(index); } + } - // an immortal global (spec 25) is written by its initializer only, and that runs once - // (a static's is a store in its static constructor, outside any loop, which - // writtenGlobals counts), and no release frees its block - if (isHandle(argument) || isImmortalGlobalRead(argument) || !ownsHeap(call.op->getLoc(), argument.getType())) - { - continue; - } + if (!released.empty()) + { + funcOp->setAttr(OWN_PARAMS_RELEASED_ATTR_NAME, mlir::DenseI32ArrayAttr::get(&getContext(), released)); + } - auto loadOp = rootOf(argument).getDefiningOp(); - auto global = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::AddressOfOp(); - if (!global || !writtenGlobals.contains(global.getGlobalName())) + if (effective.empty() && !local.empty()) + { + // through an interface first: a marker would not help there (interfaceReached) + auto *why = factsThroughInterface(funcOp, local) ? INTERFACE_REACHED_WHY : open.lookup(funcOp); + if (!why && demotedCallbacks.contains(funcOp)) { - continue; + why = "a function passed in its place disagrees"; } - call.op->emitError("a value read out of the global '") - << global.getGlobalName() - << "' is lent to a call that may release or overwrite it; -mm=own cannot prove it outlives the call"; - signalPassFailure(); + funcOp->setAttr(OWN_FACTS_LOST_ATTR_NAME, + mlir::StringAttr::get(&getContext(), why ? why : "an override in its class family disagrees")); } } } - // ---- Calls in a global's initializer ---- + bool OwnershipSignaturePass::ownsHeap(mlir::Location location, mlir::Type type) + { + return own_facts::ownsHeap(location, type); + } - // A global's region is no function: its calls get no facts pinned, and the inference never sees - // it, so its calls were read with the defaults - an owned result, borrowed arguments. A result - // that borrows an argument (`const c = child(n)`, `id(new V(1))`) then made the global a second - // owner of a block its argument's owner keeps, or kept one the region freed; and a fresh - // argument a callee keeps (`new N(new V(4))`, the constructor keeping `c`) was released at the - // end of the region, under the object that kept it. A kept argument made in the region and used - // by nothing after the call moves into the callee: its release goes. Anything else is an error. - void ownGlobalInitializerCalls(mlir::ModuleOp module) + void OwnershipSignaturePass::computeDrops() { - module.walk([&](mlir_ts::GlobalOp globalOp) { - // collected first: a move erases a release the walk has not reached yet - llvm::SmallVector calls; - globalOp.walk([&](mlir::Operation *op) { - if (isCall(op)) - { - calls.push_back(op); - } - }); + llvm::DenseMap> callsIn; + for (auto &call : calls) + { + callsIn[call.op->getParentOfType()].push_back(&call); + } + + for (auto &entry : functions) + { + auto funcOp = entry.second; + if (!funcOp.isDeclaration() && !dropsInBody(funcOp)) + { + noDrops.insert(funcOp); + } + } - for (auto *op : calls) + for (auto changed = true; changed;) + { + changed = false; + for (auto &entry : functions) { - auto callees = resolve(op); - auto facts = callFacts(callees); - auto calleeName = callees.funcs.empty() ? llvm::StringRef("the callee") : callees.funcs.front().getSymName(); - if (!facts.borrows.empty()) + auto funcOp = entry.second; + if (!noDrops.contains(funcOp)) { - op->emitError("the initializer of global '") - << globalOp.getSymName() << "' holds the result of '" << calleeName - << "', which borrows its argument; a global cannot hold a borrow under -mm=own"; - signalPassFailure(); continue; } - auto args = callArgs(op); - llvm::SmallPtrSet given; - for (auto index : facts.owned) + for (auto *call : callsIn[funcOp]) { - if (index < 0 || index >= static_cast(args.size()) || - !given.insert(rootOf(args[index])).second || !moveIntoCallee(op, args[index])) + if (!callMayDrop(call->callees)) { - op->emitError("the initializer of global '") - << globalOp.getSymName() << "' gives '" << calleeName - << "' a value it keeps, but -mm=own cannot move it there"; - signalPassFailure(); - break; + continue; } + + noDrops.erase(funcOp); + changed = true; + break; } } - }); + } } - // `value` was made in the call's block and nothing but its release, if it has one, comes after - // the call: the release goes, and the callee owns the block. Constant data (a string literal) - // is nobody's, and is given as it is. - static bool moveIntoCallee(mlir::Operation *call, mlir::Value value) + bool OwnershipSignaturePass::callMayDrop(const Callees &callees) { - auto root = rootOf(value); - if (isConstantData(root)) + if (!callees.known) { return true; } - if (!isFresh(root) || root.getDefiningOp()->getBlock() != call->getBlock()) - { - return false; - } - - mlir::Operation *release = nullptr; - auto usedAfter = false; - forEachUse(root, [&](mlir::Operation *user, mlir::Value) { - if (user == call) - { - return; - } - - if (user->getBlock() != call->getBlock()) - { - usedAfter = true; - } - else if (call->isBeforeInBlock(user)) - { - usedAfter = usedAfter || release || !mlir::isa(user); - release = mlir::isa(user) ? user : release; - } - }); - - if (usedAfter) - { - return false; - } - - if (release) - { - release->erase(); - } - - return true; + return !callees.instanceOf && !callees.imported && + llvm::any_of(callees.funcs, [&](mlir_ts::FuncOp callee) { return !noDrops.contains(callee); }); } - // ---- Export ---- - - // A library says, beside its memory-model marker, which of its exported functions destroy - // nothing (SHARED_LIB_OWN_FACTS). The global is a copy of the marker `__tsmm_own_` - // under another name and with another value, so it is exported exactly as the marker is, and - // MLIRGen emits the same under own as under rc. No marker, nothing is exported. - void exportNoDrops() + void OwnershipSignaturePass::exportNoDrops() { auto markerPrefix = std::string(SHARED_LIB_MEMORY_MODEL) + "own_"; mlir_ts::GlobalOp marker; @@ -5842,11 +1112,7 @@ class OwnershipSignaturePass : public mlir::PassWrapper work{ref}; @@ -5912,27 +1174,15 @@ class OwnershipSignaturePass : public mlir::PassWrapper()) - { - return true; - } - - if (auto propertyRefOp = value.getDefiningOp()) - { - work.push_back(propertyRefOp.getObjectRef()); - continue; - } - - if (auto elementRefOp = value.getDefiningOp()) + if (auto place = placeBase(value)) { - work.push_back(elementRefOp.getArray()); - continue; - } + // a handle's payload: any other handle may reach it (spec 23.3) + if (place.kind == PlaceKind::SharedValue) + { + return true; + } - if (auto interfaceRefOp = value.getDefiningOp(); interfaceRefOp && isInterfaceField(value)) - { - work.push_back(interfaceRefOp.getInterfaceVal()); + work.push_back(place.base); continue; } @@ -5991,7 +1241,7 @@ class OwnershipSignaturePass : public mlir::PassWrapper(user); @@ -5999,9 +1249,7 @@ class OwnershipSignaturePass : public mlir::PassWrapper mlir_ts::createOwnershipSignaturePass(bool executable) { - return std::make_unique(executable); + return std::make_unique(executable); } diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.h b/tslang/lib/TypeScript/OwnershipSignaturePass.h new file mode 100644 index 000000000..231a21ac1 --- /dev/null +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.h @@ -0,0 +1,982 @@ +// OwnershipSignaturePass, the callee facts of -mm=own. Its methods are defined across the +// lib/TypeScript/OwnershipSignature*.cpp files, one analysis each; templates stay here. + +#ifndef MLIR_TYPESCRIPT_OWNERSHIPSIGNATUREPASS_H +#define MLIR_TYPESCRIPT_OWNERSHIPSIGNATUREPASS_H + +#include "mlir/Pass/Pass.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/SymbolTable.h" +#include "mlir/Interfaces/FunctionInterfaces.h" + +#include "TypeScript/TypeScriptDialect.h" +#include "TypeScript/TypeScriptOps.h" +#include "TypeScript/Passes.h" +#include "TypeScript/Defines.h" +#include "TypeScript/MLIRLogic/MLIRTypeHelper.h" + +#include "OwnershipFacts.h" + +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/MapVector.h" +#include "llvm/ADT/SetVector.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringMap.h" +#include "llvm/ADT/StringSet.h" + +#include + +namespace mlir_ts = mlir::typescript; + +namespace own_signature +{ + +using namespace own_facts; + +// The callee facts of -mm=own (spec 2.4, 3.2), computed over the whole module before the +// per-function inference runs. Each call the pass can resolve - a direct call, a virtual call +// whose candidates are all defined here, the `.instanceOf` slot - gets the facts its candidates +// agree on, pinned on the call op, so the inference reads them without looking anything up. +// +// `__own_no_drops` goes one way: a call with no facts may destroy anything, which can only turn a +// program into an error. The facts a callee's own body relies on - a parameter it keeps, a result +// that borrows an argument - go the other way: a caller that does not know them borrows the +// argument and releases it, or releases a result it does not own, a double free either way. So a +// function has those only when every call that can reach it is one this pass resolved (the closed +// world, see `open`). +// +// `__own_no_drops` alone crosses a module boundary: a library built under own exports the names of +// its exported functions that have it (SHARED_LIB_OWN_FACTS), and a module importing it calls them +// knowing it. The other two cannot: a module linking the library statically re-parses its source +// and sees no facts at all, and a caller that does not know them frees twice. +class OwnershipSignaturePass : public mlir::PassWrapper> +{ + public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(OwnershipSignaturePass) + + // `executable`: the module is built into a program (`--emit=exe`, or the JIT's entry module), + // whose exports no other module sees (USER decision, spec 31). Unknown is a library. + explicit OwnershipSignaturePass(bool executable = false) : executable(executable) + { + } + + bool executable = false; + + // A global is a root (rc 5ai): what its initializer makes moves into it, and nothing gives it + // back. rc retains a fresh value for its `ts.GlobalResult`, and that retain is the move: it + // goes. The per-function inference never sees a global's region, so any other retain there is + // reported here, by the global's name, rather than left to the lowering's "left a retain + // behind". + // Every use of `value` ends in the global's `ts.GlobalResult`: given to it, or an element of a + // fresh array literal that is (`const nested = [[1, 2], [3]]`), or the payload of a + // `new Shared(x)` (`let g = new Shared({...})`). Its retains are judged on their own. + static bool movesIntoResult(mlir::Value value); + + // A record copied out of a local a `ts.Constant` initializes, whose owning fields are given + // nothing that holds a block: it holds constants only - numbers, immortal string literals, + // functions - and nobody else's reference. + static bool holdsOnlyConstants(mlir::Value value); + + // Whether `op` may run more than once in one run of its function: it is inside a loop op, or its + // block - or the block of a region op holding it - is on a cycle of its region's CFG, a path from + // the block's successors back to it. (By this pass the affine lowering has made every loop a CFG + // cycle of the function body; the loop op is kept for safety.) + static bool mayRunAgain(mlir::Operation *op); + + // The immortal globals (OWN_IMMORTAL_GLOBAL_ATTR_NAME, spec 25). A global of a class type, + // not exported, is immortal when only its initializer writes it, once, and gives it a fresh + // object, nothing, or another immortal global's value. Its initializer is either its own region + // (`const c = new C()`) or, for a class static, the one store into it in the class's static + // constructor, which a `ts.GlobalConstructor` runs (the region then yields only the default). + // Top-level script code is such a constructor too, so a module-level `let c: C;` it stores + // into once is one as well. "Once" is how often the store runs, not how many stores there are: + // a store in a loop (`static { for (...) K.w = new V(i); }`) would mark every block it is given + // immortal, and each old one would leak, so a store that may run again leaves the global mortal, + // and each store releases the old value, as for any other global. + // + // The store - the region's, which the lowering makes - marks the block HEAP_BLOCK_IMMORTAL, so + // every block the global ever holds is one no release frees. The value test makes sure the block + // is the global's to keep: an object made right there and given back by nobody. A call's result + // is not one, whatever its callee: these facts are found before the callees' (computeFacts), and + // a global's region is no function the inference checks, so a result that borrows its argument - + // a temporary the region frees, an element of another global's array - would be marked, and + // stored, as the global's own. Such a global stays mortal, and a read of it keeps today's errors. + void findImmortalGlobals(mlir::ModuleOp module); + + void moveIntoGlobals(mlir::ModuleOp module); + + void runOnOperation() override; + + private: + // The function a closure runs, when it names one defined here. + mlir_ts::FuncOp closureBody(mlir_ts::CreateBoundFunctionOp boundOp); + + // The fields of a closure body's capture box whose cell it assigns (OWN_ASSIGNS_CAPTURES_ATTR_NAME), + // directly or through a closure it builds over the cell. Every field, when the body is unknown or + // a cell goes anywhere this does not follow. + llvm::SetVector assignedCaptures(mlir_ts::FuncOp funcOp, int depth = 0); + + // Is the variable behind this cell assigned - here, or by a closure built over it? + bool cellAssigned(mlir::Value cell, int depth); + + llvm::DenseMap> assignedCache; + + // What a call may reach: known, when every candidate is a function defined in this module (or + // the call goes through the `.instanceOf` slot, or reaches a function another module says + // destroys nothing); else anything. + struct Callees + { + bool known = false; + bool instanceOf = false; + bool imported = false; + // a function read out of a field that only ever holds it (fieldFunctions) + bool fieldFunction = false; + // the slot of a closed interface (resolveInterfaceSlot) + bool viaInterface = false; + // a parameter of a closed function that callers pass only functions of this module + // (callbackFamilies), with each member's shift: a closure's body takes its capture box + // first, so its parameter k is the call's argument k - 1 + bool viaCallback = false; + llvm::SmallVector shifts; + // the name the imported function is exported under + llvm::StringRef importedName; + llvm::SmallVector funcs; + // the symbols the call names, whether or not it is known: a direct callee, or every + // member of a virtual slot's family (for the facts a declaration says, declaredFacts) + llvm::SmallVector named; + }; + + struct Call + { + mlir::Operation *op; + Callees callees; + }; + + llvm::StringMap functions; + llvm::StringMap globals; + // Globals whose address is used other than to load from it: what they hold may change. + llvm::StringSet<> writtenGlobals; + llvm::SmallVector calls; + + // Functions of imported libraries that destroy nothing a caller can reach, by the name they + // are exported under (SHARED_LIB_OWN_NO_DROPS_ATTR_NAME). + llvm::StringSet<> importedNoDrops; + + // (tuple type, field) -> the one function an object of that type holds in that field. A + // generator's state object holds its `next` so. + own_facts::FieldFunctions fieldFunctions; + + // Class names, from their vtables, to split a method's symbol into class and method name. + llvm::StringSet<> classNames; + // (vtable position, method name) -> the functions at that position under that name, in every + // class vtable of the module: what a virtual call through that slot may reach. + std::map, llvm::SmallVector> families; + + // Functions whose callers are not all known - a caller outside the module, or a reference to + // the symbol that is not a call this pass resolves - and why, for the error in their body. + llvm::DenseMap open; + + // Methods an interface's vtable holds (`...vtbl`, or `...vtbl` + // for an object literal) whose calls through the interface are not resolved: the slot's family + // is open (interfaceFamilies), or the vtable is one this pass cannot tie to an interface. Such + // a call (`ts.InterfaceSymbolRef`, then an indirect call) is given none of the method's facts: a + // caller that hands it an argument it keeps still releases it, and a caller of one whose result + // borrows an argument or `this` releases the borrow as its own (a double free either way). So a + // method reached this way that keeps a parameter or returns a borrow - by its body or by `@keeps` + // / `@lends` - loses its facts, and the body's error says why (`is a parameter, kept here, but + // -mm=own cannot make the callers of ... give it up`, `is a parameter, returned here, but -mm=own + // cannot tell the callers of ... that the result borrows it`). A method reached only through + // closed interfaces keeps its facts: their calls get them (spec 31). + llvm::DenseSet interfaceReached; + + // ---- Interfaces closed in this module (G9, spec 31) ---- + // + // A call through an interface reaches whatever function the slot of the interface value's vtable + // holds. When every such vtable is one this module defines and every interface value is made + // from one of them, the slot holds one of a known set of functions - the slot's family - and the + // call is resolved to it like a virtual call of a private class family: it gets the facts its + // members agree on (meetFacts demotes a family that disagrees), and `__own_no_drops` when every + // member destroys nothing. Anything this cannot see opens the interface, and then the call is + // what it was: no facts, and its members reached that way lose theirs (interfaceReached). + // + // Open, for an interface I: + // - another module can see it: its name in an exported declaration (`__decls`), or `!ts.iface<@I>` + // in the type of a function or global another module can reach - an exported or declared + // function, a global that is not private, or is exported or imported; + // - an I value made other than by `ts.NewInterface` over a vtable this pass reads (the global, a + // heap copy of it, or the one an object's class vtable holds for I): a cast from another + // interface (`J` to `I`, which opens `J` too: its members are reached through `I`), from + // `any` or an opaque pointer (which opens every interface put into one), from a tuple or array + // that does not hold I already (castMakesInterface), a `ts.InterfaceRef` used as a value; + // - a vtable of I written after its initializer, or read other than to make an interface value; + // - a class vtable that holds a vtable of I and is not private (an exported class: another + // module may extend it); + // - and, per slot, a member that is not a function this module defines, a slot that holds no + // function (a field's offset, a thunk's cell), or a member that says `@keeps` or `@lends` (its + // facts are its signature's, which a family cannot change). A member that is not private (a + // `public` method) is open (findOpen): it has no facts of its own to give the family. + struct InterfaceFamily + { + // the slot's name, which every vtable of the interface agrees on + mlir::StringAttr field; + llvm::SmallVector members; + bool closed = true; + }; + + // (interface, vtable position) -> the functions every vtable of the interface holds there + std::map, InterfaceFamily> interfaceFamilies; + + // The words of the module's exported declarations (none in an executable), the classes a class + // vtable is the own vtable of, and the classes another module can name: exported, or in the + // type of an exported or declared function or of an exported or imported global (findOpen). + llvm::StringSet<> exportWords; + llvm::StringSet<> ownClasses; + llvm::StringSet<> nameableClasses; + + // A field of function type (`f: (x: number) => number`), called through the interface: the slot + // holds the offset of a field of each implementing object literal, and the call goes to whatever + // function that field holds. When every vtable's slot is such an offset and every function ever + // stored into those fields is one this module defines - the literal's own (`{ f: function ... }`, + // in its `ts.Constant`), or one stored into the field later, through the object or through the + // interface - the call is resolved to them (spec 31). A store of anything else (a parameter, a + // call's result, a closure), a field reached other than to be read or stored, or an object of + // the literal's type made other than by the literal opens the slot. A class implementing the + // field holds a thunk's cell, which this does not follow: open. + struct InterfaceFieldFamily + { + mlir::StringAttr field; + // (object literal's storage tuple, field index) of each vtable + llvm::SmallVector, 2> targets; + llvm::SmallVector members; + bool closed = true; + }; + + std::map, InterfaceFieldFamily> interfaceFieldFamilies; + + // interface name -> the tuple types of the vtables its values are made with, when every one of + // them is read here (holdsFunction: what a value of the interface carries) + llvm::StringMap> interfaceVTableTypes; + + // `ts.PropertyRef(ts.Cast(null), i)`, seen through casts: the offset of field `i` of an object of + // that type, as an object literal's interface vtable holds it. Null for anything else. + std::pair slotField(mlir::Value value); + + // What every object-literal field an interface's field slot may be holds (InterfaceFieldFamily). + void collectFieldFunctions(mlir::ModuleOp module); + + // The interfaces mentioned by a type as values: the interface itself, a union or optional, a + // tuple's fields, an array's elements, what a reference points to. Not a class's or an object's + // fields (a reference to a block made elsewhere), nor a function's parameters and result. + static void valueInterfaces(mlir::Type type, llvm::StringSet<> &found, int depth = 0); + + // Every interface a type mentions anywhere, a class's fields and a function's parameters + // included: what another module handed a value of this type could reach. With `classes`, the + // classes it mentions too (a base class is its derived class's first field). + static void anyInterfaces(mlir::Type type, llvm::StringSet<> &found, llvm::DenseSet &seen, + llvm::StringSet<> *classes = nullptr); + + // The function values a value of this type holds: the type itself, a union's or an optional's + // member, a tuple's field, an array's element. Not what a reference points to (a vtable's + // address seen as an opaque pointer is the compiler's own, not a value). + static void functionValues(mlir::Type type, llvm::SmallVectorImpl &found, int depth = 0); + + // A function type's parameters and results, whatever kind of function it is. + static std::pair, llvm::SmallVector> signatureOf(mlir::Type type); + + // Does a cast reinterpret a function value - from `any` or `unknown`, or as a function of another + // signature? Then an interface value its parameters or + // results hold crosses unseen (a `(o: Op2) => ...` called as `(o: Op) => ...`): `interfaces` + // gets every interface either side's signatures mention, and `boxed` the input's when it is put + // into `any`. + static bool castReinterpretsFunction(mlir::Type in, mlir::Type out, llvm::StringSet<> &interfaces, bool &boxing); + + // The interfaces an interface member's type mentions: a method's or a function field's + // parameters and result, a field's type, anything they hold (a class's fields included). + static void memberTypeInterfaces(mlir::Type type, llvm::StringSet<> &found); + + static bool isNullish(mlir::Type type); + + // May a cast from `in` to `out` make an interface value out of something that was not that + // interface's value already? A union or optional narrowed to its interface member (whose other + // members are null or undefined) or widened from it, null given a nullable interface type, a + // class or object seen as another (the same block, its fields as they are) and a function seen as + // another are not; a cast from another interface, `any`, an opaque pointer, or a tuple or an + // array whose element is not the same interface is. + static bool castMakesInterface(mlir::Type in, mlir::Type out, int depth = 0); + + // The function a vtable slot holds: `ts.SymbolRef`, seen through casts. Null for anything else. + static mlir::StringAttr slotFunction(mlir::Value value); + + static mlir_ts::AddressOfOp addressOfThroughCasts(mlir::Value value); + + // The words of the module's exported declarations (`__decls*` globals): what another module + // can name. + static llvm::StringSet<> exportedWords(mlir::ModuleOp module); + + // the vtables: an interface's (`ts.InsertProperty` of each slot, slot 0 `.instanceOf` of a + // class that is not the vtable's owner, or `.instanceOf.none` of an object literal), and the + // slots of a class's own vtable that hold the address of one of its interface vtables + struct InterfaceVTable + { + mlir_ts::TupleType type; + llvm::DenseMap slots; + // a field's slot: the object literal's field it is the offset of + llvm::DenseMap> fields; + bool written = false; + }; + + // What collectInterfaceFamilies learns of the module, one step after another. + struct InterfaceScan + { + // the interfaces open whatever their vtables hold + llvm::StringSet<> open; + llvm::StringMap vtables; + // `..vtbl` globals that are neither (no `.instanceOf` this reads in slot 0) + llvm::SmallVector unread; + // interface name -> the interface vtables class vtables hold for it, read through an + // object's class at run time (`ts.VTableOffsetRef` of the object's vtable) + llvm::StringMap> classHeld; + // interface name -> the vtables its values are made with + llvm::StringMap> madeWith; + // An interface value made out of another one reaches the other's members: both are open. One + // made out of `any` or an opaque pointer may be any interface value put into one: every + // interface so boxed is open with it, and every interface at all when it is untraceable. + bool allOpen = false; + llvm::StringSet<> boxed; + llvm::StringSet<> unboxedInto; + bool unboxedFunction = false; + // the interfaces a cast makes a value of out of another interface's or an opaque one, and + // the ones it came from: closed over their members' types below + llvm::StringSet<> castOpened; + // interface -> the interfaces its members' types mention: its vtables' slots and what is + // read out of its values (`ts.InterfaceSymbolRef`), function signatures and class fields + // included + llvm::StringMap> memberInterfaces; + llvm::DenseSet tied; + + void openAll(const llvm::StringSet<> &names) + { + for (auto &entry : names) + { + open.insert(entry.getKey()); + } + } + }; + + void collectInterfaceFamilies(mlir::ModuleOp module); + + // The vtables: each interface vtable's slots, and the interface vtables a class's own vtable holds. + void readInterfaceVTables(mlir::ModuleOp module, InterfaceScan &scan); + + // What makes an interface value, out of which vtable; the casts that box, unbox or reinterpret one; + // the interfaces its members' types mention; the vtables written. + void traceInterfaceValues(mlir::ModuleOp module, InterfaceScan &scan); + + // The interfaces another module sees, and the ones a value boxed into `any` may be unboxed into. + void openExposedInterfaces(mlir::ModuleOp module, InterfaceScan &scan); + + // Every interface the members of one made out of another mention, recursively. + void openThroughMembers(InterfaceScan &scan); + + // An interface with a vtable this pass could not tie to its values. + void openLooseVTables(InterfaceScan &scan); + + // The families of each interface's slots - its methods and its function-typed fields - closed + // where nothing opened the interface. + void buildInterfaceFamilies(InterfaceScan &scan); + + // What an open family holds, and what a vtable no interface value is made with holds. + void markInterfaceReached(InterfaceScan &scan); + static constexpr const char *INTERFACE_REACHED_WHY = + "it is reached through an interface, whose calls are not given its facts"; + + // The parameters a function's result borrows and the parameters it keeps (spec 2.4); absent is + // rc's convention. A plain borrow names one parameter; a borrowing record (`record`, spec 32) + // any number, and its caller owns the box. + struct Facts + { + llvm::SmallVector borrows; + llvm::SmallVector owned; + bool record = false; + + bool empty() const + { + return borrows.empty() && owned.empty(); + } + + bool operator==(const Facts &other) const + { + return borrows == other.borrows && owned == other.owned && record == other.record; + } + + bool operator!=(const Facts &other) const + { + return !(*this == other); + } + }; + + // Each function's facts, from its body (local) and as its callers see them (effective: closed, + // and agreed by every family it is in). + llvm::DenseMap factsLocal; + llvm::DenseMap factsEffective; + // The parameters each body takes into something - a field, an element, a global, a keeping call, + // an owning local it assigns (localOwnedParams). A kept parameter that is not one of them is + // released on the paths that do not move it (OWN_PARAMS_RELEASED_ATTR_NAME, spec 33). + llvm::DenseMap> takenParams; + + // ---- Facts a signature says (`@keeps`, `@borrows`, `@lends`) ---- + // + // A function that says them has them whoever calls it: a caller in another module reads them + // from the declaration it re-parses. Its body is checked against them instead (checkDeclared). + + static llvm::ArrayRef declared(mlir::Operation *funcOp, llvm::StringRef name); + + static bool declaresFacts(mlir::Operation *funcOp); + + // The facts `@keeps` and `@lends` say (Facts): what callers move in, and what the result is a + // borrow of. + static bool declaresCallerFacts(mlir::Operation *funcOp); + + // The parameters `@lends` names: one for a plain borrow, any number for a borrowing record + // (spec 32). + static llvm::SmallVector declaredLends(mlir::Operation *funcOp); + + // What every function a call names declares under `name`, when they all declare the same; + // nothing when one of them does not. + llvm::SmallVector declaredByCallees(const Callees &callees, llvm::StringRef name); + + // The parameters, for an error, by the names MLIRGen recorded beside the marker. + static std::string describeParams(mlir_ts::FuncOp funcOp, llvm::ArrayRef indices); + + // Functions this module defines that `@borrows`, and those whose state was found to borrow. + llvm::DenseMap> borrowsFound; + + // The argument of `funcOp` that `value` is, or a field of: its own value, its slot's, or a + // read out of either. -1 for anything else. + static int32_t parameterOf(mlir::Value value, mlir_ts::FuncOp funcOp); + + // ---- The iterator protocol ---- + // + // `it.next()` returns a `{value, done}` record, and under own the caller owns what it holds, + // whoever implements it: a generator's `next` moves what it yields into the record (a yield of + // a borrow is an error in its body), and any other `next` is a function whose callers this pass + // cannot all see, which may not return a borrow. So the result is fresh even when the call goes + // through the `Iterator` interface, or into another module - the default library's `matchAll`. + + // A call of a function named `next` - read through an interface, out of a field, or by its + // symbol - whose one result is a `{value, done}` record. + static bool isIteratorNext(mlir::Operation *op); + + // A generator another module made: its state object's type is only its shape here, and what each + // field owns - a local, a yielded value now the caller's, what it borrows from its arguments - is + // known in that module alone. A release routine made here from the shape would give back what is + // not the state's. So this module's gives back none of it: only the `.captured` box's cells, and + // their values too unless the maker says it borrows them (`@borrows`). What the generator's own + // locals still hold when it is given up early leaks rather than being freed twice. + void findImportedStates(mlir::ModuleOp module); + + // A call whose result is a generator's state object (an object whose fields are `.step` and + // `next`): a generator function made it, a `ts.New` block it hands over (spec 18.1), which the + // caller owns - in another module too, as the iteration that follows needs. + static bool makesGeneratorState(mlir::Operation *op); + + // storageTupleOf, without the pass's context: an object's storage, a reference's or a tuple + static mlir_ts::TupleType storageTupleOfType(mlir::Type type); + + // ---- Fields a generator yields ---- + // + // A generator's locals are fields of its state object, and `yield groups` puts what one holds into + // the `{value, done}` record its `next` returns: a borrow of the state, which the caller cannot + // keep. When the field is read for nothing but yields, and each value stored into it is yielded + // before the generator can stop (later in the same block), the yield moves the value instead: + // the read is fresh (OWN_FRESH_RESULT_ATTR_NAME) and the field owns nothing + // (OWN_YIELDED_FIELDS_ATTR_NAME). + + // `ts.Store(value, ts.PropertyRef(record, i))`, where `record` is a local a `ts.Constant` + // initializes: a field of a record built to be returned, a yield's. + static bool isRecordFieldStore(mlir::Operation *user, mlir::Value value); + + void findYieldedFields(mlir::ModuleOp module); + + // A function that says `@borrows` and makes no state itself, but returns what a call that has + // a bounded result gives it (`return this.value.matchAll(expr)`): it borrows the parameters + // the call's bounded arguments are, or are read out of. + void findBoundedForwarders(); + + // A body that does other than its signature says, and an override that says other than the + // method it overrides: either way a caller that trusts the signature frees twice or leaks. + void checkDeclared(); + + // Functions that destroy nothing their caller can reach. + llvm::DenseSet noDrops; + + // A class's vtable is a global named `..vtbl` that holds its `..instanceOf`. An + // interface's vtable for a class (`B.I..vtbl`) holds only the methods and is not one: a call + // through an interface does not name what it reaches. + void collectClassVTables(mlir::ModuleOp module); + + // Does a method reached through an open interface keep a parameter or return a borrow + // (interfaceReached)? Then its callers through the interface cannot be told, and it has no facts + // at all. + bool factsThroughInterface(mlir::Operation *funcOp, const Facts &facts); + + mlir_ts::TupleType storageTupleOf(mlir::Type type); + + // The functions a class vtable holds, by position; empty for any other global. + static llvm::SmallVector> classVTableEntries(mlir_ts::GlobalOp globalOp); + + static bool isClassVTableEntry(mlir::Operation *op); + + // `B.get` -> `get`: the symbol with the longest class name that prefixes it taken off. An + // override keeps its base's method name, which is what puts both in one family. + std::string methodName(llvm::StringRef symbol); + + llvm::SmallVector familyOf(int64_t index, mlir::StringAttr identifier); + + // Adds `symbol`'s function to `callees`; false when it is not defined in this module. + bool addDefined(mlir::StringAttr symbol, Callees &callees); + + // A function declared here and defined in a library that exported its `__own_no_drops`: one + // linked with this module, through its import library. Or a C function declared + // `@nodrops` (OWN_DECLARED_NO_DROPS_ATTR_NAME), or one whose parameters and result hold no + // heap block (scalarDeclaration). + bool addImported(mlir::StringAttr symbol, Callees &callees); + + // A number, a boolean, a character, or an enum or a literal of one: a value that holds no block + // and no function of this module, so given to foreign code it hands it neither. Not an opaque + // pointer, a reference, a string, a class, an interface, an object, a tuple, an array, a union + // with such a member, a function, `any` or `unknown`. + static bool scalarType(mlir::Type type); + + // Every parameter and result scalar (a void result too), and no rest parameter. + static bool scalarSignature(mlir_ts::FunctionType functionType); + + // A body-less `declare function` of C (no `export` or import marker: not another TypeScript + // module's, whose declarations carry `export`) whose parameters and result are scalar + // (scalarType): given no block of this module, it can destroy none (controller ruling, spec 31: + // `sqrt`, `floor`, `pow`, `fabs`). The risk is a C function that calls back into the module + // (`signal(15, f as Opaque)` then `raise(15)`, review6r1 c3), which it can only do with a + // function it was given - so the rule is off in a module whose foreign surface could hand it + // one (functionsEscape) - or an exported function reached by name, which a `@nodrops` + // declaration takes on as well. + static bool scalarDeclaration(mlir_ts::FuncOp funcOp); + + // `ts.Load(ts.AddressOf @f)`, where the global `f` holds the address of a function exported + // by a library loaded at run time, looked up by name: that function, if the library exported + // its `__own_no_drops`. + bool addImportedByAddress(mlir::Value callee, Callees &callees); + + Callees resolve(mlir::Operation *op); + + // The function value a call goes through: a method of a class, virtual or not, a symbol, or + // the `.instanceOf` slot. Anything else - a closure, a function read from a field, an + // interface's method - is unknown. + void resolveValue(mlir::Value callee, Callees &callees); + + // A method of a closed interface (interfaceFamilies), `ts.InterfaceSymbolRef(value, index)`, or a + // function-typed field of one (interfaceFieldFamilies), read and called: the family of the slot + // after the vtable's header - every function the vtables hold there, or every function the + // implementing literals' fields are ever given. Open, the call is unknown, as before. + template + void resolveInterfaceSlot(mlir_ts::InterfaceSymbolRefOp refOp, const std::map, Family> &families, + Callees &callees) + { + auto interfaceType = mlir::dyn_cast(refOp.getInterfaceVal().getType()); + auto identifier = mlir::dyn_cast(refOp.getIdentifier()); + if (!interfaceType || !identifier) + { + return; + } + + auto position = static_cast(refOp.getIndex()) + INTERFACE_VTABLE_HEADER_SLOTS; + auto found = families.find({interfaceType.getName().getValue().str(), position}); + if (found == families.end() || !found->second.closed || found->second.field != identifier || + found->second.members.empty()) + { + return; + } + + callees.named.assign(found->second.members.begin(), found->second.members.end()); + callees.viaInterface = true; + callees.known = true; + for (auto member : found->second.members) + { + callees.known = addDefined(member, callees) && callees.known; + } + } + + // Can a module other than this one call the function? A `public` method (not private, not + // exported itself) of a class no other module can name - not exported, not a word of the + // exported declarations, not in the type of an exported or declared function or of an exported + // or imported global (nameableClasses) - no: another module calls a function of this one through + // the declaration `__decls` gives it (controller ruling, spec 31). But a method of a class that + // extends a class another module can name, or one another module defines, yes, private or not: + // that module calls it through the base's vtable. + bool callableFromOutside(mlir_ts::FuncOp funcOp); + + // Can foreign code learn of a function of this module, to call it back during a call of a + // scalar declaration (scalarDeclaration)? It can only through the module's foreign surface, or + // through a function turned into data (controller ruling, spec 31, fix round 5: a closed-world + // test, in place of listing the routes a function value takes out). So the rule is on only when: + // - every foreign symbol is scalar (scalarType): every body-less function (C's, another + // module's - `import`, a DLL's - a `declare class`'s, a `@cpp` class's slot; a rest parameter + // is not scalar), every global declared with no initializer or imported (a `declare var`, a + // `declare class`'s vtable and type name), every `@cpp` class's fields and its bases', with + // no method of its own (C++ calls one through the C++ vtable), every function found in a + // library at run time (`SearchForAddressOfSymbol`, seen through its cast); an opaque pointer + // or a reference anywhere in such a type is a value of foreign memory, not scalar; + // - no cast makes an opaque pointer or a reference out of a number (`0x1234`, then a + // store through it reaches foreign memory); + // - no cast makes a value of a type that holds no function (`any`, `unknown`, an opaque + // pointer, an integer, a reference; a boolean cannot carry one and does not count) out of one + // that holds one (`onSig as Opaque`, `k.m as u64`, `>k`; a class or object + // takes its fields along unless seen as a class or an interface); + // - no `inline_asm` or `call_intrinsic` is given a value whose type holds a function. + bool functionsEscape = false; + + // The types a holdsFunction walk has been through, with or without their fields. + using SeenTypes = llvm::DenseSet>; + + // Does a value of this type hold a function: the type itself, a union's or an optional's + // member, a tuple's field, an array's element, what a reference points to, an interface whose + // vtable holds one past its header - a method, a function-typed field, or a field whose type + // holds one, recursively (an interface with methods, a class with a function field, an array of + // them) - or whose vtable this module does not make values with (a foreign one: `declare + // function makeW(): W`), and with `fields` a class's or an object's fields. The header slot + // (`.instanceOf`, the compiler's own, which destroys nothing) does not count. + bool holdsFunction(mlir::Type type, bool fields, SeenTypes &seen); + + // The `@cpp` class storage a method's `this` is of, if any: the storage of its first + // parameter's class, when the method's symbol is `Class.name`. + mlir_ts::ClassStorageType cppClassOfMethod(mlir_ts::FuncOp funcOp); + + // Does a value of this type carry an address of memory this module made - so a cast of it to + // an opaque pointer or a reference points into this module, not into foreign memory? + static bool addressType(mlir::Type type); + + void findEscapingFunctions(mlir::ModuleOp module); + + // class name -> its storage, from every class type a function's signature or a global mentions + llvm::StringMap classStorages; + + void collectClassStorages(mlir::ModuleOp module); + + // A class another module can see may be extended there, and its override is a candidate this + // module never sees: every member has to be private and defined here. + void resolveFamily(int64_t index, mlir::StringAttr identifier, Callees &callees); + + // `ts.Cast(ts.VTableOffsetRef(vtable, 0))`, where `vtable` is the one an object's first word + // points to or an interface value's own: slot 0 of either is `.instanceOf` - a class's + // generated one, a string compare, or `.instanceOf.none` for an object literal behind an + // interface. Only mlirGenInstanceOfThroughVTable builds a call through it. Neither destroys + // anything. + static bool isInstanceOfSlot(mlir::Operation *def); + + // Is every use of this function value the callee of a call - through `ts.GetMethod`, with + // `ts.GetThis` reading the object beside it? + static bool onlyCalled(mlir::Value value); + + // The closed world. A function is open when a caller this pass cannot see may reach it: it is + // not private (another module may call it), or its symbol is used other than by a direct call, + // a class vtable's entry, or a method reference used only as a callee. A virtual reference + // that escapes opens its whole family. + void findOpen(mlir::ModuleOp module); + + void openFamily(int64_t index, mlir::StringAttr symbol, const char *why); + + // ---- Callbacks (spec 33) ---- + // + // `___array_reduce(arr, f, init)` calls its parameter `f`: an indirect call, which reaches + // whatever function its callers pass. When the function is closed - private, and every use of its + // symbol a direct call - the parameter is only ever called, and every caller passes there a + // function this module defines, by its symbol or as a closure over a capture box made right + // there, a call through the parameter reaches exactly those functions: the parameter's family. + // Its members are closed too, when their symbols are used only to be called directly or passed + // so; then the call gets the facts the members agree on (callFacts), and `__own_no_drops` when + // none of them destroys anything. A closure's body takes its capture box first: its parameter k + // is the call's argument k - 1 (CallbackMember::shift). + // + // A kept parameter is agreed on the other way round from a class family's (meetCallbacks): a + // member that only borrows a parameter another member keeps is given it to keep too, and releases + // it on every path (OWN_PARAMS_RELEASED_ATTR_NAME) - `reduce` with a callback that returns its + // accumulator on some paths and one that never does, in one `___array_reduce`. Anything + // else the members disagree on demotes them all, as a class family's members are. + + struct CallbackMember + { + mlir::StringAttr symbol; + int32_t shift = 0; + }; + + struct CallbackFamily + { + llvm::SmallVector members; + // the ops that name a member's symbol to pass it here + llvm::SmallVector passing; + bool closed = true; + }; + + // (function, parameter index) -> the functions its callers pass there + std::map, CallbackFamily> callbackFamilies; + // the members whose families disagreed (meetFacts), for the error in their bodies + llvm::DenseSet demotedCallbacks; + // uses of a member's symbol that only pass it to a closed family: no escape (findOpen) + llvm::DenseSet callbackPassing; + + static bool isFunctionValueType(mlir::Type type); + + // Is every use of this parameter - through its slot, never assigned or captured - the function + // a call goes through? + static bool onlyCalledParam(mlir::BlockArgument argument); + + // The function a caller passes as `value`: its symbol, seen as a function value through casts + // between function types, or a closure over a capture box built of it. Null for anything else. + static mlir_ts::SymbolRefOp passedFunction(mlir::Value value, int32_t &shift); + + // Does the function value `value` go only where `passes` allows - as an argument of a call - and + // through casts between function types, a closure built of it, and that closure's own retains + // and releases? + static bool passedOnly(mlir::Value value, llvm::function_ref passes, int depth = 0); + + void collectCallbackFamilies(mlir::ModuleOp module); + + // A call through a parameter whose family is closed: its members. + void resolveCallback(mlir_ts::LoadOp loadOp, Callees &callees); + + // A member's facts as a call through its family sees them: each parameter k as argument + // k - shift. False when a fact is about the capture box itself, which the call does not pass. + static bool shiftFacts(const Facts &facts, int32_t shift, Facts &shifted); + + // Can the body of `funcOp` be given parameter `index` to keep and release it where it does not + // move it: a plain slot (isParameterSlot), a block, not a handle, not a string? + bool canReleaseParam(mlir_ts::FuncOp funcOp, int32_t index); + + // The members of each closed callback family agree (the section above), by promotion of a kept + // parameter or by demotion. True when any member's facts changed. + bool meetCallbacks(llvm::DenseSet &demoted); + + // ---- Results that borrow an argument ---- + + // The values a function returns: what is stored into the local its returns read (MLIRGen's + // result slot), or what a return hands back directly. + static llvm::SmallVector returnedValues(mlir_ts::FuncOp funcOp); + + struct ResultBorrows + { + llvm::SmallVector borrows; + bool record = false; + }; + + // What the function's result borrows (spec 2.4, 32): the one parameter every heap value it returns + // is a borrow of, or, when every such value is a borrowing record, the parameters the records + // borrow. A result that holds no block (`null`, a number) agrees with any; a record on one path + // and anything else on another is nothing a caller can be told. + ResultBorrows localResultBorrows(mlir_ts::FuncOp funcOp); + + // The parameter a function returns as it is on some paths, where every other value it returns + // that holds a block is one it owns (spec 33): `return v.x > 2 ? acc : V.plus(acc, v)`. Such a + // parameter is kept: the callers move the argument in, the path that returns it moves it out, + // and every other path releases it. -1 for none - a parameter returned on every path is a + // borrowed result; two parameters returned, or a part of one (`acc.next`), or a borrowing + // record, have no such answer; a string is copied instead (spec 22). + int32_t mixedReturnParam(mlir_ts::FuncOp funcOp); + + // ---- Borrowing records (spec 32) ---- + // + // `{ thing: this, ray: ray, dist: d }` given as an interface: an object literal's box holding values + // it does not own beside values it does. Rust's `struct Hit<'a> { thing: &'a Th, .. }`: the box is + // its owner's - the caller's, when it is returned - and it may not outlive what it borrows. Each box + // is classified where it is made: the fields it borrows (OWN_RECORD_BORROWED_FIELDS_ATTR_NAME, on the + // box and on every interface made of it, whose tag then names a release routine that leaves them), + // and, when every one of them is a parameter, `this`, or a read out of one, those parameters - what + // a function returning it lends (OWN_RESULT_RECORD_ATTR_NAME). The inference bounds every use. + + struct RecordBox + { + llvm::SmallVector params; + // every borrowed field is a parameter's, `this`'s, a read out of one, or immortal + bool returnable = true; + }; + + llvm::DenseMap recordBoxes; + + void classifyRecordBoxes(); + + void classifyRecordBox(mlir_ts::NewOp newOp); + + enum class RecordFlow + { + None, // no borrowing record + Record, // borrowing records only (or values that hold no block) + Bad // a record that borrows what is not a parameter, or one beside anything else + }; + + // Whether `value` - through views, merges and the owning locals that hold it - is a borrowing + // record, and the parameters it borrows: a box made here, or a call's result that is one, whose + // arguments are this function's parameters or reads out of them. + RecordFlow recordParamsOf(mlir::Value value, llvm::SmallVectorImpl ¶ms); + + // ---- Parameters the callee keeps ---- + + // Does `user` take `used` - a read of a parameter - into something that outlives the call: a + // store into a field, an element or a global, an insertion into an array, or a call that keeps + // it in turn? A local that holds it, and the result it is returned as, do not count: the first + // is a borrow, the second a borrowed result. + static bool keeps(mlir::Operation *user, mlir::Value used); + + // The parameters the body keeps (spec 2.4, owned-by-callee): a read of the parameter's slot - + // never assigned - that something keeps. + static llvm::SmallVector localOwnedParams(mlir_ts::FuncOp funcOp); + + // ---- The facts callers must know ---- + + // Owned parameters and a result that borrows only hold where every caller knows them, so the + // facts survive only on a closed function whose every family agrees, and families are + // demoted until they do: a member that loses its facts can make another family disagree. A + // caller that returns what such a call returns, or passes a parameter on to a kept one, gets + // the fact too, so the facts are recomputed with the calls pinned until nothing changes; if + // that does not settle, nobody gets any. + void computeFacts(); + + void meetFacts(); + + Facts callFacts(const Callees &callees); + + static void setFacts(mlir::Operation *op, const Facts &facts); + + // The calls get the facts their callees agree on; each function gets its own, or the reason it + // lost those its body has, for the error there. + void pinFacts(const llvm::DenseMap &pinned); + + // ---- Generators that borrow (phase 7b) ---- + // + // A generator over a parameter that holds a block - `function* each(a: number[])` - keeps the + // parameter's cell in a box in its state object's `.captured` field. Under 5b's rule that is an + // escaping closure over a value that is the caller's, an error. Instead the state object + // borrows: the caller owns it, it may not outlive the argument (OWN_RESULT_BOUNDED_ATTR_NAME), + // and its release gives back neither the parameter's value nor any field that only ever holds + // what was read out of it (OWN_BORROWING_FIELDS_ATTR_NAME). A state type is unique to its + // generator - its `next` field's type names a per-site `object_storage` - so a fact per type is + // a fact per generator, which is what the per-type release routine needs. + + struct BorrowingState + { + mlir_ts::FuncOp maker; + int32_t captured = -1; + llvm::SmallVector bounded; + llvm::SetVector fields; + }; + + static int32_t fieldIndex(mlir_ts::TupleType tupleType, llvm::StringRef name); + + // The box the maker stores into the `.captured` field of the initial state it copies into + // `newOp`'s block: `ts.Store(box, ts.PropertyRef(state, captured))`, `state` a local whose only + // read fills the block. + static mlir::Value boxStoredIntoState(mlir_ts::NewOp newOp, int32_t captured); + + // Is every caller of `funcOp` a direct call this pass sees? It is private, and each use of its + // symbol is a call, or a closure over it that is only retained and released - `.map(f)` builds + // one and calls its body straight through the box. + bool calledOnlyDirectly(mlir::ModuleOp module, mlir_ts::FuncOp funcOp, bool publicToo = false); + + // A parameter's cell: a captured local declared from an argument of `maker`'s entry block, that + // owns nothing (its value is the caller's). + static bool isParameterCell(mlir_ts::VariableOp cellOp, mlir_ts::FuncOp maker, int32_t &index); + + // `ts.Load` of one of `maker`'s parameter slots (isParameterSlot): `index` is its position. + static bool readsParameterSlot(mlir::Value value, mlir_ts::FuncOp maker, int32_t &index); + + // `ts.Load(ts.PropertyRef(argument, i))` of one of `maker`'s parameters: `index` is its position. + static bool readsParameterField(mlir::Value value, mlir_ts::FuncOp maker, int32_t &index); + + bool ownsHeap(mlir::Location location, mlir::Type type); + + void findBorrowingStates(mlir::ModuleOp module); + + // Is `value` a read of what a state of `tupleType` borrows: the value in one of its box's cells + // (`ts.Load` of `ts.Load(ts.PropertyRef(ts.Load(ts.PropertyRef(state, captured)), i))`), or a + // borrowing field's (`ts.Load(ts.PropertyRef(state, p))`)? + bool readsBorrowed(mlir::Value value, mlir_ts::TupleType tupleType, const BorrowingState &state); + + // ---- Globals given a borrow ---- + + // A global initialized with a read of another global, a field or an element (`let k = a`, + // `let k = h.c`) would be a second owner of a block another owner keeps: the first overwrite + // of either frees what the other still reads, and both give it back at exit. rc counts the + // global's own reference (OwnedReturnConsumptionPass, claimGlobalInitializers); own cannot + // move the value out of its owner, and a global cannot hold a borrow. A handle is counted + // under own too. + void rejectBorrowedGlobalInitializers(mlir::ModuleOp module); + + // ---- Drops ---- + + // Least fixpoint: a function drops nothing until its body or a call it makes shows it may. + void computeDrops(); + + bool callMayDrop(const Callees &callees); + + // ---- Globals a call may assign ---- + + // The globals each call may give a new value, pinned on the call for the inference + // (OWN_ASSIGNS_GLOBALS_ATTR_NAME, OWN_ASSIGNS_ANY_GLOBAL_ATTR_NAME): a store to a global, or its + // release, in a callee's body, or in a call it makes. A call that may drop and whose callees are + // not known may assign any; one that drops nothing assigns none. A read of a global ends where + // the global is given a new value (checkGlobalRead), and a call that does so ends it as surely as + // a store in the same function: `const c = G; reset(); print(c.x)`. + void pinAssignedGlobals(); + + // An argument read out of a global (`run(G)`, `G.m()`) is lent to the callee, and the callee may + // free it under its own parameter: `function run(h: C) { G = mk(); print(h.x); }`. The caller's + // borrow ends at the call that takes it, so nothing in the caller sees that, and in the callee + // the parameter is not known to be the global's. A call that may drop is an error for such an + // argument, when something assigns that global. (A field of a global given that way is a + // borrow of a place, which the inference already bounds by the call.) + void rejectGlobalArgumentsDropped(); + + // ---- Calls in a global's initializer ---- + + // A global's region is no function: its calls get no facts pinned, and the inference never sees + // it, so its calls were read with the defaults - an owned result, borrowed arguments. A result + // that borrows an argument (`const c = child(n)`, `id(new V(1))`) then made the global a second + // owner of a block its argument's owner keeps, or kept one the region freed; and a fresh + // argument a callee keeps (`new N(new V(4))`, the constructor keeping `c`) was released at the + // end of the region, under the object that kept it. A kept argument made in the region and used + // by nothing after the call moves into the callee: its release goes. Anything else is an error. + void ownGlobalInitializerCalls(mlir::ModuleOp module); + + // `value` was made in the call's block and nothing but its release, if it has one, comes after + // the call: the release goes, and the callee owns the block. Constant data (a string literal) + // is nobody's, and is given as it is. + static bool moveIntoCallee(mlir::Operation *call, mlir::Value value); + + // ---- Export ---- + + // A library says, beside its memory-model marker, which of its exported functions destroy + // nothing (SHARED_LIB_OWN_FACTS). The global is a copy of the marker `__tsmm_own_` + // under another name and with another value, so it is exported exactly as the marker is, and + // MLIRGen emits the same under own as under rc. No marker, nothing is exported. + void exportNoDrops(); + + // Does the body itself destroy something a caller may reach: overwrite a field or an element + // of a block it did not make, assign a global, remove elements from an array it did not make, + // or `delete`? A local's own releases are not drops, nor is the constructor's filling of the + // object it is building. + bool dropsInBody(mlir_ts::FuncOp funcOp); + + // Can the block this reference points into be one the function's caller can reach? Walks up + // from a place to what holds it. Not when every way up ends in a block the function made (an + // allocation, a call's result it owns) or in an owning local that only ever held such blocks, + // or, in a constructor, in the object being built. + bool reachesOutside(mlir::Value ref, mlir_ts::FuncOp funcOp); + + static bool isAssigned(mlir_ts::VariableOp varOp); + + // An owning, uncaptured local whose every value - its initializer and each assignment - is one + // the function made. + static bool holdsOnlyFresh(mlir_ts::VariableOp varOp); +}; + +} // namespace own_signature + +#endif // MLIR_TYPESCRIPT_OWNERSHIPSIGNATUREPASS_H diff --git a/tslang/lib/TypeScript/OwnershipSignatureRecords.cpp b/tslang/lib/TypeScript/OwnershipSignatureRecords.cpp new file mode 100644 index 000000000..909ea7bb0 --- /dev/null +++ b/tslang/lib/TypeScript/OwnershipSignatureRecords.cpp @@ -0,0 +1,301 @@ +// OwnershipSignaturePass (see OwnershipSignaturePass.h): borrowing records (spec 32) - the boxes an +// object literal is made into, and the parameters a record result borrows. + +#include "OwnershipSignaturePass.h" + +#define DEBUG_TYPE "own" + +namespace own_signature +{ + + void OwnershipSignaturePass::classifyRecordBoxes() + { + recordBoxes.clear(); + getOperation().walk([&](mlir_ts::NewOp newOp) { + newOp->removeAttr(OWN_RECORD_BORROWED_FIELDS_ATTR_NAME); + for (auto *user : newOp->getUsers()) + { + for (auto *viewUser : user->getUsers()) + { + if (mlir::isa(viewUser)) + { + viewUser->removeAttr(OWN_RECORD_BORROWED_FIELDS_ATTR_NAME); + } + } + } + + classifyRecordBox(newOp); + }); + } + + void OwnershipSignaturePass::classifyRecordBox(mlir_ts::NewOp newOp) + { + auto boxOp = recordBoxOf(newOp.getResult()); + auto varOp = boxOp == newOp ? recordLocalOf(newOp.getResult()) : mlir_ts::VariableOp(); + if (!varOp) + { + return; + } + + // the record read once, and only into its box (rc's retain of it beside) + mlir_ts::LoadOp read; + for (auto *user : varOp->getUsers()) + { + if (auto loadOp = mlir::dyn_cast(user)) + { + if (read) + { + return; + } + + read = loadOp; + } + } + + for (auto *user : read->getUsers()) + { + auto storeOp = mlir::dyn_cast(user); + if (!mlir::isa(user) && !(storeOp && storeOp.getReference() == newOp.getResult())) + { + return; + } + } + + // the box seen only as interfaces: every release of it reads their tag + llvm::SmallVector interfaces; + for (auto &use : newOp->getUses()) + { + auto *user = use.getOwner(); + if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getReference() == newOp.getResult()) + { + continue; + } + + auto castOp = mlir::dyn_cast(user); + if (!castOp || !isView(castOp)) + { + return; + } + + for (auto *castUser : castOp->getUsers()) + { + auto newInterfaceOp = mlir::dyn_cast(castUser); + if (!newInterfaceOp || newInterfaceOp.getThisVal() != castOp.getResult()) + { + return; + } + + interfaces.push_back(castUser); + } + } + + if (interfaces.empty()) + { + return; + } + + // each field that holds a block: owned (made here, or nothing) or borrowed, never both + RecordBox info; + llvm::SmallVector borrowed; + auto mth = typeHelper(&getContext()); + for (auto *user : varOp->getUsers()) + { + auto propertyRefOp = mlir::dyn_cast(user); + if (!propertyRefOp) + { + continue; + } + + auto field = propertyRefOp.getResult(); + auto refType = mlir::dyn_cast(field.getType()); + if (!refType || !mth.ownsHeapMemory(field.getLoc(), refType.getElementType())) + { + continue; + } + + auto owns = false; + auto borrows = false; + for (auto *fieldUser : field.getUsers()) + { + if (mlir::isa(fieldUser)) + { + continue; + } + + auto storeOp = mlir::dyn_cast(fieldUser); + if (!storeOp || storeOp.getReference() != field) + { + return; + } + + auto value = storeOp.getValue(); + switch (recordFieldSource(value)) + { + case RecordFieldSource::NoBlock: + continue; + case RecordFieldSource::Fresh: + owns = true; + continue; + case RecordFieldSource::Unknown: + return; + case RecordFieldSource::Immortal: + borrows = true; + continue; + case RecordFieldSource::Param: + case RecordFieldSource::Place: + if (auto param = borrowedParam(value); param >= 0) + { + info.params.push_back(param); + } + else + { + info.returnable = false; + } + + borrows = true; + continue; + case RecordFieldSource::Slot: + case RecordFieldSource::Cell: + info.returnable = false; + borrows = true; + continue; + } + } + + // a string is copied, not borrowed (spec 22) + if ((owns && borrows) || (borrows && isCopyableString(refType.getElementType()))) + { + return; + } + + if (borrows) + { + borrowed.push_back(propertyRefOp.getPosition()); + } + } + + if (borrowed.empty()) + { + return; + } + + llvm::sort(borrowed); + borrowed.erase(std::unique(borrowed.begin(), borrowed.end()), borrowed.end()); + llvm::sort(info.params); + info.params.erase(std::unique(info.params.begin(), info.params.end()), info.params.end()); + + auto attr = mlir::DenseI32ArrayAttr::get(&getContext(), borrowed); + newOp->setAttr(OWN_RECORD_BORROWED_FIELDS_ATTR_NAME, attr); + for (auto *newInterfaceOp : interfaces) + { + newInterfaceOp->setAttr(OWN_RECORD_BORROWED_FIELDS_ATTR_NAME, attr); + } + + recordBoxes[newOp.getOperation()] = std::move(info); + } + + OwnershipSignaturePass::RecordFlow OwnershipSignaturePass::recordParamsOf(mlir::Value value, llvm::SmallVectorImpl ¶ms) + { + llvm::SmallVector work{value}; + llvm::SmallPtrSet seen; + auto record = false; + auto other = false; + while (!work.empty()) + { + // what holds no block, by its own type (a box's `value_ref` owns nothing by type, but + // the object its view shows does) + auto next = work.pop_back_val(); + auto current = rootOf(next); + if (!seen.insert(current).second || holdsNoBlock(next)) + { + continue; + } + + auto *def = current.getDefiningOp(); + if (auto found = def ? recordBoxes.find(def) : recordBoxes.end(); found != recordBoxes.end()) + { + if (!found->second.returnable) + { + return RecordFlow::Bad; + } + + params.append(found->second.params.begin(), found->second.params.end()); + record = true; + continue; + } + + if (def && isCall(def) && isRecordResult(def)) + { + auto args = callArgs(def); + for (auto index : resultBorrowsAll(def)) + { + if (static_cast(index) >= args.size()) + { + return RecordFlow::Bad; + } + + auto arg = args[index]; + if (argumentHoldsNoBlock(arg) || isImmortalGlobalRead(arg)) + { + continue; + } + + auto param = borrowedParam(arg); + if (param < 0) + { + return RecordFlow::Bad; + } + + params.push_back(param); + } + + record = true; + continue; + } + + if (auto argument = mlir::dyn_cast(current)) + { + llvm::SmallVector merged; + if (mergedInto(argument, merged)) + { + work.append(merged.begin(), merged.end()); + continue; + } + + other = true; + continue; + } + + // what an owning local holds: its initializer and every value stored into it + auto loadOp = mlir::dyn_cast_or_null(def); + auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + if (varOp && isOwningVariable(varOp) && !varOp.getCaptured().value_or(false)) + { + if (varOp.getInitializer()) + { + work.push_back(varOp.getInitializer()); + } + + for (auto *user : varOp->getUsers()) + { + if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getReference() == varOp.getResult()) + { + work.push_back(storeOp.getValue()); + } + } + + continue; + } + + other = true; + } + + if (!record) + { + return RecordFlow::None; + } + + return other ? RecordFlow::Bad : RecordFlow::Record; + } + +} // namespace own_signature diff --git a/tslang/lib/TypeScript/OwnershipSignatureResolve.cpp b/tslang/lib/TypeScript/OwnershipSignatureResolve.cpp new file mode 100644 index 000000000..2fa6e57e7 --- /dev/null +++ b/tslang/lib/TypeScript/OwnershipSignatureResolve.cpp @@ -0,0 +1,364 @@ +// OwnershipSignaturePass (see OwnershipSignaturePass.h): a call resolved to its callees - class +// vtables and their families, imported declarations and the C-scalar surface (spec 31). + +#include "OwnershipSignaturePass.h" + +#define DEBUG_TYPE "own" + +namespace own_signature +{ + + void OwnershipSignaturePass::collectClassVTables(mlir::ModuleOp module) + { + llvm::SmallVector>>> vtables; + module.walk([&](mlir_ts::GlobalOp globalOp) { + auto entries = classVTableEntries(globalOp); + if (!entries.empty()) + { + auto name = globalOp.getSymName(); + classNames.insert(name.drop_back(llvm::StringRef("..vtbl").size())); + vtables.push_back({name, std::move(entries)}); + } + }); + + for (auto &[name, entries] : vtables) + { + for (auto [position, symbol] : entries) + { + families[{position, methodName(symbol.getValue())}].push_back(symbol); + } + } + } + + mlir_ts::TupleType OwnershipSignaturePass::storageTupleOf(mlir::Type type) + { + return own_facts::storageTupleOf(&getContext(), type); + } + + llvm::SmallVector> OwnershipSignaturePass::classVTableEntries(mlir_ts::GlobalOp globalOp) + { + llvm::SmallVector> entries; + if (!globalOp.getSymName().ends_with("..vtbl")) + { + return entries; + } + + auto isClass = false; + globalOp.getInitializerRegion().walk([&](mlir_ts::InsertPropertyOp insertOp) { + auto symbolRefOp = insertOp.getValue().getDefiningOp(); + if (!symbolRefOp || insertOp.getPosition().size() != 1) + { + return; + } + + auto symbol = symbolRefOp.getIdentifierAttr().getAttr(); + isClass = isClass || symbol.getValue().ends_with("..instanceOf"); + entries.push_back({insertOp.getPosition()[0], symbol}); + }); + + if (!isClass) + { + entries.clear(); + } + + return entries; + } + + bool OwnershipSignaturePass::isClassVTableEntry(mlir::Operation *op) + { + auto globalOp = op->getParentOfType(); + return globalOp && !classVTableEntries(globalOp).empty(); + } + + std::string OwnershipSignaturePass::methodName(llvm::StringRef symbol) + { + size_t best = 0; + for (auto &entry : classNames) + { + auto name = entry.getKey(); + if (name.size() > best && symbol.size() > name.size() && symbol.starts_with(name) && + symbol[name.size()] == '.') + { + best = name.size() + 1; + } + } + + return symbol.drop_front(best).str(); + } + + llvm::SmallVector OwnershipSignaturePass::familyOf(int64_t index, mlir::StringAttr identifier) + { + llvm::SmallVector members; + if (auto found = families.find({index, methodName(identifier.getValue())}); found != families.end()) + { + members = found->second; + } + + if (!llvm::is_contained(members, identifier)) + { + members.push_back(identifier); + } + + return members; + } + + bool OwnershipSignaturePass::addDefined(mlir::StringAttr symbol, Callees &callees) + { + auto funcOp = functions.lookup(symbol.getValue()); + if (!funcOp || funcOp.isDeclaration()) + { + return false; + } + + if (!llvm::is_contained(callees.funcs, funcOp)) + { + callees.funcs.push_back(funcOp); + } + + return true; + } + + bool OwnershipSignaturePass::addImported(mlir::StringAttr symbol, Callees &callees) + { + auto funcOp = functions.lookup(symbol.getValue()); + if (!funcOp || !funcOp.isDeclaration() || + !(importedNoDrops.contains(symbol.getValue()) || funcOp->hasAttr(OWN_DECLARED_NO_DROPS_ATTR_NAME) || + (scalarDeclaration(funcOp) && !functionsEscape))) + { + return false; + } + + callees.imported = true; + callees.importedName = symbol.getValue(); + return true; + } + + bool OwnershipSignaturePass::scalarType(mlir::Type type) + { + if (auto literalType = mlir::dyn_cast(type)) + { + return scalarType(literalType.getElementType()); + } + + if (auto enumType = mlir::dyn_cast(type)) + { + return scalarType(enumType.getElementType()); + } + + return mlir::isa(type); + } + + bool OwnershipSignaturePass::scalarSignature(mlir_ts::FunctionType functionType) + { + return !functionType.isVarArg() && llvm::all_of(functionType.getInputs(), scalarType) && + llvm::all_of(functionType.getResults(), + [](mlir::Type type) { return mlir::isa(type) || scalarType(type); }); + } + + bool OwnershipSignaturePass::scalarDeclaration(mlir_ts::FuncOp funcOp) + { + return funcOp.isDeclaration() && !funcOp->hasAttr("export") && !funcOp->hasAttr("import") && + !funcOp->hasAttr(DLL_EXPORT) && !funcOp->hasAttr(DLL_IMPORT) && scalarSignature(funcOp.getFunctionType()); + } + + bool OwnershipSignaturePass::addImportedByAddress(mlir::Value callee, Callees &callees) + { + auto loadOp = callee.getDefiningOp(); + auto addressOfOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::AddressOfOp(); + if (!addressOfOp) + { + return false; + } + + auto globalOp = globals.lookup(addressOfOp.getGlobalName()); + if (!globalOp || writtenGlobals.contains(addressOfOp.getGlobalName())) + { + return false; + } + + // the global is never anything but that address: `Cast(SearchForAddressOfSymbol(name))` + auto ®ion = globalOp.getInitializerRegion(); + if (!region.hasOneBlock()) + { + return false; + } + + auto resultOp = mlir::dyn_cast(region.front().getTerminator()); + auto castOp = resultOp && resultOp->getNumOperands() == 1 + ? resultOp->getOperand(0).getDefiningOp() + : mlir_ts::CastOp(); + auto searchOp = castOp ? castOp.getIn().getDefiningOp() + : mlir_ts::SearchForAddressOfSymbolOp(); + auto nameOp = searchOp ? searchOp->getOperand(0).getDefiningOp() : mlir_ts::ConstantOp(); + auto name = nameOp ? mlir::dyn_cast(nameOp.getValue()) : mlir::StringAttr(); + if (!name || !importedNoDrops.contains(name.getValue())) + { + return false; + } + + callees.imported = true; + callees.importedName = name.getValue(); + return true; + } + + OwnershipSignaturePass::Callees OwnershipSignaturePass::resolve(mlir::Operation *op) + { + Callees callees; + if (auto callee = directCallee(op)) + { + callees.named.push_back(callee.getAttr()); + callees.known = addDefined(callee.getAttr(), callees) || addImported(callee.getAttr(), callees); + } + else if (mlir::isa(op)) + { + resolveValue(calleeValue(op), callees); + } + + if (!callees.known) + { + callees.funcs.clear(); + } + + return callees; + } + + void OwnershipSignaturePass::resolveValue(mlir::Value callee, Callees &callees) + { + // `ts.Load(ts.AddressOf @g)`: a dynamic import's global, which may say what the function + // keeps; a method of a class imported so is bound to its object first + auto function = callee; + if (auto getMethodOp = function.getDefiningOp()) + { + function = getMethodOp.getBoundFunc(); + } + + if (auto boundOp = function.getDefiningOp()) + { + function = boundOp.getFunc(); + } + + auto loadOp = function.getDefiningOp(); + if (auto addressOfOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::AddressOfOp(); + addressOfOp && !writtenGlobals.contains(addressOfOp.getGlobalName())) + { + callees.named.push_back(addressOfOp.getGlobalNameAttr().getAttr()); + } + + if (addImportedByAddress(callee, callees)) + { + callees.known = true; + return; + } + + if (auto getMethodOp = callee.getDefiningOp()) + { + callee = getMethodOp.getBoundFunc(); + } + + auto *def = callee.getDefiningOp(); + mlir::StringAttr identifier; + int64_t index = -1; + if (auto refOp = mlir::dyn_cast_or_null(def)) + { + identifier = refOp.getIdentifierAttr().getAttr(); + index = refOp.getIndex(); + } + else if (auto refOp = mlir::dyn_cast_or_null(def)) + { + identifier = refOp.getIdentifierAttr().getAttr(); + index = refOp.getIndex(); + } + else if (auto refOp = mlir::dyn_cast_or_null(def)) + { + identifier = refOp.getIdentifierAttr().getAttr(); + } + else if (auto refOp = mlir::dyn_cast_or_null(def)) + { + identifier = refOp.getIdentifierAttr().getAttr(); + } + else if (isInstanceOfSlot(def)) + { + callees.known = true; + callees.instanceOf = true; + return; + } + else if (auto symbol = fieldFunctions.of(def)) + { + callees.known = addDefined(symbol, callees); + callees.fieldFunction = callees.known; + return; + } + else if (auto interfaceRefOp = mlir::dyn_cast_or_null(def)) + { + resolveInterfaceSlot(interfaceRefOp, interfaceFamilies, callees); + return; + } + else if (auto loadOp = mlir::dyn_cast_or_null(def)) + { + resolveCallback(loadOp, callees); + return; + } + else if (auto boundOp = mlir::dyn_cast_or_null(def)) + { + // a function-typed field of an interface, called: `ts.CreateBoundFunction( + // ts.ExtractInterfaceThis(i), ts.Load(ts.InterfaceSymbolRef(i, f)))` + auto loadOp = boundOp.getFunc().getDefiningOp(); + auto refOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::InterfaceSymbolRefOp(); + auto thisOp = boundOp.getThisVal().getDefiningOp(); + if (refOp && isInterfaceField(refOp.getResult()) && thisOp && thisOp.getInterfaceVal() == refOp.getInterfaceVal()) + { + resolveInterfaceSlot(refOp, interfaceFieldFamilies, callees); + } + + return; + } + + if (!identifier) + { + return; + } + + if (index < 0) + { + callees.named.push_back(identifier); + callees.known = addDefined(identifier, callees) || addImported(identifier, callees); + return; + } + + callees.named = familyOf(index, identifier); + resolveFamily(index, identifier, callees); + } + + void OwnershipSignaturePass::resolveFamily(int64_t index, mlir::StringAttr identifier, Callees &callees) + { + callees.known = true; + for (auto member : familyOf(index, identifier)) + { + callees.known = addDefined(member, callees) && callees.known; + } + + callees.known = callees.known && llvm::all_of(callees.funcs, [&](mlir_ts::FuncOp funcOp) { return !callableFromOutside(funcOp); }); + } + + bool OwnershipSignaturePass::isInstanceOfSlot(mlir::Operation *def) + { + auto castOp = mlir::dyn_cast_or_null(def); + auto slot = castOp ? castOp.getIn().getDefiningOp() : mlir_ts::VTableOffsetRefOp(); + if (!slot || slot.getIndex() != INTERFACE_VTABLE_INSTANCEOF_SLOT) + { + return false; + } + + // a class vtable, read out of an object's first word, or an interface's own vtable, + // whose slot 0 is its implementer's `.instanceOf` (INTERFACE_VTABLE_HEADER_SLOTS) + auto *vtable = slot.getVtable().getDefiningOp(); + if (auto classVTable = mlir::dyn_cast_or_null(vtable)) + { + return classVTable.getIndex() == 0; + } + + return mlir::isa_and_nonnull(vtable); + } + +} // namespace own_signature diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 2dc4d3043..6f840bb26 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -3887,181 +3887,184 @@ set_tests_properties(test-own-err-names-the-variable # Each negative program has one shape phase 0 cannot prove, and must be rejected with this text. set(own_error_cases - "own_err_assign_after_move|borrows .* but is used here after .* is released or overwritten" - "own_err_borrow_escape|borrows .* and cannot be stored, returned or captured" - "own_err_move_some_paths_field|is moved here on some paths only" - "own_err_borrow_and_move|is moved here while .* borrows it" - "own_err_borrow_bound_method|borrows .* but is used here after .* is released or overwritten" - "own_err_const_bound_method_reassigned|takes a second reference" - "own_err_borrow_catch_local|borrows .* but is used here after .* is released or overwritten" - "own_err_borrow_finally_local|borrows .* but is used here after .* is released or overwritten" - "own_err_borrow_derived_ref|borrows .* but is used here after .* is released or overwritten" - "own_err_const_alias_move|borrows .* and cannot be assigned" - "own_err_const_alias_field|is moved here while .* borrows it" - "own_err_field_alias|is used here after its value was moved" - "own_err_call_in_try_drops|borrows .* but is used here after it may be released or overwritten" - "own_err_closure_escape_used|is captured by a closure that escapes, and is still used here" - "own_err_closure_escape_twice|is captured by a closure that escapes, and is still used here" - "own_err_closure_escape_param|cannot move this variable into it: its value is the caller's" - "own_err_closure_escape_inherited|cannot move this variable into it: it is captured from an enclosing function" - "own_err_closure_escape_loop|is moved inside a loop" - "own_err_closure_escape_some_paths|is moved here on some paths only" - "own_err_closure_outlives_cell|borrows .*, which it captures, but runs here after .* is released" - "own_err_closure_copy_moved|borrows .*, which it captures, but runs here after .* is released" - "own_err_closure_param_assigned|is a captured parameter and cannot be assigned under -mm=own" - "own_err_closure_assigns_param|is a captured parameter and cannot be assigned under -mm=own" - "own_err_cell_assigned_in_frame|borrows .* but is used here after it may be released or overwritten" - "own_err_cell_assigned_in_closure|borrows .* but is used here after it may be released or overwritten" - "own_err_closure_assigns_captured_field|borrows .* but is used here after it may be released or overwritten" - "own_err_unknown_producer|takes a second reference" - "own_err_interface_two_views|takes a second reference" - "own_err_field_function_reassigned|borrows .* but is used here after it may be released or overwritten" - "own_err_generator_outlives_arg|borrows .* but is used here after .* is released or overwritten" - "own_err_generator_bounded_escapes|borrows its generator's arguments and cannot be stored, returned or captured" - "own_err_literal_two_records|is used here after its value was moved" + "own_err_assign_after_move|:12:16: error: '[^']*' borrows '[^']*' but is used here after '[^']*' is released or overwritten" + "own_err_borrow_escape|:9:5: error: '[^']*' borrows '[^']*' and cannot be stored, returned or captured" + "own_err_move_some_paths_field|:8:9: error: '[^']*' is moved here on some paths only" + "own_err_borrow_and_move|:8:5: error: '[^']*' is moved here while '[^']*' borrows it" + "own_err_borrow_bound_method|:20:17: error: '[^']*' borrows '[^']*' but is used here after '[^']*' is released or overwritten" + "own_err_const_bound_method_reassigned|:20:11: error: '[^']*' takes a second reference" + "own_err_borrow_catch_local|:31:20: error: '[^']*' borrows '[^']*' but is used here after '[^']*' is released or overwritten" + "own_err_borrow_finally_local|:30:20: error: '[^']*' borrows '[^']*' but is used here after '[^']*' is released or overwritten" + "own_err_borrow_derived_ref|:28:5: error: '[^']*' borrows '[^']*' but is used here after '[^']*' is released or overwritten" + "own_err_const_alias_move|:12:5: error: '[^']*' borrows '[^']*' and cannot be assigned" + "own_err_const_alias_field|:15:5: error: '[^']*' is moved here while '[^']*' borrows it" + "own_err_field_alias|:9:11: error: '[^']*' is used here after its value was moved" + "own_err_call_in_try_drops|:30:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_closure_escape_used|:9:11: error: '[^']*' is captured by a closure that escapes, and is still used here" + "own_err_closure_escape_twice|:7:19: error: '[^']*' is captured by a closure that escapes, and is still used here" + "own_err_closure_escape_param|:7:12: error: a closure that escapes owns what it captures, but -mm=own cannot move this variable into it: its value is the caller's" + "own_err_closure_escape_inherited|:9:21: error: a closure that escapes owns what it captures, but -mm=own cannot move this variable into it: it is captured from an enclosing function" + "own_err_closure_escape_loop|:11:23: error: '[^']*' is moved inside a loop but was made outside it" + "own_err_closure_escape_some_paths|:12:23: error: '[^']*' is moved here on some paths only" + "own_err_closure_outlives_cell|:13:11: error: '[^']*' borrows '[^']*', which it captures, but runs here after '[^']*' is released" + "own_err_closure_copy_moved|:13:11: error: '[^']*' borrows '[^']*', which it captures, but runs here after '[^']*' is released" + "own_err_closure_param_assigned|:9:5: error: '[^']*' is a captured parameter and cannot be assigned under -mm=own: its value is the caller's" + "own_err_closure_assigns_param|:8:15: error: '[^']*' is a captured parameter and cannot be assigned under -mm=own: its value is the caller's" + "own_err_cell_assigned_in_frame|:16:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_cell_assigned_in_closure|:18:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_closure_assigns_captured_field|:19:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_unknown_producer|:5:11: error: '[^']*' takes a second reference" + "own_err_interface_two_views|:20:11: error: '[^']*' takes a second reference" + "own_err_field_function_reassigned|:7:17: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_generator_outlives_arg|:12:11: error: '[^']*' borrows '[^']*' but is used here after '[^']*' is released or overwritten" + "own_err_generator_bounded_escapes|:8:17: error: '[^']*' borrows its generator's arguments and cannot be stored, returned or captured" + "own_err_literal_two_records|:11:16: error: '[^']*' is used here after its value was moved" "own_err_generator_yields_borrow|borrows the generator's argument and cannot be stored, returned or captured" - "own_err_generator_map_outlives|borrows .* but is used here after .* is released or overwritten" - "own_err_generator_place_overwritten|borrows a field but is used here after it may be released or overwritten" - "own_err_two_owners|is used here after its value was moved" - "own_err_reassign_some_paths|is moved here on some paths only" - "own_err_push_let|is used here after its value was moved" - "own_err_push_loop|is moved inside a loop" - "own_err_field_loop|is moved inside a loop" - "own_err_union_moved|is used here after its value was moved" - "own_err_field_read_overwritten|borrows .* but is used here after it may be released or overwritten" - "own_err_element_read_overwritten|borrows .* but is used here after it may be released or overwritten" - "own_err_element_read_popped|borrows .* but is used here after it may be released or overwritten" - "own_err_element_read_loop_pop|borrows .* but is used here after it may be released or overwritten" - "own_err_field_read_owner_assigned|borrows .* but is used here after it may be released or overwritten" - "own_err_field_read_owner_moved|borrows .* but is used here after it may be released or overwritten" - "own_err_field_read_call|borrows .* but is used here after it may be released or overwritten" - "own_err_param_read_call|borrows .* but is used here after it may be released or overwritten" - "own_err_global_read_assigned|borrows .* but is used here after it may be released or overwritten" - "own_err_merge_read_overwritten|borrows .* but is used here after it may be released or overwritten" - "own_err_alias_read_popped|borrows .* but is used here after it may be released or overwritten" - "own_err_alias_read_stale|borrows .* but is used here after it may be released or overwritten" - "own_err_merge_root_assigned|borrows .* but is used here after it may be released or overwritten" - "own_err_container_read_stored|borrows .* and cannot be stored, returned or captured" - "own_err_field_let_overwritten|borrows .* but is used here after it may be released or overwritten" - "own_err_field_let_assigned|borrows .* and cannot be assigned" - "own_err_call_drops_indirectly|borrows .* but is used here after it may be released or overwritten" - "own_err_call_given_borrow|borrows .* but is used here after it may be released or overwritten" - "own_err_call_this_borrow|borrows .* but is used here after it may be released or overwritten" - "own_err_declared_drops|borrows .* but is used here after it may be released or overwritten" - "own_err_exported_virtual_call|borrows .* but is used here after it may be released or overwritten" + "own_err_generator_map_outlives|:7:11: error: '[^']*' borrows 'the generator's source' but is used here after 'the generator's source' is released or overwritten" + "own_err_generator_place_overwritten|:15:16: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_two_owners|:8:13: error: '[^']*' is used here after its value was moved" + "own_err_reassign_some_paths|:10:5: error: '[^']*' is moved here on some paths only" + "own_err_push_let|:7:9: error: '[^']*' is used here after its value was moved" + "own_err_push_loop|:6:33: error: '[^']*' is moved inside a loop but was made outside it" + "own_err_field_loop|:10:9: error: '[^']*' is moved inside a loop but was made outside it" + "own_err_union_moved|:10:11: error: '[^']*' is used here after its value was moved" + "own_err_field_read_overwritten|:28:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_element_read_overwritten|:22:11: error: '[^']*' borrows an element but is used here after it may be released or overwritten" + "own_err_element_read_popped|:26:11: error: '[^']*' borrows an element but is used here after it may be released or overwritten" + "own_err_element_read_loop_pop|:25:15: error: '[^']*' borrows an element but is used here after it may be released or overwritten" + "own_err_field_read_owner_assigned|:27:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_field_read_owner_moved|:33:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_field_read_call|:31:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_param_read_call|:33:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_global_read_assigned|:29:11: error: '[^']*' borrows 'g[.]c' but is used here after it may be released or overwritten" + "own_err_merge_read_overwritten|:28:5: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_alias_read_popped|:28:9: error: '[^']*' borrows an element but is used here after it may be released or overwritten" + "own_err_alias_read_stale|:31:19: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_merge_root_assigned|:36:12: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_container_read_stored|:26:5: error: '[^']*' borrows a field and cannot be stored, returned or captured" + "own_err_field_let_overwritten|:28:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_field_let_assigned|:26:5: error: '[^']*' borrows a field and cannot be assigned" + "own_err_call_drops_indirectly|:24:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_call_given_borrow|:21:5: error: '[^']*' borrows 'g[.]c' but is used here after it may be released or overwritten" + "own_err_call_this_borrow|:19:5: error: '[^']*' borrows 'g[.]c' but is used here after it may be released or overwritten" + "own_err_declared_drops|:15:16: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_exported_virtual_call|:11:16: error: '[^']*' borrows a field but is used here after it may be released or overwritten" "own_err_borrowed_result_stored|borrows the result of 'firstOf' and cannot be stored, returned or captured" - "own_err_borrowed_result_outlives|borrows the result of 'firstOf' but is used here after it may be released or overwritten" + "own_err_borrowed_result_outlives|:19:11: error: '[^']*' borrows the result of 'firstOf' but is used here after it may be released or overwritten" "own_err_borrowed_result_let_outlives|borrows the result of 'H.first' but is used here after it may be released or overwritten" "own_err_borrowed_result_escapes|could return a borrow or keep a parameter, but it is used other than by a call" "own_err_borrowed_result_family|an override in its class family disagrees" - "own_err_param_kept_used|is used here after its value was moved" - "own_err_any_unboxed_outlives|borrows the result of '___unbox<.*>' but is used here after it may be released or overwritten" - "own_err_param_kept_some_paths|is moved here on some paths only" - "own_err_param_kept_loop|is moved inside a loop" + "own_err_param_kept_used|:19:11: error: '[^']*' is used here after its value was moved" + "own_err_any_unboxed_outlives|:11:11: error: '[^']*' borrows the result of '___unbox>>' but is used here after it may be released or overwritten" + "own_err_param_kept_some_paths|:13:9: error: '[^']*' is moved here on some paths only" + "own_err_param_kept_loop|:19:9: error: '[^']*' is moved inside a loop but was made outside it" "own_err_param_kept_not_owned|is given to 'keep', which keeps it, but -mm=own cannot move it here" "own_err_delete|'delete' is not supported by -mm=own yet" - "own_err_mixed_return|is used here after its value was moved" - "own_err_mixed_return_join_use|is moved here on some paths only" + "own_err_mixed_return|:13:28: error: '[^']*' is used here after its value was moved" + "own_err_mixed_return_join_use|:14:15: error: '[^']*' is moved here on some paths only" "own_err_callback_escapes|cannot make the callers of 'pick' give it up" "own_err_callback_disagrees|a function passed in its place disagrees" - "own_err_callback_promoted_used|is used here after its value was moved" - "own_err_kept_param_let_overwritten|borrows a field but is used here after it may be released or overwritten" - "own_err_kept_param_field_overwritten|borrows a field but is used here after it may be released or overwritten" - "own_err_kept_and_borrowed_arg|is used here after its value was moved" - "own_err_kept_and_borrowed_arg_slot|is used here after its value was moved" - "own_err_kept_and_borrowed_arg_field|is used here after its value was moved" - "own_err_kept_twice|is used here after its value was moved" - "own_err_kept_twice_field|is used here after its value was moved" - "own_err_kept_twice_push|is used here after its value was moved" - "own_err_kept_arg_assigned_in_call|is used here after its value was moved" - "own_err_kept_and_borrowed_arg_assigned_in_call|is used here after its value was moved" - "own_err_kept_arg_assigned_on_arm|is used here after its value was moved" - "own_err_kept_arg_assigned_in_and|is used here after its value was moved" - "own_err_call_result_bounded|borrows a field but is used here after it may be released or overwritten" - "own_err_call_result_record|borrows a field but is used here after it may be released or overwritten" + "own_err_callback_promoted_used|:25:26: error: '[^']*' is used here after its value was moved" + "own_err_kept_param_let_overwritten|:30:12: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_kept_param_field_overwritten|:34:12: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_kept_and_borrowed_arg|:41:14: error: '[^']*' is used here after its value was moved" + "own_err_kept_and_borrowed_arg_slot|:44:14: error: '[^']*' is used here after its value was moved" + "own_err_kept_and_borrowed_arg_field|:41:14: error: '[^']*' is used here after its value was moved" + "own_err_kept_twice|:38:14: error: '[^']*' is used here after its value was moved" + "own_err_kept_twice_field|:42:14: error: '[^']*' is used here after its value was moved" + "own_err_kept_twice_push|:36:14: error: '[^']*' is used here after its value was moved" + "own_err_kept_arg_assigned_in_call|:39:20: error: '[^']*' is used here after its value was moved" + "own_err_kept_and_borrowed_arg_assigned_in_call|:37:23: error: '[^']*' is used here after its value was moved" + "own_err_kept_arg_assigned_on_arm|:40:29: error: '[^']*' is used here after its value was moved" + "own_err_kept_arg_assigned_in_and|:40:35: error: '[^']*' is used here after its value was moved" + "own_err_call_result_bounded|:43:5: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_call_result_record|:47:12: error: '[^']*' borrows a field but is used here after it may be released or overwritten" "own_err_param_returned_exported|is a parameter, returned here, but -mm=own cannot tell the callers of 'identity' that the result borrows it" "own_err_param_kept_exported|is a parameter, kept here, but -mm=own cannot make the callers of 'Animal.constructor' give it up" - "own_err_param_assigned|is a parameter and cannot be assigned under -mm=own yet" - "own_err_local_owns_nothing|owns nothing under -mm=own .* so it cannot be given a new value here" - "own_err_merged_result|is merged from several branches" + "own_err_param_assigned|:7:5: error: '[^']*' is a parameter and cannot be assigned under -mm=own yet" + "own_err_local_owns_nothing|:8:5: error: '[^']*' owns nothing under -mm=own [(]it is declared without a value, or in a catch or finally clause[)], so it cannot be given a new value here" + "own_err_merged_result|:7:5: error: '[^']*' is merged from several branches [(]`[?]:`, `&&`, `[|][|]`, `[?][?]`, a loop[)]" "own_err_global_borrowed_result|the initializer of global 'c' holds the result of 'child', which borrows its argument" - "own_err_global_borrowed_temporary|the initializer of global 'c' holds the result of 'id', which borrows its argument" + "own_err_global_borrowed_temporary|:11:11: error: the initializer of global 'c' holds the result of 'id', which borrows its argument" "own_err_global_kept_not_fresh|the initializer of global 'n' gives 'N.constructor' a value it keeps, but -mm=own cannot move it there" "own_err_global_returned|borrows the global 'V.def' and cannot be stored, returned or captured" - "own_err_immortal_borrowed_init|borrows 'K.list.*' and cannot be stored, returned or captured" - "own_err_immortal_field_overwritten|borrows .* but is used here after .* released or overwritten" + "own_err_immortal_borrowed_init|:9:5: error: '[^']*' borrows 'K[.]list\\[\\]' and cannot be stored, returned or captured" + "own_err_immortal_field_overwritten|:15:11: error: '[^']*' borrows 'K[.]n[.]c' but is used here after it may be released or overwritten" "own_err_immortal_region_borrow|borrows the global 'c' and cannot be stored, returned or captured" - "own_err_immortal_region_call|the initializer of global 'c' holds the result of 'id', which borrows its argument" - "own_err_global_loop_store_static|borrows the global 'K[.]w' and cannot be stored, returned or captured" - "own_err_global_loop_store_script|borrows the global 'K[.]w' and cannot be stored, returned or captured" - "own_err_global_const_store_static|borrows 'K[.]w' but is used here after 'K[.]w' is released or overwritten" - "own_err_global_const_store_script|borrows 'K[.]w' but is used here after 'K[.]w' is released or overwritten" - "own_err_global_const_store|borrows 'G' but is used here after 'G' is released or overwritten" - "own_err_global_record_static|borrows 'K[.]w' but is used here after 'K[.]w' is released or overwritten" - "own_err_global_record|borrows 'G' but is used here after 'G' is released or overwritten" - "own_err_global_let_holder_static|borrows 'K[.]w' but is used here after 'K[.]w' is released or overwritten" - "own_err_global_let_destructure|borrows 'K[.]w' but is used here after 'K[.]w' is released or overwritten" - "own_err_global_let_holder|borrows 'G' but is used here after 'G' is released or overwritten" - "own_err_global_outer_holder|borrows 'G' but is used here after 'G' is released or overwritten" - "own_err_global_outer_holder_static|borrows 'K[.]w' but is used here after 'K[.]w' is released or overwritten" - "own_err_global_uninit_let|borrows 'G' but is used here after 'G' is released or overwritten" - "own_err_global_uninit_let_static|borrows 'K[.]w' but is used here after 'K[.]w' is released or overwritten" - "own_err_global_argument_assigned|a value read out of the global 'G' is lent to a call that may release or overwrite it" - "own_err_global_method_assigns|a value read out of the global 'G' is lent to a call that may release or overwrite it" - "own_err_global_argument_transitive|a value read out of the global 'G' is lent to a call that may release or overwrite it" + "own_err_immortal_region_call|:10:11: error: the initializer of global 'c' holds the result of 'id', which borrows its argument" + "own_err_global_loop_store_static|:18:5: error: '[^']*' borrows the global 'K[.]w' and cannot be stored, returned or captured" + "own_err_global_loop_store_script|:16:5: error: '[^']*' borrows the global 'K[.]w' and cannot be stored, returned or captured" + "own_err_global_const_store_static|:21:28: error: '[^']*' borrows 'K[.]w' but is used here after 'K[.]w' is released or overwritten" + "own_err_global_const_store_script|:21:20: error: '[^']*' borrows 'K[.]w' but is used here after 'K[.]w' is released or overwritten" + "own_err_global_const_store|:19:16: error: '[^']*' borrows 'G' but is used here after 'G' is released or overwritten" + "own_err_global_record_static|:30:23: error: '[^']*' borrows 'K[.]w' but is used here after 'K[.]w' is released or overwritten" + "own_err_global_record|:26:11: error: '[^']*' borrows 'G' but is used here after 'G' is released or overwritten" + "own_err_global_let_holder_static|:30:28: error: '[^']*' borrows 'K[.]w' but is used here after 'K[.]w' is released or overwritten" + "own_err_global_let_destructure|:30:20: error: '[^']*' borrows 'K[.]w' but is used here after 'K[.]w' is released or overwritten" + "own_err_global_let_holder|:27:16: error: '[^']*' borrows 'G' but is used here after 'G' is released or overwritten" + "own_err_global_outer_holder|:32:15: error: '[^']*' borrows 'G' but is used here after 'G' is released or overwritten" + "own_err_global_outer_holder_static|:33:23: error: '[^']*' borrows 'K[.]w' but is used here after 'K[.]w' is released or overwritten" + "own_err_global_uninit_let|:28:19: error: '[^']*' borrows 'G' but is used here after 'G' is released or overwritten" + "own_err_global_uninit_let_static|:28:23: error: '[^']*' borrows 'K[.]w' but is used here after 'K[.]w' is released or overwritten" + "own_err_global_argument_assigned|:19:5: error: a value read out of the global 'G' is lent to a call that may release or overwrite it" + "own_err_global_method_assigns|:15:11: error: a value read out of the global 'G' is lent to a call that may release or overwrite it" + "own_err_global_argument_transitive|:23:5: error: a value read out of the global 'G' is lent to a call that may release or overwrite it" "own_err_global_read_callee_assigns|borrows 'G' but is used here after 'G' is released or overwritten" "own_err_global_read_callee_assigns_transitive|borrows 'G' but is used here after 'G' is released or overwritten" "own_err_argument_assigned|is lent to 'g', but is given a new value before the call takes it" "own_err_argument_assigned_conditional|is lent to 'g', but is given a new value before the call takes it" "own_err_argument_assigned_closure|is lent to 'g', but is given a new value before the call takes it" - "own_err_global_method_transitive|a value read out of the global 'G' is lent to a call that may release or overwrite it" + "own_err_global_method_transitive|:19:11: error: a value read out of the global 'G' is lent to a call that may release or overwrite it" "own_err_global_this_field_move|a value read out of the global 'GK' is lent to a call that may release or overwrite it" - "own_err_global_argument_field_let|a value read out of the global 'G' is lent to a call that may release or overwrite it" + "own_err_global_argument_field_let|:21:5: error: a value read out of the global 'G' is lent to a call that may release or overwrite it" "own_err_global_kept_field|a value read out of the global 'g' is lent to a call that may release or overwrite it" "own_err_global_let_call|borrows the global 'names', but -mm=own cannot prove the global keeps its value" - "own_err_global_from_global|the initializer of global 'k' reads a value another owner keeps" - "own_err_global_from_field|the initializer of global 'k' reads a value another owner keeps" - "own_err_captured_returned|borrows a captured variable and cannot be stored, returned or captured" - "own_err_literal_moved_twice|is used here after its value was moved" + "own_err_global_from_global|:8:5: error: the initializer of global 'k' reads a value another owner keeps" + "own_err_global_from_field|:12:7: error: the initializer of global 'k' reads a value another owner keeps" + "own_err_global_kept_typeof|:15:9: error: the value read from 'M[.]w' is kept where its end at an assignment of 'M[.]w' cannot be checked" + "own_err_record_literal_nested|:8:24: error: an object literal that holds a value it owns is moved only into a local or returned as it is" + "own_err_record_literal_let_field|:10:11: error: an object literal that holds a value it owns is moved only into a local or returned as it is" + "own_err_captured_returned|:6:9: error: '[^']*' borrows a captured variable and cannot be stored, returned or captured" + "own_err_literal_moved_twice|:14:16: error: '[^']*' is used here after its value was moved" "own_err_interface_keeps_literal|is a parameter, kept here, but -mm=own cannot make the callers of 'KS.take' give it up" "own_err_interface_keeps_param|is a parameter, kept here, but -mm=own cannot make the callers of 'KV.take' give it up" - "own_err_interface_borrow_param|is a parameter, returned here, but -mm=own cannot tell the callers of 'G.get' that the result borrows it" - "own_err_interface_borrow_element|borrows an element and cannot be stored, returned or captured" - "own_err_interface_borrow_field|borrows a field and cannot be stored, returned or captured" - "own_err_interface_exported_impl|borrows .* but is used here after it may be released or overwritten" + "own_err_interface_borrow_param|:9:44: error: '[^']*' is a parameter, returned here, but -mm=own cannot tell the callers of 'G[.]get' that the result borrows it" + "own_err_interface_borrow_element|:9:66: error: '[^']*' borrows an element and cannot be stored, returned or captured" + "own_err_interface_borrow_field|:9:59: error: '[^']*' borrows a field and cannot be stored, returned or captured" + "own_err_interface_exported_impl|:15:36: error: '[^']*' borrows a field but is used here after it may be released or overwritten" "own_err_interface_family_disagrees|is a parameter, returned here, but -mm=own cannot tell the callers of 'GA.get' that the result borrows it" - "own_err_interface_member_drops|borrows a field but is used here after it may be released or overwritten" - "own_err_interface_from_interface|is a parameter, returned here, but -mm=own cannot tell the callers of 'G.get' that the result borrows it" + "own_err_interface_member_drops|:13:12: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_interface_from_interface|:9:65: error: '[^']*' is a parameter, returned here, but -mm=own cannot tell the callers of 'G[.]get' that the result borrows it" "own_err_interface_result_outlives|borrows the result of 'First.first' but is used here after it may be released or overwritten" - "own_err_interface_fn_field_drops|borrows a field but is used here after it may be released or overwritten" - "own_err_interface_fn_field_unknown|borrows a field but is used here after it may be released or overwritten" - "own_err_interface_fn_field_exported|borrows a field but is used here after it may be released or overwritten" - "own_err_interface_fn_any_param|is a parameter, returned here, but -mm=own cannot tell the callers of 'G.take' that the result borrows it" - "own_err_interface_fn_any_result|is a parameter, returned here, but -mm=own cannot tell the callers of 'G.take' that the result borrows it" - "own_err_interface_fn_any_drops|borrows a field but is used here after it may be released or overwritten" - "own_err_interface_public_exported|is a parameter, returned here, but -mm=own cannot tell the callers of 'PA.pick' that the result borrows it" - "own_err_interface_public_reachable|is a parameter, returned here, but -mm=own cannot tell the callers of 'PA.pick' that the result borrows it" - "own_err_interface_member_types|borrows a field but is used here after it may be released or overwritten" - "own_err_interface_member_methods|borrows a field but is used here after it may be released or overwritten" - "own_err_declared_callback|borrows 'gh.c' but is used here after it may be released or overwritten" - "own_err_declared_callback_global|borrows 'gh.c' but is used here after it may be released or overwritten" - "own_err_declared_callback_method|borrows 'gh.c' but is used here after it may be released or overwritten" - "own_err_declared_callback_tuple|borrows 'gh.c' but is used here after it may be released or overwritten" - "own_err_declared_callback_indirect|borrows 'gh.c' but is used here after it may be released or overwritten" - "own_err_declared_callback_wrapper|borrows 'gh.c' but is used here after it may be released or overwritten" - "own_err_declared_callback_class_method|borrows 'gh.c' but is used here after it may be released or overwritten" - "own_err_declared_callback_interface_field|borrows 'gh.c' but is used here after it may be released or overwritten" - "own_err_declared_callback_stored_global|borrows 'gh.c' but is used here after it may be released or overwritten" - "own_err_declared_callback_stored_reference|borrows 'gh.c' but is used here after it may be released or overwritten" - "own_err_declared_callback_cpp_class|borrows 'gh.c' but is used here after it may be released or overwritten" - "own_err_declared_callback_field_reference|borrows 'gh.c' but is used here after it may be released or overwritten" - "own_err_declared_callback_intrinsic|borrows 'gh.c' but is used here after it may be released or overwritten" - "own_err_interface_member_class|borrows a field but is used here after it may be released or overwritten" - "own_err_interface_member_any|borrows a field but is used here after it may be released or overwritten" - "own_err_override_nameable_base|is a parameter, kept here, but -mm=own cannot make the callers of 'Der.keep' give it up" - "own_err_override_imported_base|is a parameter, kept here, but -mm=own cannot make the callers of 'Der.keep' give it up" - "own_err_override_imported_class|is a parameter, kept here, but -mm=own cannot make the callers of 'Der.keep' give it up" - "own_err_string_kept_param_known|is used here after its value was moved" + "own_err_interface_fn_field_drops|:12:12: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_interface_fn_field_unknown|:8:27: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_interface_fn_field_exported|:6:27: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_interface_fn_any_param|:9:41: error: '[^']*' is a parameter, returned here, but -mm=own cannot tell the callers of 'G[.]take' that the result borrows it" + "own_err_interface_fn_any_result|:9:41: error: '[^']*' is a parameter, returned here, but -mm=own cannot tell the callers of 'G[.]take' that the result borrows it" + "own_err_interface_fn_any_drops|:11:70: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_interface_public_exported|:9:58: error: '[^']*' is a parameter, returned here, but -mm=own cannot tell the callers of 'PA[.]pick' that the result borrows it" + "own_err_interface_public_reachable|:8:35: error: '[^']*' is a parameter, returned here, but -mm=own cannot tell the callers of 'PA[.]pick' that the result borrows it" + "own_err_interface_member_types|:12:70: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_interface_member_methods|:13:90: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_declared_callback|:12:61: error: '[^']*' borrows 'gh[.]c' but is used here after it may be released or overwritten" + "own_err_declared_callback_global|:11:61: error: '[^']*' borrows 'gh[.]c' but is used here after it may be released or overwritten" + "own_err_declared_callback_method|:10:61: error: '[^']*' borrows 'gh[.]c' but is used here after it may be released or overwritten" + "own_err_declared_callback_tuple|:11:61: error: '[^']*' borrows 'gh[.]c' but is used here after it may be released or overwritten" + "own_err_declared_callback_indirect|:11:61: error: '[^']*' borrows 'gh[.]c' but is used here after it may be released or overwritten" + "own_err_declared_callback_wrapper|:11:61: error: '[^']*' borrows 'gh[.]c' but is used here after it may be released or overwritten" + "own_err_declared_callback_class_method|:10:61: error: '[^']*' borrows 'gh[.]c' but is used here after it may be released or overwritten" + "own_err_declared_callback_interface_field|:15:61: error: '[^']*' borrows 'gh[.]c' but is used here after it may be released or overwritten" + "own_err_declared_callback_stored_global|:12:61: error: '[^']*' borrows 'gh[.]c' but is used here after it may be released or overwritten" + "own_err_declared_callback_stored_reference|:12:61: error: '[^']*' borrows 'gh[.]c' but is used here after it may be released or overwritten" + "own_err_declared_callback_cpp_class|:12:61: error: '[^']*' borrows 'gh[.]c' but is used here after it may be released or overwritten" + "own_err_declared_callback_field_reference|:13:61: error: '[^']*' borrows 'gh[.]c' but is used here after it may be released or overwritten" + "own_err_declared_callback_intrinsic|:13:61: error: '[^']*' borrows 'gh[.]c' but is used here after it may be released or overwritten" + "own_err_interface_member_class|:12:70: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_interface_member_any|:10:70: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_override_nameable_base|:6:63: error: '[^']*' is a parameter, kept here, but -mm=own cannot make the callers of 'Der[.]keep' give it up" + "own_err_override_imported_base|:5:63: error: '[^']*' is a parameter, kept here, but -mm=own cannot make the callers of 'Der[.]keep' give it up" + "own_err_override_imported_class|:5:60: error: '[^']*' is a parameter, kept here, but -mm=own cannot make the callers of 'Der[.]keep' give it up" + "own_err_string_kept_param_known|:12:11: error: '[^']*' is used here after its value was moved" "own_err_keeps_mismatch|'@keeps' of 'Pair.constructor' names 'a', but its body keeps 'b'" "own_err_borrows_not_generator|'@borrows' of 'copyOf' names 'w', but it neither makes a generator whose state borrows its arguments nor returns one" "own_err_borrows_mixed_return|'@borrows' of 'pick' names 'w', but it neither makes a generator whose state borrows its arguments nor returns one" @@ -4072,54 +4075,54 @@ set(own_error_cases "own_err_lends_not_borrow|'@lends' of 'made' names 'l', but its body does not return a borrow of it" "own_err_lends_and_borrows|'firstOf' cannot say both '@lends' and '@borrows'" "own_err_lends_two_params|'@lends' of 'pick' names 'a', 'b', but its body returns a borrow of 'a'" - "own_err_lends_index_64|'@lends' of 'K[.]pick' names its argument 64, but a method's '@lends' can name only its arguments 0 to 63" - "own_err_lends_outlives|borrows the result of 'firstOf' but is used here after it may be released or overwritten" + "own_err_lends_index_64|'@lends' of 'K[.]pick' names 'p63', but a method's '@lends' can name only its first 64 arguments, the receiver among them" + "own_err_lends_outlives|:20:11: error: '[^']*' borrows the result of 'firstOf' but is used here after it may be released or overwritten" "own_err_generator_method_this|'this value' is a parameter, kept here, but -mm=own cannot make the callers of 'S.chars' give it up" - "own_err_string_call_keeps_twice|borrows a field and cannot be stored, returned or captured" - "own_err_string_pushed_twice|is used here after its value was moved" - "own_err_shared_borrow_write|but is used here after it may be released or overwritten" - "own_err_shared_borrow_call|but is used here after it may be released or overwritten" - "own_err_shared_borrow_push|but is used here after it may be released or overwritten" - "own_err_shared_moved_in|is used here after its value was moved" - "own_err_shared_borrow_other_type|but is used here after it may be released or overwritten" - "own_err_shared_borrow_push_call|but is used here after it may be released or overwritten" - "own_err_shared_borrow_reassigned|but is used here after it may be released or overwritten" + "own_err_string_call_keeps_twice|:19:5: error: '[^']*' borrows a field and cannot be stored, returned or captured" + "own_err_string_pushed_twice|:9:5: error: '[^']*' is used here after its value was moved" + "own_err_shared_borrow_write|:12:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_shared_borrow_call|:19:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_shared_borrow_push|:17:11: error: '[^']*' borrows an element but is used here after it may be released or overwritten" + "own_err_shared_moved_in|:10:11: error: '[^']*' is used here after its value was moved" + "own_err_shared_borrow_other_type|:29:16: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_shared_borrow_push_call|:20:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_shared_borrow_reassigned|:11:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" "own_err_shared_union_owned|a Shared in a union with 'string', which -mm=own does not count, is not supported" - "own_err_shared_place_arg|borrows a field but is used here after it may be released or overwritten" - "own_err_shared_place_two_uses|borrows a field but is used here after it may be released or overwritten" - "own_err_shared_place_literal|borrows a field and cannot be stored, returned or captured" - "own_err_shared_place_tuple|borrows a field and cannot be stored, returned or captured" + "own_err_shared_place_arg|:29:5: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_shared_place_two_uses|:32:5: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_shared_place_literal|:31:15: error: '[^']*' borrows a field and cannot be stored, returned or captured" + "own_err_shared_place_tuple|:31:39: error: '[^']*' borrows a field and cannot be stored, returned or captured" "own_err_shared_literal_borrowed|the initializer of global 'b' takes a second reference" - "own_err_iface_field_overwritten|borrows a field but is used here after it may be released or overwritten" - "own_err_iface_field_view_overwritten|borrows a field but is used here after it may be released or overwritten" - "own_err_iface_field_call|borrows a field but is used here after it may be released or overwritten" + "own_err_iface_field_overwritten|:23:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_iface_field_view_overwritten|:26:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_iface_field_call|:27:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" "own_err_iface_field_result|borrows the result of 'dirOf' but is used here after it may be released or overwritten" - "own_err_read_after_move_overwritten|is used here after its value was moved" - "own_err_read_after_move_view|is used here after its value was moved" - "own_err_read_after_move_call|is used here after its value was moved" - "own_err_read_after_move_loop|is used here after its value was moved" - "own_err_read_after_move_after_loop|is used here after its value was moved" - "own_err_read_after_move_global|is used here after its value was moved" - "own_err_field_let_global_overwritten|borrows a field but is used here after it may be released or overwritten" - "own_err_conditional_move_read|is used here after its value was moved" - "own_err_conditional_move_outer|is moved inside a loop but was made outside it" - "own_err_conditional_move_read_after_join|is moved here on some paths only" - "own_err_conditional_move_field_borrow|is moved here on some paths only" - "own_err_conditional_move_catch|is moved here on some paths only" - "own_err_conditional_move_bound_method|is moved here on some paths only" - "own_err_conditional_move_view|is moved here on some paths only" - "own_err_conditional_move_old_view|is moved here on some paths only" - "own_err_borrowing_record_stored|borrows the result of .* and cannot be stored, returned or captured" - "own_err_borrowing_record_outlives|borrows the result of .* but is used here after it may be released or overwritten" - "own_err_borrowing_record_kept_arg|borrows a field and cannot be stored, returned or captured" - "own_err_borrowing_record_let_outlives|borrows '.*' but is used here after '.*' is released or overwritten" - "own_err_borrowing_record_returned_local|borrows '.*' and cannot be stored, returned or captured" - "own_err_borrowing_record_field_written|borrows a field and cannot be stored, returned or captured" - "own_err_borrowing_record_cell|borrows a captured variable but is used here after it may be released or overwritten" - "own_err_borrowing_record_kept_param|borrows '.*' but is used here after it may be released or overwritten" - "own_err_borrowing_record_place_overwritten|borrows a field but is used here after it may be released or overwritten" - "own_err_borrowing_record_open|borrows '.*' and cannot be stored, returned or captured" - "own_err_borrowing_record_borrowed_again|borrows the result of .* but is used here after it may be released or overwritten") + "own_err_read_after_move_overwritten|:10:11: error: '[^']*' is used here after its value was moved" + "own_err_read_after_move_view|:12:11: error: '[^']*' is used here after its value was moved" + "own_err_read_after_move_call|:11:11: error: '[^']*' is used here after its value was moved" + "own_err_read_after_move_loop|:25:14: error: '[^']*' is used here after its value was moved" + "own_err_read_after_move_after_loop|:14:11: error: '[^']*' is used here after its value was moved" + "own_err_read_after_move_global|:42:15: error: '[^']*' is used here after its value was moved" + "own_err_field_let_global_overwritten|:42:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_conditional_move_read|:20:11: error: '[^']*' is used here after its value was moved" + "own_err_conditional_move_outer|:19:13: error: '[^']*' is moved inside a loop but was made outside it" + "own_err_conditional_move_read_after_join|:18:9: error: '[^']*' is moved here on some paths only" + "own_err_conditional_move_field_borrow|:22:9: error: '[^']*' is moved here on some paths only" + "own_err_conditional_move_catch|:24:13: error: '[^']*' is moved here on some paths only" + "own_err_conditional_move_bound_method|:43:9: error: '[^']*' is moved here on some paths only" + "own_err_conditional_move_view|:43:9: error: '[^']*' is moved here on some paths only" + "own_err_conditional_move_old_view|:43:9: error: '[^']*' is moved here on some paths only" + "own_err_borrowing_record_stored|:27:5: error: '[^']*' borrows the result of 'Th[.]hit' and cannot be stored, returned or captured" + "own_err_borrowing_record_outlives|:26:11: error: '[^']*' borrows the result of 'A[.]hit' but is used here after it may be released or overwritten" + "own_err_borrowing_record_kept_arg|:24:21: error: '[^']*' borrows a field and cannot be stored, returned or captured" + "own_err_borrowing_record_let_outlives|:24:11: error: '[^']*' borrows '[^']*' but is used here after '[^']*' is released or overwritten" + "own_err_borrowing_record_returned_local|:17:20: error: '[^']*' borrows '[^']*' and cannot be stored, returned or captured" + "own_err_borrowing_record_field_written|:15:9: error: '[^']*' borrows a field and cannot be stored, returned or captured" + "own_err_borrowing_record_cell|:25:16: error: '[^']*' borrows a captured variable but is used here after it may be released or overwritten" + "own_err_borrowing_record_kept_param|:30:12: error: '[^']*' borrows '[^']*' but is used here after it may be released or overwritten" + "own_err_borrowing_record_place_overwritten|:28:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_borrowing_record_open|:17:31: error: '[^']*' borrows '[^']*' and cannot be stored, returned or captured" + "own_err_borrowing_record_borrowed_again|:29:11: error: '[^']*' borrows the result of 'A[.]hit' but is used here after it may be released or overwritten") 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_global_kept_typeof.ts b/tslang/test/tester/own/own_err_global_kept_typeof.ts new file mode 100644 index 000000000..534dd138b --- /dev/null +++ b/tslang/test/tester/own/own_err_global_kept_typeof.ts @@ -0,0 +1,21 @@ +// -mm=own rejects: `k.w = M.w` keeps the read of `M.w` in whatever static the vtable slot of `k` +// holds, so no assignment of `M.w` can be seen ending it (spec 25). `M.w` is mortal: it is written +// through the vtable. Only checkGlobalRead's keep backstop catches this shape; every other rule +// lets it through. +class V { + constructor(public x: number) {} +} + +class M { + static w = new V(2); +} + +class D { + set(k: typeof M) { + k.w = M.w; + } +} + +function main() { + print(M.w.x); +} diff --git a/tslang/test/tester/own/own_err_record_literal_let_field.ts b/tslang/test/tester/own/own_err_record_literal_let_field.ts new file mode 100644 index 000000000..d08f6e292 --- /dev/null +++ b/tslang/test/tester/own/own_err_record_literal_let_field.ts @@ -0,0 +1,14 @@ +// -mm=own rejects (spec 21.2): `r` holds the value of the `let` `v`, which `v` owns, and is not seen as +// an interface (where it would be a borrowing record, spec 32), so it moves only into a local or the +// result, not into the array. +class V { + constructor(public x: number) {} +} + +function main() { + let v = new V(1); + const r = { v: v }; + const a: { v: V }[] = []; + a.push(r); + print(a[0].v.x); +} diff --git a/tslang/test/tester/own/own_err_record_literal_nested.ts b/tslang/test/tester/own/own_err_record_literal_nested.ts new file mode 100644 index 000000000..5097786e1 --- /dev/null +++ b/tslang/test/tester/own/own_err_record_literal_nested.ts @@ -0,0 +1,12 @@ +// -mm=own rejects (spec 21.2): `r` holds a literal nested by value, which it does not own outright, so +// it moves only into a local or the result, not into the array. `{ v: new V(1) }` alone moves. +class V { + constructor(public x: number) {} +} + +function main() { + const r = { inner: { v: new V(1) } }; + const a: { inner: { v: V } }[] = []; + a.push(r); + print(a[0].inner.v.x); +}