diff --git a/include/swift/SIL/OwnershipUtils.h b/include/swift/SIL/OwnershipUtils.h index ce320448c66db..16353143ddca0 100644 --- a/include/swift/SIL/OwnershipUtils.h +++ b/include/swift/SIL/OwnershipUtils.h @@ -9,6 +9,36 @@ // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// +/// +/// Terminology: +/// +/// Simple liveness: +/// - Ends at lifetime-ending operations. +/// - Transitively follows guaranteed forwarding operations and address uses +/// within the current scope. +/// - Assumes inner scopes are complete, including borrow and address scopes +/// - Rarely returns AddressUseKind::PointerEscape. +/// - Per-definition dominance holds +/// - Insulates outer scopes from inner scope details. Maintains the +/// invariant that inlining cannot pessimize optimization. +/// +/// Transitive liveness +/// - Transitively follows uses within inner scopes, including forwarding +/// operations and address uses. +/// - Much more likely to returns AddressUseKind::PointerEscape +/// - Per-definition dominance holds +/// - Does not assume that any scopes are complete. +/// +/// Extended liveness (copy-extension and reborrow-extension) +/// - Extends a live range across lifetime-ending operations +/// - Depending on context: owned values are extended across copies or +/// guaranteed values are extended across reborrows +/// - Copy-extension is used to canonicalize an OSSA lifetime +/// - Reborrow-extension is used to check borrow scopes relative to its inner +/// uses and outer lifetime +/// - Per-definition dominance does not hold +/// +//===----------------------------------------------------------------------===// #ifndef SWIFT_SIL_OWNERSHIPUTILS_H #define SWIFT_SIL_OWNERSHIPUTILS_H @@ -30,7 +60,7 @@ class SILInstruction; class SILModule; class SILValue; class DeadEndBlocks; -class PrunedLiveness; +class MultiDefPrunedLiveness; struct BorrowedValue; /// Returns true if v is an address or trivial. @@ -566,8 +596,10 @@ struct BorrowedValue { bool isLocalScope() const { return kind.isLocalScope(); } - /// Add this scopes live blocks into the PrunedLiveness result. - void computeLiveness(PrunedLiveness &liveness) const; + /// Add this scope's live blocks into the PrunedLiveness result. This + /// includes reborrow scopes that are reachable from this borrow scope but not + /// necessarilly dominated by the borrow scope. + void computeTransitiveLiveness(MultiDefPrunedLiveness &liveness) const; /// Returns true if \p uses are completely within this borrow introducer's /// local scope. @@ -580,8 +612,8 @@ struct BorrowedValue { /// /// \p deadEndBlocks is optional during transition. It will be completely /// removed in an upcoming commit. - bool areUsesWithinTransitiveScope(ArrayRef uses, - DeadEndBlocks *deadEndBlocks) const; + bool areUsesWithinExtendedScope(ArrayRef uses, + DeadEndBlocks *deadEndBlocks) const; /// Given a local borrow scope introducer, visit all non-forwarding consuming /// users. This means that this looks through guaranteed block arguments. \p diff --git a/include/swift/SIL/PrunedLiveness.h b/include/swift/SIL/PrunedLiveness.h index 140fdf48af0ec..b9a75ab4a46a9 100644 --- a/include/swift/SIL/PrunedLiveness.h +++ b/include/swift/SIL/PrunedLiveness.h @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See https://swift.org/LICENSE.txt for license information @@ -12,9 +12,7 @@ /// /// Incrementally compute and represent basic block liveness of a single live /// range. The live range is defined by points in the CFG, independent of any -/// particular SSA value; however, it must be contiguous. Unlike traditional -/// variable liveness, a definition within the live range does not create a -/// "hole" in the live range. The client initializes liveness with a set of +/// particular SSA value. The client initializes liveness with a set of /// definition blocks, typically a single block. The client then incrementally /// updates liveness by providing a set of "interesting" uses one at a time. /// @@ -85,16 +83,15 @@ /// | Use | [LiveWithin] /// ----- /// -/// -/// An invariant is that for any liveness region, the post-dominating blocks of -/// the region are the LiveWithin regions. -/// //===----------------------------------------------------------------------===// #ifndef SWIFT_SILOPTIMIZER_UTILS_PRUNEDLIVENESS_H #define SWIFT_SILOPTIMIZER_UTILS_PRUNEDLIVENESS_H #include "swift/AST/TypeExpansionContext.h" +#include "swift/SIL/BasicBlockDatastructures.h" +#include "swift/SIL/NodeDatastructures.h" +#include "swift/SIL/OwnershipUtils.h" #include "swift/SIL/SILBasicBlock.h" #include "swift/SIL/SILFunction.h" #include "llvm/ADT/MapVector.h" @@ -109,15 +106,14 @@ class DeadEndBlocks; /// liveness by first initializing "def" blocks, then incrementally feeding uses /// to updateForUse(). /// -/// For SSA live ranges, a single "def" block will dominate all uses. If no def -/// block is provided, liveness is computed as if defined by a function -/// argument. If the client does not provide a single, dominating def block, -/// then the client must at least ensure that no uses precede the first -/// definition in a def block. Since this analysis does not remember the -/// positions of defs, it assumes that, within a block, uses follow -/// defs. Breaking this assumption will result in a "hole" in the live range in -/// which the def block's predecessors incorrectly remain dead. This situation -/// could be handled by adding an updateForUseBeforeFirstDef() API. +/// Incrementally building liveness is important for algorithms that create an +/// initial live region, perform some analysis on that, then expand the live +/// region by adding new uses before continuing the analysis. +/// +/// Initializing "def blocks" restricts liveness on any path through those def +/// blocks to the blocks that occur on or after the def block. If any uses is +/// not dominated by a def block, then liveness will include the entry block, +/// as if defined by a function argument /// /// We allow for multiple bits of liveness information to be tracked by /// internally using a SmallBitVector. The multiple bit tracking is useful when @@ -127,8 +123,7 @@ class DeadEndBlocks; /// represent dead by not having liveness state for a block. With multiple bits /// possible this is no longer true. /// -/// TODO: This can be made space-efficient if all clients can maintain a block -/// numbering so liveness info can be represented as bitsets across the blocks. +/// TODO: For efficiency, use BasicBlockBitfield rather than SmallDenseMap. class PrunedLiveBlocks { public: /// Per-block liveness state computed during backward dataflow propagation. @@ -173,6 +168,8 @@ class PrunedLiveBlocks { unsigned size() const { return bits.size() / 2; } + // FIXME: specialize this for scalar liveness, which is the critical path + // for all OSSA utilities. IsLive getLiveness(unsigned bitNo) const { SmallVector foundLiveness; getLiveness(bitNo, bitNo + 1, foundLiveness); @@ -282,6 +279,8 @@ class PrunedLiveBlocks { return isLive[0]; } + // FIXME: This API should directly return the live bitset. The live bitset + // type should have an api for querying and iterating over the live fields. void getBlockLiveness(SILBasicBlock *bb, unsigned startBitNo, unsigned endBitNo, SmallVectorImpl &foundLivenessInfo) const { @@ -297,6 +296,10 @@ class PrunedLiveBlocks { liveBlockIter->second.getLiveness(startBitNo, endBitNo, foundLivenessInfo); } + llvm::StringRef getStringRef(IsLive isLive) const; + void print(llvm::raw_ostream &OS) const; + void dump() const; + protected: void markBlockLive(SILBasicBlock *bb, unsigned bitNo, IsLive isLive) { markBlockLive(bb, bitNo, bitNo + 1, isLive); @@ -327,6 +330,51 @@ class PrunedLiveBlocks { unsigned endBitNo); }; +/// If inner borrows are 'Contained', then liveness is fully described by the +/// scope-ending instructions of any inner borrow scopes, and those scope-ending +/// uses are dominated by the current def. This is known as a "simple" live +/// range. +/// +/// If nested borrows are 'Reborrowed' then simple liveness computed here based +/// on dominated uses is not sufficient to guarantee the value's lifetime. To do +/// that, the client needs to consider the reborrow scopes. OSSALiveness handles +/// those details. +/// +/// Reborrows are only relevant when they apply to the first level of borrow +/// scope. Reborrows within nested borrows scopes are already summarized by the +/// outer borrow scope. +enum class InnerBorrowKind { + Contained, // any borrows are fully contained within this live range + Reborrowed // at least one immediately nested borrow is reborrowed +}; + +inline InnerBorrowKind meet(InnerBorrowKind lhs, InnerBorrowKind rhs) { + return (lhs > rhs) ? lhs : rhs; +} + +/// Summarize reborrows and pointer escapes that affect a live range. Reborrows +/// and pointer escapes that are encapsulated in a nested borrow don't affect +/// the outer live range. +struct SimpleLiveRangeSummary { + InnerBorrowKind innerBorrowKind; + AddressUseKind addressUseKind; + + SimpleLiveRangeSummary(): innerBorrowKind(InnerBorrowKind::Contained), + addressUseKind(AddressUseKind::NonEscaping) + {} + + void meet(const InnerBorrowKind lhs) { + innerBorrowKind = swift::meet(innerBorrowKind, lhs); + } + void meet(const AddressUseKind lhs) { + addressUseKind = swift::meet(addressUseKind, lhs); + } + void meet(const SimpleLiveRangeSummary lhs) { + meet(lhs.innerBorrowKind); + meet(lhs.addressUseKind); + } +}; + /// PrunedLiveness tracks PrunedLiveBlocks along with "interesting" use /// points. The set of interesting uses is a superset of all uses on the /// liveness boundary. Filtering out uses that are obviously not on the liveness @@ -343,6 +391,11 @@ class PrunedLiveBlocks { /// boundary, then it is up to the client to figure out how to "extend" the /// lifetime beyond those uses. /// +/// Note: a live-out block may contain a lifetime-ending use. This happens when +/// the client is computing "extended" livenes, for example by ignoring +/// copies. Lifetime ending uses are irrelevant for finding the liveness +/// boundary. +/// /// Note: unlike OwnershipLiveRange, this represents a lifetime in terms of the /// CFG boundary rather that the use set, and, because it is "pruned", it only /// includes liveness generated by select uses. For example, it does not @@ -362,26 +415,9 @@ class PrunedLiveness { // Non-lifetime-ending within a LiveOut block are uninteresting. llvm::SmallMapVector users; - /// A side array that stores any non lifetime ending uses we find in live out - /// blocks. This is used to enable our callers to emit errors on non-lifetime - /// ending uses that extend liveness into a loop body. - SmallSetVector *nonLifetimeEndingUsesInLiveOut; - -private: - bool isWithinBoundaryHelper(SILInstruction *inst, SILValue def) const; - - bool areUsesWithinBoundaryHelper(ArrayRef uses, SILValue def, - DeadEndBlocks *deadEndBlocks) const; - - bool areUsesOutsideBoundaryHelper(ArrayRef uses, SILValue def, - DeadEndBlocks *deadEndBlocks) const; - public: - PrunedLiveness(SmallVectorImpl *discoveredBlocks = nullptr, - SmallSetVector - *nonLifetimeEndingUsesInLiveOut = nullptr) - : liveBlocks(1 /*num bits*/, discoveredBlocks), - nonLifetimeEndingUsesInLiveOut(nonLifetimeEndingUsesInLiveOut) {} + PrunedLiveness(SmallVectorImpl *discoveredBlocks = nullptr) + : liveBlocks(1 /*num bits*/, discoveredBlocks) {} bool empty() const { assert(!liveBlocks.empty() || users.empty()); @@ -391,8 +427,6 @@ class PrunedLiveness { void clear() { liveBlocks.clear(); users.clear(); - if (nonLifetimeEndingUsesInLiveOut) - nonLifetimeEndingUsesInLiveOut->clear(); } unsigned numLiveBlocks() const { return liveBlocks.numLiveBlocks(); } @@ -403,47 +437,6 @@ class PrunedLiveness { return liveBlocks.getDiscoveredBlocks(); } - using NonLifetimeEndingUsesInLiveOutRange = - iterator_range; - - NonLifetimeEndingUsesInLiveOutRange - getNonLifetimeEndingUsesInLiveOut() const { - assert(nonLifetimeEndingUsesInLiveOut && - "Called without passing in nonLifetimeEndingUsesInLiveOut to " - "constructor?!"); - return llvm::make_range(nonLifetimeEndingUsesInLiveOut->begin(), - nonLifetimeEndingUsesInLiveOut->end()); - } - - using NonLifetimeEndingUsesInLiveOutBlocksRange = - TransformRange>; - NonLifetimeEndingUsesInLiveOutBlocksRange - getNonLifetimeEndingUsesInLiveOutBlocks() const { - function_ref op; - op = [](const SILInstruction *&ptr) -> SILBasicBlock * { - return ptr->getParent(); - }; - return NonLifetimeEndingUsesInLiveOutBlocksRange( - getNonLifetimeEndingUsesInLiveOut(), op); - } - - using UserRange = iterator_range *>; - UserRange getAllUsers() const { - return llvm::make_range(users.begin(), users.end()); - } - - using UserBlockRange = TransformRange< - UserRange, - function_ref &)>>; - UserBlockRange getAllUserBlocks() const { - function_ref &)> op; - op = [](const std::pair &pair) -> SILBasicBlock * { - return pair.first->getParent(); - }; - return UserBlockRange(getAllUsers(), op); - } - void initializeDefBlock(SILBasicBlock *defBB) { liveBlocks.initializeDefBlock(defBB, 0); } @@ -456,8 +449,15 @@ class PrunedLiveness { void updateForUse(SILInstruction *user, bool lifetimeEnding); /// Updates the liveness for a whole borrow scope, beginning at \p op. - /// Returns false if this cannot be done. - bool updateForBorrowingOperand(Operand *op); + /// Returns false if this cannot be done. This assumes that nested OSSA + /// lifetimes are complete. + InnerBorrowKind updateForBorrowingOperand(Operand *operand); + + /// Update liveness for an interior pointer use. These are normally handled + /// like an instantaneous use. But if \p operand "borrows" a value for the + /// duration of a scoped address (store_borrow), then update liveness for the + /// entire scope. This assumes that nested OSSA lifetimes are complete. + AddressUseKind checkAndUpdateInteriorPointer(Operand *operand); /// Update this liveness to extend across the given liveness. void extendAcrossLiveness(PrunedLiveness &otherLiveness); @@ -466,7 +466,11 @@ class PrunedLiveness { return liveBlocks.getBlockLiveness(bb, 0); } - enum IsInterestingUser { NonUser, NonLifetimeEndingUse, LifetimeEndingUse }; + enum IsInterestingUser { + NonUser = 0, + NonLifetimeEndingUse, + LifetimeEndingUse + }; /// Return a result indicating whether the given user was identified as an /// interesting use of the current def and whether it ends the lifetime. @@ -477,50 +481,30 @@ class PrunedLiveness { return useIter->second ? LifetimeEndingUse : NonLifetimeEndingUse; } - /// Return true if \p inst occurs before the liveness boundary. Used when the - /// client already knows that inst occurs after the start of liveness. - bool isWithinBoundary(SILInstruction *inst) const; - - /// \p deadEndBlocks is optional. - bool areUsesWithinBoundary(ArrayRef uses, - DeadEndBlocks *deadEndBlocks) const; - - /// \p deadEndBlocks is optional. - bool areUsesOutsideBoundary(ArrayRef uses, - DeadEndBlocks *deadEndBlocks) const; - - /// PrunedLiveness utilities can be used with multiple defs. This api can be - /// used to check if \p inst occurs in between the definition \p def and the - /// liveness boundary. - // This api varies from isWithinBoundary(SILInstruction *inst) which cannot - // distinguish when \p inst is a use before definition in the same block as - // the definition. - bool isWithinBoundaryOfDef(SILInstruction *inst, SILValue def) const; - - /// Returns true when all \p uses are between \p def and the liveness boundary - /// \p deadEndBlocks is optional. - bool areUsesWithinBoundaryOfDef(ArrayRef uses, SILValue def, - DeadEndBlocks *deadEndBlocks) const; - - /// Returns true if any of the \p uses are before the \p def or after the - /// liveness boundary - /// \p deadEndBlocks is optional. - bool areUsesOutsideBoundaryOfDef(ArrayRef uses, SILValue def, - DeadEndBlocks *deadEndBlocks) const; - - /// Compute liveness for a single SSA definition. - void computeSSALiveness(SILValue def); + void print(llvm::raw_ostream &OS) const; + void dump() const; }; /// Record the last use points and CFG edges that form the boundary of /// PrunedLiveness. +/// +/// Dead defs may occur even when the liveness result has uses for every +/// definition because those uses may occur in unreachable blocks. A dead def +/// must either be a SILInstruction or SILArgument. This supports memory +/// location liveness, so there isn't necessary a defining SILValue. +/// +/// Each boundary edge is identified by its target block. The source of the edge +/// is the target block's single predecessor which must have at least one other +/// non-boundary successor. struct PrunedLivenessBoundary { SmallVector lastUsers; SmallVector boundaryEdges; + SmallVector deadDefs; void clear() { lastUsers.clear(); boundaryEdges.clear(); + deadDefs.clear(); } /// Visit the point at which a lifetime-ending instruction must be inserted, @@ -531,6 +515,50 @@ struct PrunedLivenessBoundary { llvm::function_ref visitor, DeadEndBlocks *deBlocks = nullptr); + void print(llvm::raw_ostream &OS) const; + void dump() const; +}; + +/// PrunedLiveness with information about defs for computing the live range +/// boundary. +/// +/// LivenessWithDefs implements: +/// +/// bool isInitialized() const +/// +/// bool isDef(SILInstruction *inst) const +/// +/// bool isDefBlock(SILBasicBlock *block) const +/// +template +class PrunedLiveRange : public PrunedLiveness { +protected: + const LivenessWithDefs &asImpl() const { + return static_cast(*this); + } + + PrunedLiveRange(SmallVectorImpl *discoveredBlocks = nullptr) + : PrunedLiveness(discoveredBlocks) {} + +public: + /// Update liveness for all direct uses of \p def. + SimpleLiveRangeSummary updateForDef(SILValue def); + + /// Check if \p inst occurs in between the definition this def and the + /// liveness boundary. + bool isWithinBoundary(SILInstruction *inst) const; + + /// Returns true when all \p uses are between this def and the liveness + /// boundary \p deadEndBlocks is optional. + bool areUsesWithinBoundary(ArrayRef uses, + DeadEndBlocks *deadEndBlocks) const; + + /// Returns true if any of the \p uses are before this def or after the + /// liveness boundary + /// \p deadEndBlocks is optional. + bool areUsesOutsideBoundary(ArrayRef uses, + DeadEndBlocks *deadEndBlocks) const; + /// Compute the boundary from the blocks discovered during liveness analysis. /// /// Precondition: \p liveness.getDiscoveredBlocks() is a valid list of all @@ -539,7 +567,7 @@ struct PrunedLivenessBoundary { /// The computed boundary will completely post-dominate, including dead end /// paths. The client should query DeadEndBlocks to ignore those dead end /// paths. - void compute(const PrunedLiveness &liveness); + void computeBoundary(PrunedLivenessBoundary &boundary) const; /// Compute the boundary from a backward CFG traversal from a known set of /// jointly post-dominating blocks. Avoids the need to record an ordered list @@ -552,8 +580,195 @@ struct PrunedLivenessBoundary { /// resulting partial boundary will have holes along those paths. The dead end /// successors of blocks in this live set on are not necessarily identified /// by DeadEndBlocks. - void compute(const PrunedLiveness &liveness, - ArrayRef postDomBlocks); + void computeBoundary(PrunedLivenessBoundary &boundary, + ArrayRef postDomBlocks) const; +}; + +// Singly-defined liveness. +// +// An SSA def results in pruned liveness with a contiguous liverange. +// +// An unreachable self-loop might result in a "gap" between the last use above +// the def in the same block. +// +// For SSA live ranges, a single "def" block dominates all uses. If no def +// block is provided, liveness is computed as if defined by a function +// argument. If the client does not provide a single, dominating def block, +// then the client must at least ensure that no uses precede the first +// definition in a def block. Since this analysis does not remember the +// positions of defs, it assumes that, within a block, uses follow +// defs. Breaking this assumption will result in a "hole" in the live range in +// which the def block's predecessors incorrectly remain dead. This situation +// could be handled by adding an updateForUseBeforeFirstDef() API. +class SSAPrunedLiveness : public PrunedLiveRange { + SILValue def; + SILInstruction *defInst = nullptr; // nullptr for argument defs. + +public: + SSAPrunedLiveness( + SmallVectorImpl *discoveredBlocks = nullptr) + : PrunedLiveRange(discoveredBlocks) {} + + SILValue getDef() const { return def; } + + void clear() { + def = SILValue(); + defInst = nullptr; + PrunedLiveRange::clear(); + } + + void initializeDef(SILValue def) { + assert(!this->def && "reinitialization"); + + this->def = def; + defInst = def->getDefiningInstruction(); + initializeDefBlock(def->getParentBlock()); + } + + bool isInitialized() const { return bool(def); } + + bool isDef(SILInstruction *inst) const { return inst == defInst; } + + bool isDefBlock(SILBasicBlock *block) const { + return def->getParentBlock() == block; + } + + /// SSA implementation of computeBoundary. + void findBoundariesInBlock(SILBasicBlock *block, bool isLiveOut, + PrunedLivenessBoundary &boundary) const; + + /// Compute liveness for a single SSA definition. The lifetime-ending uses are + /// also recorded--destroy_value or end_borrow. + /// + /// This only handles simple liveness in which all uses are dominated by the + /// definition. If the returned summary includes InnerBorrowKind::Reborrow, + /// then the resulting liveness does not includes potentially non-dominated + /// uses within the reborrow scope. If the summary returns something other + /// than AddressUseKind::NonEscaping, then the resulting liveness does not + /// necessarilly encapsulate value ownership. + /// + /// Warning: If OSSA lifetimes are incomplete, then destroy_values might not + /// jointly-post dominate if dead-end blocks are present. Nested scopes may + /// also lack scope-ending instructions, so the liveness of their nested uses + /// may be ignored. + SimpleLiveRangeSummary computeSimple() { + assert(def && "SSA def uninitialized"); + return updateForDef(def); + } +}; + +/// MultiDefPrunedLiveness is computed incrementally by calling updateForUse. +/// +/// Defs should be initialized before calling updatingForUse on any def +/// that reaches the use. +class MultiDefPrunedLiveness : public PrunedLiveRange { + NodeSetVector defs; + BasicBlockSet defBlocks; + +public: + MultiDefPrunedLiveness( + SILFunction *function, + SmallVectorImpl *discoveredBlocks = nullptr) + : PrunedLiveRange(discoveredBlocks), defs(function), defBlocks(function) { + } + + void clear() { + llvm_unreachable("multi-def liveness cannot be reused"); + } + + void initializeDef(SILNode *def) { + assert(isa(def) || isa(def)); + defs.insert(def); + auto *block = def->getParentBlock(); + defBlocks.insert(block); + initializeDefBlock(block); + } + + bool isInitialized() const { return !defs.empty(); } + + bool isDef(SILInstruction *inst) const { + return defs.contains(cast(inst)); + } + + bool isDefBlock(SILBasicBlock *block) const { + return defBlocks.contains(block); + } + + /// Multi-Def implementation of computeBoundary. + void findBoundariesInBlock(SILBasicBlock *block, bool isLiveOut, + PrunedLivenessBoundary &boundary) const; + + /// Compute liveness for a all currently initialized definitions. The + /// lifetime-ending uses are also recorded--destroy_value or + /// end_borrow. However destroy_values might not jointly-post dominate if + /// dead-end blocks are present. + /// + /// This only handles simple liveness in which all uses are dominated by the + /// definition. If the returned summary includes InnerBorrowKind::Reborrow, + /// then the resulting liveness does not includes potentially non-dominated + /// uses within the reborrow scope. If the summary returns something other + /// than AddressUseKind::NonEscaping, then the resulting liveness does not + /// necessarilly encapsulate value ownership. + /// + /// Warning: If OSSA lifetimes are incomplete, then destroy_values might not + /// jointly-post dominate if dead-end blocks are present. Nested scopes may + /// also lack scope-ending instructions, so the liveness of their nested uses + /// may be ignored. + SimpleLiveRangeSummary computeSimple(); +}; + +//===----------------------------------------------------------------------===// +// DiagnosticPrunedLiveness +//===----------------------------------------------------------------------===// + +// FIXME: it isn't clear what this is for or what nonLifetimeEndingUseInLiveOut +// means precisely. +class DiagnosticPrunedLiveness : public SSAPrunedLiveness { + /// A side array that stores any non lifetime ending uses we find in live out + /// blocks. This is used to enable our callers to emit errors on non-lifetime + /// ending uses that extend liveness into a loop body. + SmallSetVector *nonLifetimeEndingUsesInLiveOut; + +public: + DiagnosticPrunedLiveness( + SmallVectorImpl *discoveredBlocks = nullptr, + SmallSetVector *nonLifetimeEndingUsesInLiveOut = + nullptr) + : SSAPrunedLiveness(discoveredBlocks), + nonLifetimeEndingUsesInLiveOut(nonLifetimeEndingUsesInLiveOut) {} + + void clear() { + SSAPrunedLiveness::clear(); + if (nonLifetimeEndingUsesInLiveOut) + nonLifetimeEndingUsesInLiveOut->clear(); + } + + void updateForUse(SILInstruction *user, bool lifetimeEnding); + + using NonLifetimeEndingUsesInLiveOutRange = + iterator_range; + + NonLifetimeEndingUsesInLiveOutRange + getNonLifetimeEndingUsesInLiveOut() const { + assert(nonLifetimeEndingUsesInLiveOut && + "Called without passing in nonLifetimeEndingUsesInLiveOut to " + "constructor?!"); + return llvm::make_range(nonLifetimeEndingUsesInLiveOut->begin(), + nonLifetimeEndingUsesInLiveOut->end()); + } + + using NonLifetimeEndingUsesInLiveOutBlocksRange = + TransformRange>; + NonLifetimeEndingUsesInLiveOutBlocksRange + getNonLifetimeEndingUsesInLiveOutBlocks() const { + function_ref op; + op = [](const SILInstruction *&ptr) -> SILBasicBlock * { + return ptr->getParent(); + }; + return NonLifetimeEndingUsesInLiveOutBlocksRange( + getNonLifetimeEndingUsesInLiveOut(), op); + } }; //===----------------------------------------------------------------------===// diff --git a/include/swift/SIL/ScopedAddressUtils.h b/include/swift/SIL/ScopedAddressUtils.h index 7e5155e6ea3fb..b176eeae947f5 100644 --- a/include/swift/SIL/ScopedAddressUtils.h +++ b/include/swift/SIL/ScopedAddressUtils.h @@ -15,6 +15,7 @@ #include "swift/Basic/Debug.h" #include "swift/Basic/LLVM.h" +#include "swift/SIL/OwnershipUtils.h" #include "swift/SIL/PrunedLiveness.h" #include "swift/SIL/SILBasicBlock.h" #include "swift/SIL/SILInstruction.h" @@ -83,20 +84,39 @@ struct ScopedAddressValue { SILValue operator*() { return value; } SILValue operator*() const { return value; } - /// Returns true if \p op is a scope edning use of the scoped address value. + /// Returns true if \p op is a scope ending use of the scoped address value. bool isScopeEndingUse(Operand *op) const; /// Pass all scope ending instructions to the visitor. bool visitScopeEndingUses(function_ref visitor) const; - /// Returns false, if liveness cannot be computed due to pointer escape or - /// unkown address use. Add this scope's live blocks into the PrunedLiveness - /// result. - bool computeLiveness(PrunedLiveness &liveness) const; + + /// Optimistically computes liveness for all transitive uses, and adds this + /// scope's live blocks into the SSA PrunedLiveness result. Returns + /// AddressUseKind indicated whether a PointerEscape or Unknown use was + /// encountered. + /// + /// This transitively finds uses within nested borrow scopes to handle + /// incomplete nested lifetimes. Here, liveness will consider the apply to be + /// a live use of the store_borrow: + /// %a = store_borrow + /// %v = load_borrow + /// apply (%v) + /// unreachable + /// + /// FIXME: with complete OSSA lifetimes, store borrow liveness is simply + /// computed by visiting the end_borrow users. + AddressUseKind computeTransitiveLiveness(SSAPrunedLiveness &liveness) const; + + /// Update \p liveness for all the transitive address uses. + /// + /// Valid for any type of liveness, SSA or MultiDef, that may be used by a + /// scoped address. + AddressUseKind updateTransitiveLiveness(PrunedLiveness &liveness) const; /// Create appropriate scope ending instruction at \p insertPt. void createScopeEnd(SILBasicBlock::iterator insertPt, SILLocation loc) const; /// Create scope ending instructions at \p liveness boundary. - void endScopeAtLivenessBoundary(PrunedLiveness *liveness) const; + void endScopeAtLivenessBoundary(SSAPrunedLiveness *liveness) const; }; llvm::raw_ostream &operator<<(llvm::raw_ostream &os, @@ -105,7 +125,7 @@ llvm::raw_ostream &operator<<(llvm::raw_ostream &os, /// Returns true if there are other store_borrows enclosed within a store_borrow /// \p sbi's scope bool hasOtherStoreBorrowsInLifetime(StoreBorrowInst *sbi, - PrunedLiveness *liveness, + SSAPrunedLiveness *liveness, DeadEndBlocks *deadEndBlocks); /// Extend the store_borrow \p sbi's scope such that it encloses \p newUsers. diff --git a/include/swift/SILOptimizer/PassManager/Passes.def b/include/swift/SILOptimizer/PassManager/Passes.def index 2a9df82a420df..a204408029b49 100644 --- a/include/swift/SILOptimizer/PassManager/Passes.def +++ b/include/swift/SILOptimizer/PassManager/Passes.def @@ -332,6 +332,8 @@ SWIFT_FUNCTION_PASS(ObjCBridgingOptimization, "objc-bridging-opt", "Optimize ObjectiveC briging operations") PASS(ObjectOutliner, "object-outliner", "Outlining of Global Objects") +PASS(OSSALifetimeAnalysis, "ossa-lifetime-analysis", + "Dump OSSA Lifetimes for Traced Values") PASS(Outliner, "outliner", "Function Outlining Optimization") PASS(OwnershipModelEliminator, "ownership-model-eliminator", diff --git a/include/swift/SILOptimizer/Utils/CanonicalOSSALifetime.h b/include/swift/SILOptimizer/Utils/CanonicalOSSALifetime.h index cdbbbd8c05190..e020de3b4eecc 100644 --- a/include/swift/SILOptimizer/Utils/CanonicalOSSALifetime.h +++ b/include/swift/SILOptimizer/Utils/CanonicalOSSALifetime.h @@ -267,9 +267,6 @@ class CanonicalizeOSSALifetime { InstructionDeleter &deleter; - /// Current copied def for which this state describes the liveness. - SILValue currentDef; - /// Original points in the CFG where the current value's lifetime is consumed /// or destroyed. For guaranteed values it remains empty. A backward walk from /// these blocks must discover all uses on paths that lead to a return or @@ -293,7 +290,7 @@ class CanonicalizeOSSALifetime { /// Pruned liveness for the extended live range including copies. For this /// purpose, only consuming instructions are considered "lifetime /// ending". end_borrows do not end a liverange that may include owned copies. - PrunedLiveness liveness; + SSAPrunedLiveness liveness; /// The destroys of the value. These are not uses, but need to be recorded so /// that we know when the last use in a consuming block is (without having to @@ -355,7 +352,7 @@ class CanonicalizeOSSALifetime { accessBlockAnalysis(accessBlockAnalysis), domTree(domTree), deleter(deleter) {} - SILValue getCurrentDef() const { return currentDef; } + SILValue getCurrentDef() const { return liveness.getDef(); } void initDef(SILValue def) { assert(consumingBlocks.empty() && debugValues.empty() && liveness.empty()); @@ -364,8 +361,7 @@ class CanonicalizeOSSALifetime { accessBlocks = nullptr; consumes.clear(); - currentDef = def; - liveness.initializeDefBlock(def->getParentBlock()); + liveness.initializeDef(def); } void clearLiveness() { diff --git a/include/swift/SILOptimizer/Utils/CanonicalizeBorrowScope.h b/include/swift/SILOptimizer/Utils/CanonicalizeBorrowScope.h index 98e8085a3da06..65f4d0fda6183 100644 --- a/include/swift/SILOptimizer/Utils/CanonicalizeBorrowScope.h +++ b/include/swift/SILOptimizer/Utils/CanonicalizeBorrowScope.h @@ -61,7 +61,7 @@ class CanonicalizeBorrowScope { /// Pruned liveness for the extended live range including copies. For this /// purpose, only consuming instructions are considered "lifetime /// ending". end_borrows do not end a liverange that may include owned copies. - PrunedLiveness liveness; + SSAPrunedLiveness liveness; InstructionDeleter &deleter; @@ -87,7 +87,7 @@ class CanonicalizeBorrowScope { BorrowedValue getBorrowedValue() const { return borrowedValue; } - const PrunedLiveness &getLiveness() const { return liveness; } + const SSAPrunedLiveness &getLiveness() const { return liveness; } InstructionDeleter &getDeleter() { return deleter; } @@ -137,7 +137,7 @@ class CanonicalizeBorrowScope { updatedCopies.clear(); borrowedValue = borrow; - liveness.initializeDefBlock(borrowedValue->getParentBlock()); + liveness.initializeDef(borrowedValue.value); } bool computeBorrowLiveness(); diff --git a/include/swift/SILOptimizer/Utils/OwnershipOptUtils.h b/include/swift/SILOptimizer/Utils/OwnershipOptUtils.h index a60177a1ef5d3..91d7aa050997c 100644 --- a/include/swift/SILOptimizer/Utils/OwnershipOptUtils.h +++ b/include/swift/SILOptimizer/Utils/OwnershipOptUtils.h @@ -100,15 +100,16 @@ class GuaranteedOwnershipExtension { DeadEndBlocks &deBlocks; // --- analysis state - PrunedLiveness guaranteedLiveness; - PrunedLiveness ownedLifetime; + MultiDefPrunedLiveness guaranteedLiveness; + SSAPrunedLiveness ownedLifetime; SmallVector ownedConsumeBlocks; BeginBorrowInst *beginBorrow = nullptr; public: GuaranteedOwnershipExtension(InstructionDeleter &deleter, - DeadEndBlocks &deBlocks) - : deleter(deleter), deBlocks(deBlocks) {} + DeadEndBlocks &deBlocks, SILFunction *function) + : deleter(deleter), deBlocks(deBlocks), + guaranteedLiveness(function) {} void clear() { guaranteedLiveness.clear(); diff --git a/lib/SIL/Utils/OwnershipUtils.cpp b/lib/SIL/Utils/OwnershipUtils.cpp index 200e1cb3467ed..2fdc555ecfbf8 100644 --- a/lib/SIL/Utils/OwnershipUtils.cpp +++ b/lib/SIL/Utils/OwnershipUtils.cpp @@ -753,22 +753,23 @@ llvm::raw_ostream &swift::operator<<(llvm::raw_ostream &os, } /// Add this scopes live blocks into the PrunedLiveness result. -void BorrowedValue::computeLiveness(PrunedLiveness &liveness) const { - liveness.initializeDefBlock(value->getParentBlock()); +void BorrowedValue:: +computeTransitiveLiveness(MultiDefPrunedLiveness &liveness) const { + liveness.initializeDef(value); visitTransitiveLifetimeEndingUses([&](Operand *endOp) { if (endOp->getOperandOwnership() == OperandOwnership::EndBorrow) { liveness.updateForUse(endOp->getUser(), /*lifetimeEnding*/ true); return true; } assert(endOp->getOperandOwnership() == OperandOwnership::Reborrow); - auto *succBlock = cast(endOp->getUser())->getDestBB(); - liveness.initializeDefBlock(succBlock); + PhiOperand phiOper(endOp); + liveness.initializeDef(phiOper.getValue()); liveness.updateForUse(endOp->getUser(), /*lifetimeEnding*/ false); return true; }); } -bool BorrowedValue::areUsesWithinTransitiveScope( +bool BorrowedValue::areUsesWithinExtendedScope( ArrayRef uses, DeadEndBlocks *deadEndBlocks) const { // First make sure that we actually have a local scope. If we have a non-local // scope, then we have something (like a SILFunctionArgument) where a larger @@ -779,8 +780,8 @@ bool BorrowedValue::areUsesWithinTransitiveScope( return true; // Compute the local scope's liveness. - PrunedLiveness liveness; - computeLiveness(liveness); + MultiDefPrunedLiveness liveness(value->getFunction()); + computeTransitiveLiveness(liveness); return liveness.areUsesWithinBoundary(uses, deadEndBlocks); } @@ -922,6 +923,10 @@ bool BorrowedValue::visitInteriorPointerOperandHelper( return true; } +// FIXME: This does not yet assume complete lifetimes. Therefore, it currently +// recursively looks through scoped uses, such as load_borrow. We should +// separate the logic for lifetime completion from the logic that can assume +// complete lifetimes. AddressUseKind swift::findTransitiveUsesForAddress(SILValue projectedAddress, SmallVectorImpl *foundUses, @@ -1018,10 +1023,12 @@ swift::findTransitiveUsesForAddress(SILValue projectedAddress, // If we have a load_borrow, add it's end scope to the liveness requirement. if (auto *lbi = dyn_cast(user)) { if (foundUses) { - for (Operand *use : lbi->getUses()) { - if (use->endsLocalBorrowScope()) { - leafUse(use); - } + // FIXME: if we can assume complete lifetimes, then this should be + // as simple as: + // for (Operand *use : lbi->getUses()) { + // if (use->endsLocalBorrowScope()) { + if (!findInnerTransitiveGuaranteedUses(lbi, foundUses)) { + result = meet(result, AddressUseKind::PointerEscape); } } continue; @@ -1063,7 +1070,7 @@ swift::findTransitiveUsesForAddress(SILValue projectedAddress, if (onError) { (*onError)(op); } - result = AddressUseKind::Unknown; + result = meet(result, AddressUseKind::Unknown); } return result; } @@ -1080,13 +1087,21 @@ bool AddressOwnership::areUsesWithinLifetime( SILValue root = base.getOwnershipReferenceRoot(); BorrowedValue borrow(root); if (borrow) - return borrow.areUsesWithinTransitiveScope(uses, &deadEndBlocks); + return borrow.areUsesWithinExtendedScope(uses, &deadEndBlocks); - // --- A reference no borrow scope. Currently happens for project_box. + // --- A reference with no borrow scope! Currently happens for project_box. // Compute the reference value's liveness. - PrunedLiveness liveness; - liveness.computeSSALiveness(root); + SSAPrunedLiveness liveness; + liveness.initializeDef(root); + SimpleLiveRangeSummary summary = liveness.computeSimple(); + // Conservatively ignore InnerBorrowKind::Reborrowed and + // AddressUseKind::PointerEscape and Reborrowed. The resulting liveness at + // least covers the known uses. + (void)summary; + + // FIXME (implicit borrow): handle reborrows transitively just like above so + // we don't bail out if a uses is within the reborrowed scope. return liveness.areUsesWithinBoundary(uses, &deadEndBlocks); } diff --git a/lib/SIL/Utils/PrunedLiveness.cpp b/lib/SIL/Utils/PrunedLiveness.cpp index 56ff201600f0a..a43a5cba5fc3c 100644 --- a/lib/SIL/Utils/PrunedLiveness.cpp +++ b/lib/SIL/Utils/PrunedLiveness.cpp @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See https://swift.org/LICENSE.txt for license information @@ -16,6 +16,7 @@ #include "swift/SIL/BasicBlockDatastructures.h" #include "swift/SIL/BasicBlockUtils.h" #include "swift/SIL/OwnershipUtils.h" +#include "swift/SIL/ScopedAddressUtils.h" #include "swift/SIL/SILInstruction.h" using namespace swift; @@ -92,19 +93,9 @@ void PrunedLiveBlocks::updateForUse( //===----------------------------------------------------------------------===// void PrunedLiveness::updateForUse(SILInstruction *user, bool lifetimeEnding) { - auto useBlockLive = liveBlocks.updateForUse(user, 0); - // Record all uses of blocks on the liveness boundary. For blocks marked - // LiveWithin, the boundary is considered to be the last use in the block. - // - // FIXME: Why is nonLifetimeEndingUsesInLiveOut inside PrunedLiveness, and - // what does it mean? Blocks may transition to LiveOut later. Or they may - // already be LiveOut from a previous use. After computing liveness, clients - // should check uses that are in PrunedLivenessBoundary. - if (!lifetimeEnding && useBlockLive == PrunedLiveBlocks::LiveOut) { - if (nonLifetimeEndingUsesInLiveOut) - nonLifetimeEndingUsesInLiveOut->insert(user); - return; - } + assert(!empty() && "at least one definition must be initialized"); + + liveBlocks.updateForUse(user, 0); // Note that a user may use the current value from multiple operands. If any // of the uses are non-lifetime-ending, then we must consider the user // itself non-lifetime-ending; it cannot be a final destroy point because @@ -121,8 +112,8 @@ void PrunedLiveness::updateForUse(SILInstruction *user, bool lifetimeEnding) { iterAndSuccess.first->second &= lifetimeEnding; } -bool PrunedLiveness::updateForBorrowingOperand(Operand *op) { - assert(op->getOperandOwnership() == OperandOwnership::Borrow); +InnerBorrowKind PrunedLiveness::updateForBorrowingOperand(Operand *operand) { + assert(operand->getOperandOwnership() == OperandOwnership::Borrow); // A nested borrow scope is considered a use-point at each scope ending // instruction. @@ -130,162 +121,112 @@ bool PrunedLiveness::updateForBorrowingOperand(Operand *op) { // TODO: Handle reborrowed copies by considering the extended borrow // scope. Temporarily bail-out on reborrows because we can't handle uses // that aren't dominated by currentDef. - if (!BorrowingOperand(op).visitScopeEndingUses([this](Operand *end) { + if (!BorrowingOperand(operand).visitScopeEndingUses([this](Operand *end) { if (end->getOperandOwnership() == OperandOwnership::Reborrow) { return false; } updateForUse(end->getUser(), /*lifetimeEnding*/ false); return true; })) { - return false; - } - return true; -} - -void PrunedLiveness::extendAcrossLiveness(PrunedLiveness &otherLivesness) { - // update this liveness for all the interesting users in otherLiveness. - for (std::pair userAndEnd : otherLivesness.users) { - updateForUse(userAndEnd.first, userAndEnd.second); + return InnerBorrowKind::Reborrowed; } + return InnerBorrowKind::Contained; } -bool PrunedLiveness::isWithinBoundaryHelper(SILInstruction *inst, - SILValue def) const { - SILBasicBlock *block = inst->getParent(); +AddressUseKind PrunedLiveness::checkAndUpdateInteriorPointer(Operand *operand) { + assert(operand->getOperandOwnership() == OperandOwnership::InteriorPointer); - /// Returns true if \p inst is before \p def in this block. - auto foundInBlockBeforeDef = [](SILInstruction *inst, SILBasicBlock *block, - SILValue def) { - if (!def || def->getParentBlock() != block) { - return false; - } - auto *defInst = def->getDefiningInstruction(); - if (!defInst) { - return false; - } - // Check if instruction is before the definition - for (SILInstruction &it : - make_range(block->begin(), defInst->getIterator())) { - if (&it == inst) { - return true; - } - } - return false; - }; - - switch (getBlockLiveness(block)) { - case PrunedLiveBlocks::Dead: - return false; - case PrunedLiveBlocks::LiveOut: - return !foundInBlockBeforeDef(inst, block, def); - case PrunedLiveBlocks::LiveWithin: - if (foundInBlockBeforeDef(inst, block, def)) { - return false; - } - // The boundary is within this block. This instruction is before the - // boundary iff any interesting uses occur after it. - for (SILInstruction &it : - make_range(std::next(inst->getIterator()), block->end())) { - switch (isInterestingUser(&it)) { - case PrunedLiveness::NonUser: - break; - case PrunedLiveness::NonLifetimeEndingUse: - case PrunedLiveness::LifetimeEndingUse: + if (auto *svi = dyn_cast(operand->getUser())) { + if (auto scopedAddress = ScopedAddressValue(svi)) { + scopedAddress.visitScopeEndingUses([this](Operand *end) { + updateForUse(end->getUser(), /*lifetimeEnding*/ false); return true; - } + }); + return AddressUseKind::NonEscaping; } - return false; } + // FIXME: findTransitiveUses should be a visitor so we're not recursively + // allocating use vectors and potentially merging the use points. + SmallVector uses; + auto useKind = InteriorPointerOperand(operand).findTransitiveUses(&uses); + for (auto *use : uses) { + updateForUse(use->getUser(), /*lifetimeEnding*/ false); + } + return useKind; } -bool PrunedLiveness::isWithinBoundary(SILInstruction *inst) const { - return isWithinBoundaryHelper(inst, /*def*/ SILValue()); -} - -bool PrunedLiveness::isWithinBoundaryOfDef(SILInstruction *inst, - SILValue def) const { - return isWithinBoundaryHelper(inst, def); +void PrunedLiveness::extendAcrossLiveness(PrunedLiveness &otherLivesness) { + // update this liveness for all the interesting users in otherLiveness. + for (std::pair userAndEnd : otherLivesness.users) { + updateForUse(userAndEnd.first, userAndEnd.second); + } } -bool PrunedLiveness::areUsesWithinBoundaryHelper( - ArrayRef uses, SILValue def, - DeadEndBlocks *deadEndBlocks) const { - auto checkDeadEnd = [deadEndBlocks](SILInstruction *inst) { - return deadEndBlocks && deadEndBlocks->isDeadEnd(inst->getParent()); - }; - - for (auto *use : uses) { - auto *user = use->getUser(); - if (!isWithinBoundaryHelper(user, def) && !checkDeadEnd(user)) - return false; +llvm::StringRef PrunedLiveBlocks::getStringRef(IsLive isLive) const { + switch (isLive) { + case Dead: + return "Dead"; + case LiveWithin: + return "LiveWithin"; + case LiveOut: + return "LiveOut"; } - return true; } -bool PrunedLiveness::areUsesWithinBoundary(ArrayRef uses, - DeadEndBlocks *deadEndBlocks) const { - return areUsesWithinBoundaryHelper(uses, SILValue(), deadEndBlocks); +void PrunedLiveBlocks::print(llvm::raw_ostream &OS) const { + if (!discoveredBlocks) { + OS << "No deterministic live block list\n"; + } + for (auto *block : *discoveredBlocks) { + block->printAsOperand(OS); + OS + << ": " << getStringRef(this->getBlockLiveness(block, 0)) << "\n"; + } } -bool PrunedLiveness::areUsesWithinBoundaryOfDef( - ArrayRef uses, SILValue def, - DeadEndBlocks *deadEndBlocks) const { - return areUsesWithinBoundaryHelper(uses, def, deadEndBlocks); +void PrunedLiveBlocks::dump() const { + print(llvm::dbgs()); } -bool PrunedLiveness::areUsesOutsideBoundaryHelper( - ArrayRef uses, SILValue def, - DeadEndBlocks *deadEndBlocks) const { - auto checkDeadEnd = [deadEndBlocks](SILInstruction *inst) { - return deadEndBlocks && deadEndBlocks->isDeadEnd(inst->getParent()); - }; - - for (auto *use : uses) { - auto *user = use->getUser(); - if (isWithinBoundaryHelper(user, def) || checkDeadEnd(user)) - return false; +void PrunedLiveness::print(llvm::raw_ostream &OS) const { + liveBlocks.print(OS); + for (auto &userAndIsLifetimeEnding : users) { + if (userAndIsLifetimeEnding.second) + OS << "lifetime-ending user: "; + else + OS << "regular user: "; + userAndIsLifetimeEnding.first->print(OS); } - return true; } -bool PrunedLiveness::areUsesOutsideBoundary( - ArrayRef uses, DeadEndBlocks *deadEndBlocks) const { - return areUsesOutsideBoundaryHelper(uses, SILValue(), deadEndBlocks); +void PrunedLiveness::dump() const { + print(llvm::dbgs()); } -bool PrunedLiveness::areUsesOutsideBoundaryOfDef( - ArrayRef uses, SILValue def, - DeadEndBlocks *deadEndBlocks) const { - return areUsesOutsideBoundaryHelper(uses, def, deadEndBlocks); -} +//===----------------------------------------------------------------------===// +// PrunedLivenessBoundary +//===----------------------------------------------------------------------===// -// An SSA def meets all the criteria for pruned liveness--def dominates all -// uses with no holes in the liverange. The lifetime-ending uses are also -// recorded--destroy_value or end_borrow. However destroy_values may not -// jointly-post dominate if dead-end blocks are present. -// -// Note: Uses with OperandOwnership::NonUse cannot be considered normal uses for -// liveness. Otherwise, liveness would need to separately track non-uses -// everywhere. Non-uses cannot be treated like normal non-lifetime-ending uses -// because they can occur on both applies, which need to extend liveness to the -// return point, and on forwarding instructions, like init_existential_ref, -// which need to consume their use even when type-dependent operands exist. -void PrunedLiveness::computeSSALiveness(SILValue def) { - initializeDefBlock(def->getParentBlock()); - for (Operand *use : def->getUses()) { - switch (use->getOperandOwnership()) { - default: - updateForUse(use->getUser(), use->isLifetimeEnding()); - break; - case OperandOwnership::NonUse: - break; - case OperandOwnership::Borrow: - updateForBorrowingOperand(use); - break; +void PrunedLivenessBoundary::print(llvm::raw_ostream &OS) const { + for (auto *user : lastUsers) { + OS << "last user: " << *user; + } + for (auto *block : boundaryEdges) { + OS << "boundary edge: "; + block->printAsOperand(OS); + OS << "\n"; + } + if (!deadDefs.empty()) { + for (auto *deadDef : deadDefs) { + OS << "dead def: " << *deadDef; } } } +void PrunedLivenessBoundary::dump() const { + print(llvm::dbgs()); +} + void PrunedLivenessBoundary::visitInsertionPoints( llvm::function_ref visitor, DeadEndBlocks *deBlocks) { @@ -309,40 +250,141 @@ void PrunedLivenessBoundary::visitInsertionPoints( visitor(edge->begin()); } + for (SILNode *deadDef : deadDefs) { + if (auto *arg = dyn_cast(deadDef)) + visitor(arg->getParent()->begin()); + else + visitor(std::next(cast(deadDef)->getIterator())); + } } -// Use \p liveness to find the last use in \p bb and add it to \p -// boundary.lastUsers. -static void findLastUserInBlock(SILBasicBlock *bb, - PrunedLivenessBoundary &boundary, - const PrunedLiveness &liveness) { - for (auto instIter = bb->rbegin(), endIter = bb->rend(); instIter != endIter; - ++instIter) { - auto *inst = &*instIter; - if (liveness.isInterestingUser(inst) == PrunedLiveness::NonUser) - continue; +//===----------------------------------------------------------------------===// +// PrunedLiveRange +//===----------------------------------------------------------------------===// - boundary.lastUsers.push_back(inst); - return; +template +SimpleLiveRangeSummary PrunedLiveRange::updateForDef(SILValue def) { + SimpleLiveRangeSummary summary; + // Note: Uses with OperandOwnership::NonUse cannot be considered normal uses + // for liveness. Otherwise, liveness would need to separately track non-uses + // everywhere. Non-uses cannot be treated like normal non-lifetime-ending uses + // because they can occur on both applies, which need to extend liveness to + // the return point, and on forwarding instructions, like + // init_existential_ref, which need to consume their use even when + // type-dependent operands exist. + for (Operand *use : def->getUses()) { + switch (use->getOperandOwnership()) { + case OperandOwnership::NonUse: + break; + case OperandOwnership::Borrow: + summary.meet(updateForBorrowingOperand(use)); + break; + case OperandOwnership::PointerEscape: + summary.meet(AddressUseKind::PointerEscape); + break; + case OperandOwnership::InteriorPointer: + summary.meet(checkAndUpdateInteriorPointer(use)); + break; + case OperandOwnership::ForwardingBorrow: { + ForwardingOperand(use).visitForwardedValues([&](SILValue result) { + // Do not include transitive uses with 'none' ownership + if (result->getOwnershipKind() != OwnershipKind::None) { + updateForDef(result); + } + return true; + }); + break; + } + default: + updateForUse(use->getUser(), use->isLifetimeEnding()); + break; + } } - llvm_unreachable("No user in LiveWithin block"); + return summary; } -void PrunedLivenessBoundary::compute(const PrunedLiveness &liveness) { - for (SILBasicBlock *bb : liveness.getDiscoveredBlocks()) { +template +bool PrunedLiveRange::isWithinBoundary( + SILInstruction *inst) const { + assert(asImpl().isInitialized()); + + auto *block = inst->getParent(); + auto blockLiveness = getBlockLiveness(block); + if (blockLiveness == PrunedLiveBlocks::Dead) + return false; + + bool isLive = blockLiveness == PrunedLiveBlocks::LiveOut; + + if (isLive && !asImpl().isDefBlock(block)) + return true; + + // Check if instruction is between a last use and a definition + for (SILInstruction &it : llvm::reverse(*block)) { + // the def itself is not within the boundary, so cancel liveness before + // matching 'inst'. + if (asImpl().isDef(&it)) { + isLive = false; + } + if (&it == inst) { + return isLive; + } + if (!isLive && isInterestingUser(&it)) { + isLive = true; + } + } + llvm_unreachable("instruction must be in its parent block"); +} + +template +bool PrunedLiveRange::areUsesWithinBoundary( + ArrayRef uses, DeadEndBlocks *deadEndBlocks) const { + assert(asImpl().isInitialized()); + + auto checkDeadEnd = [deadEndBlocks](SILInstruction *inst) { + return deadEndBlocks && deadEndBlocks->isDeadEnd(inst->getParent()); + }; + for (auto *use : uses) { + auto *user = use->getUser(); + if (!asImpl().isWithinBoundary(user) && !checkDeadEnd(user)) + return false; + } + return true; +} + +template +bool PrunedLiveRange::areUsesOutsideBoundary( + ArrayRef uses, DeadEndBlocks *deadEndBlocks) const { + assert(asImpl().isInitialized()); + + auto checkDeadEnd = [deadEndBlocks](SILInstruction *inst) { + return deadEndBlocks && deadEndBlocks->isDeadEnd(inst->getParent()); + }; + for (auto *use : uses) { + auto *user = use->getUser(); + if (asImpl().isWithinBoundary(user) || checkDeadEnd(user)) + return false; + } + return true; +} + +template +void PrunedLiveRange::computeBoundary( + PrunedLivenessBoundary &boundary) const { + assert(asImpl().isInitialized()); + + for (SILBasicBlock *block : getDiscoveredBlocks()) { // Process each block that has not been visited and is not LiveOut. - switch (liveness.getBlockLiveness(bb)) { + switch (getBlockLiveness(block)) { case PrunedLiveBlocks::LiveOut: - for (SILBasicBlock *succBB : bb->getSuccessors()) { - if (liveness.getBlockLiveness(succBB) == PrunedLiveBlocks::Dead) { - boundaryEdges.push_back(succBB); + for (SILBasicBlock *succBB : block->getSuccessors()) { + if (getBlockLiveness(succBB) == PrunedLiveBlocks::Dead) { + boundary.boundaryEdges.push_back(succBB); } } + asImpl().findBoundariesInBlock(block, /*isLiveOut*/ true, boundary); break; case PrunedLiveBlocks::LiveWithin: { - // The liveness boundary is inside this block. Insert a final destroy - // inside the block if it doesn't already have one. - findLastUserInBlock(bb, *this, liveness); + asImpl().findBoundariesInBlock(block, /*isLiveOut*/ false, boundary); break; } case PrunedLiveBlocks::Dead: @@ -351,8 +393,12 @@ void PrunedLivenessBoundary::compute(const PrunedLiveness &liveness) { } } -void PrunedLivenessBoundary::compute(const PrunedLiveness &liveness, - ArrayRef postDomBlocks) { +template +void PrunedLiveRange::computeBoundary( + PrunedLivenessBoundary &boundary, + ArrayRef postDomBlocks) const { + assert(asImpl().isInitialized()); + if (postDomBlocks.empty()) return; // all paths must be dead-ends or infinite loops @@ -360,27 +406,24 @@ void PrunedLivenessBoundary::compute(const PrunedLiveness &liveness, // Visit each post-dominating block as the starting point for a // backward CFG traversal. - for (auto *bb : postDomBlocks) { - blockWorklist.push(bb); + for (auto *block : postDomBlocks) { + blockWorklist.push(block); } - while (auto *bb = blockWorklist.pop()) { + while (auto *block = blockWorklist.pop()) { // Process each block that has not been visited and is not LiveOut. - switch (liveness.getBlockLiveness(bb)) { + switch (getBlockLiveness(block)) { case PrunedLiveBlocks::LiveOut: - // A lifetimeEndBlock may be determined to be LiveOut after analyzing - // the extended liveness. It is irrelevant for finding the boundary. + asImpl().findBoundariesInBlock(block, /*isLiveOut*/ true, boundary); break; case PrunedLiveBlocks::LiveWithin: { - // The liveness boundary is inside this block. Insert a final destroy - // inside the block if it doesn't already have one. - findLastUserInBlock(bb, *this, liveness); + asImpl().findBoundariesInBlock(block, /*isLiveOut*/ false, boundary); break; } case PrunedLiveBlocks::Dead: // Continue searching upward to find the pruned liveness boundary. - for (auto *predBB : bb->getPredecessorBlocks()) { - if (liveness.getBlockLiveness(predBB) == PrunedLiveBlocks::LiveOut) { - boundaryEdges.push_back(bb); + for (auto *predBB : block->getPredecessorBlocks()) { + if (getBlockLiveness(predBB) == PrunedLiveBlocks::LiveOut) { + boundary.boundaryEdges.push_back(block); } else { blockWorklist.pushIfNotVisited(predBB); } @@ -390,6 +433,116 @@ void PrunedLivenessBoundary::compute(const PrunedLiveness &liveness, } } +namespace swift { +template class PrunedLiveRange; +template class PrunedLiveRange; +} // namespace swift + +//===----------------------------------------------------------------------===// +// SSAPrunedLiveness +//===----------------------------------------------------------------------===// + +void SSAPrunedLiveness::findBoundariesInBlock( + SILBasicBlock *block, bool isLiveOut, + PrunedLivenessBoundary &boundary) const { + assert(isInitialized()); + + // For SSA, a live-out block cannot have a boundary. + if (isLiveOut) + return; + + bool isDefBlockState = isDefBlock(block); + for (SILInstruction &inst : llvm::reverse(*block)) { + if (isDefBlockState && isDef(&inst)) { + boundary.deadDefs.push_back(cast(&inst)); + return; + } + if (isInterestingUser(&inst)) { + boundary.lastUsers.push_back(&inst); + return; + } + } + auto *deadArg = dyn_cast(def); + assert(deadArg && deadArg->getParent() == block + && "findBoundariesInBlock must be called on a live block"); + boundary.deadDefs.push_back(deadArg); +} + +//===----------------------------------------------------------------------===// +// MultiDefPrunedLiveness +//===----------------------------------------------------------------------===// + +void MultiDefPrunedLiveness::findBoundariesInBlock( + SILBasicBlock *block, bool isLiveOut, + PrunedLivenessBoundary &boundary) const { + assert(isInitialized()); + unsigned prevCount = boundary.deadDefs.size() + boundary.lastUsers.size(); + + bool isLive = isLiveOut; + bool isDefBlockState = isDefBlock(block); + for (auto &inst : llvm::reverse(*block)) { + // Check if the instruction is a def before checking whether it is a + // use. The same instruction can be both a dead def and boundary use. + if (isDefBlockState && isDef(&inst)) { + if (!isLive) { + boundary.deadDefs.push_back(cast(&inst)); + } + isLive = false; + } + if (!isLive && isInterestingUser(&inst)) { + boundary.lastUsers.push_back(&inst); + isLive = true; + } + } + if (!isLive && isDefBlockState) { + for (SILArgument *deadArg : block->getArguments()) { + if (defs.contains(deadArg)) { + boundary.deadDefs.push_back(deadArg); + } + } + } + assert(prevCount < boundary.deadDefs.size() + boundary.lastUsers.size() + && "findBoundariesInBlock must be called on a live block"); +} + +SimpleLiveRangeSummary MultiDefPrunedLiveness::computeSimple() { + assert(isInitialized() && "defs uninitialized"); + + SimpleLiveRangeSummary summary; + for (SILNode *defNode : defs) { + if (auto *arg = dyn_cast(defNode)) + summary.meet(updateForDef(arg)); + else { + for (auto result : cast(defNode)->getResults()) { + summary.meet(updateForDef(result)); + } + } + } + return summary; +} + +//===----------------------------------------------------------------------===// +// DiagnosticPrunedLiveness +//===----------------------------------------------------------------------===// + +// FIXME: This is wrong. Why is nonLifetimeEndingUsesInLiveOut inside +// PrunedLiveness, and what does it mean? Blocks may transition to LiveOut +// later. Or they may already be LiveOut from a previous use. After computing +// liveness, clients should check uses that are in PrunedLivenessBoundary. +void DiagnosticPrunedLiveness:: +updateForUse(SILInstruction *user, bool lifetimeEnding) { + PrunedLiveness::updateForUse(user, 0); + + auto useBlockLive = getBlockLiveness(user->getParent()); + // Record all uses of blocks on the liveness boundary. For blocks marked + // LiveWithin, the boundary is considered to be the last use in the block. + if (!lifetimeEnding && useBlockLive == PrunedLiveBlocks::LiveOut) { + if (nonLifetimeEndingUsesInLiveOut) + nonLifetimeEndingUsesInLiveOut->insert(user); + return; + } +} + //===----------------------------------------------------------------------===// // Field Sensitive PrunedLiveness //===----------------------------------------------------------------------===// diff --git a/lib/SIL/Utils/ScopedAddressUtils.cpp b/lib/SIL/Utils/ScopedAddressUtils.cpp index 132668b831b2f..05cf06f160908 100644 --- a/lib/SIL/Utils/ScopedAddressUtils.cpp +++ b/lib/SIL/Utils/ScopedAddressUtils.cpp @@ -86,16 +86,34 @@ bool ScopedAddressValue::visitScopeEndingUses( } } -bool ScopedAddressValue::computeLiveness(PrunedLiveness &liveness) const { +// Note: This is used to fixup an incomplete address scope, so cannot assume the +// scope's lifetime is already complete. Therefore, it needs to transitively +// process all address uses. +// +// FIXME: users of this should use the standard recursive lifetime completion +// utility. Otherwise dealing with nested incomplete lifetimes becomes +// expensive and complex. e.g. +// +// %storeBorrow = store_borrow %_ to %adr +// %loadBorrow = load_borrow %storeBorrow +// apply %f(%loadBorrow) : $@convention(thin) (...) -> Never +// unreachable +// +AddressUseKind ScopedAddressValue::computeTransitiveLiveness( + SSAPrunedLiveness &liveness) const { + liveness.initializeDef(value); + return updateTransitiveLiveness(liveness); +} + +AddressUseKind +ScopedAddressValue::updateTransitiveLiveness(PrunedLiveness &liveness) const { SmallVector uses; // Collect all uses that need to be enclosed by the scope. auto addressKind = findTransitiveUsesForAddress(value, &uses); - if (addressKind != AddressUseKind::NonEscaping) { - return false; - } - - liveness.initializeDefBlock(value->getParentBlock()); for (auto *use : uses) { + if (isScopeEndingUse(use)) + continue; + // Update all collected uses as non-lifetime ending. liveness.updateForUse(use->getUser(), /* lifetimeEnding */ false); } @@ -103,7 +121,7 @@ bool ScopedAddressValue::computeLiveness(PrunedLiveness &liveness) const { liveness.updateForUse(endOp->getUser(), /* isLifetimeEnding */ true); return true; }); - return true; + return addressKind; } void ScopedAddressValue::createScopeEnd(SILBasicBlock::iterator insertPt, @@ -123,7 +141,7 @@ void ScopedAddressValue::createScopeEnd(SILBasicBlock::iterator insertPt, } void ScopedAddressValue::endScopeAtLivenessBoundary( - PrunedLiveness *liveness) const { + SSAPrunedLiveness *liveness) const { // If no users exist, create scope ending instruction immediately after the // scoped address value. if (liveness->empty()) { @@ -133,7 +151,7 @@ void ScopedAddressValue::endScopeAtLivenessBoundary( } PrunedLivenessBoundary scopedAddressBoundary; - scopedAddressBoundary.compute(*liveness); + liveness->computeBoundary(scopedAddressBoundary); // Go over the boundary and create scope ending instructions. scopedAddressBoundary.visitInsertionPoints( [&](SILBasicBlock::iterator insertPt) { @@ -142,7 +160,7 @@ void ScopedAddressValue::endScopeAtLivenessBoundary( } bool swift::hasOtherStoreBorrowsInLifetime(StoreBorrowInst *storeBorrow, - PrunedLiveness *liveness, + SSAPrunedLiveness *liveness, DeadEndBlocks *deadEndBlocks) { SmallVector otherStoreBorrows; // Collect all other store_borrows to the destination of \p storeBorrow @@ -157,7 +175,7 @@ bool swift::hasOtherStoreBorrowsInLifetime(StoreBorrowInst *storeBorrow, for (auto *otherStoreBorrow : otherStoreBorrows) { // Return true, if otherStoreBorrow was in \p storeBorrow's scope - if (liveness->isWithinBoundaryOfDef(otherStoreBorrow, storeBorrow)) { + if (liveness->isWithinBoundary(otherStoreBorrow)) { return true; } } @@ -171,15 +189,19 @@ bool swift::extendStoreBorrow(StoreBorrowInst *sbi, ScopedAddressValue scopedAddress(sbi); SmallVector discoveredBlocks; - PrunedLiveness storeBorrowLiveness(&discoveredBlocks); - bool success = scopedAddress.computeLiveness(storeBorrowLiveness); + SSAPrunedLiveness storeBorrowLiveness(&discoveredBlocks); + + // FIXME: if OSSA lifetimes are complete, then we don't need transitive + // liveness here. + AddressUseKind useKind = + scopedAddress.computeTransitiveLiveness(storeBorrowLiveness); // If all new uses are within store_borrow boundary, no need for extension. if (storeBorrowLiveness.areUsesWithinBoundary(newUses, deadEndBlocks)) { return true; } - if (!success) { + if (useKind != AddressUseKind::NonEscaping) { return false; } @@ -193,7 +215,8 @@ bool swift::extendStoreBorrow(StoreBorrowInst *sbi, InstModCallbacks tempCallbacks = callbacks; InstructionDeleter deleter(std::move(tempCallbacks)); - GuaranteedOwnershipExtension borrowExtension(deleter, *deadEndBlocks); + GuaranteedOwnershipExtension borrowExtension(deleter, *deadEndBlocks, + sbi->getFunction()); auto status = borrowExtension.checkBorrowExtension( BorrowedValue(sbi->getSrc()), newUses); if (status == GuaranteedOwnershipExtension::Invalid) { diff --git a/lib/SIL/Verifier/LoadBorrowImmutabilityChecker.cpp b/lib/SIL/Verifier/LoadBorrowImmutabilityChecker.cpp index 9680d3f55088a..97a59be2203de 100644 --- a/lib/SIL/Verifier/LoadBorrowImmutabilityChecker.cpp +++ b/lib/SIL/Verifier/LoadBorrowImmutabilityChecker.cpp @@ -329,8 +329,8 @@ bool LoadBorrowImmutabilityAnalysis::isImmutableInScope( : SILValue(); BorrowedValue borrowedValue(lbi); - PrunedLiveness borrowLiveness; - borrowedValue.computeLiveness(borrowLiveness); + MultiDefPrunedLiveness borrowLiveness(lbi->getFunction()); + borrowedValue.computeTransitiveLiveness(borrowLiveness); // Then for each write... for (auto *op : *writes) { @@ -346,7 +346,7 @@ bool LoadBorrowImmutabilityAnalysis::isImmutableInScope( continue; } - if (borrowLiveness.isWithinBoundaryOfDef(write, lbi)) { + if (borrowLiveness.isWithinBoundary(write)) { llvm::errs() << "Write: " << *write; return false; } diff --git a/lib/SIL/Verifier/SILVerifier.cpp b/lib/SIL/Verifier/SILVerifier.cpp index 2fcbfd64c7815..e07687d29eedb 100644 --- a/lib/SIL/Verifier/SILVerifier.cpp +++ b/lib/SIL/Verifier/SILVerifier.cpp @@ -2260,7 +2260,7 @@ class SILVerifier : public SILVerifierBase { } bool checkScopedAddressUses(ScopedAddressValue scopedAddress, - PrunedLiveness *scopedAddressLiveness, + SSAPrunedLiveness *scopedAddressLiveness, DeadEndBlocks *deadEndBlocks) { SmallVector uses; findTransitiveUsesForAddress(scopedAddress.value, &uses); @@ -2444,9 +2444,22 @@ class SILVerifier : public SILVerifierBase { require(isa(SI->getDest()), "store_borrow destination can only be an alloc_stack"); - PrunedLiveness scopedAddressLiveness; + SSAPrunedLiveness scopedAddressLiveness; ScopedAddressValue scopedAddress(SI); - bool success = scopedAddress.computeLiveness(scopedAddressLiveness); + // FIXME: Reenable @test_load_borrow_store_borrow_nested in + // store_borrow_verify_errors once computeLivess can successfully handle a + // store_borrow within a load_borrow. This can be fixed in two ways + // + // (1) With complete lifetimes, this no longer needs to perform transitive + // liveness at all. + // + // (2) findInnerTransitiveGuaranteedUses, which ends up being called on the + // load_borrow to compute liveness, can be taught to transitively process + // InteriorPointer uses instead of returning PointerEscape. We need to make + // sure all uses of the utility need to handle this first. + AddressUseKind useKind = + scopedAddress.computeTransitiveLiveness(scopedAddressLiveness); + bool success = useKind == AddressUseKind::NonEscaping; require(!success || checkScopedAddressUses( scopedAddress, &scopedAddressLiveness, &DEBlocks), diff --git a/lib/SILOptimizer/Mandatory/AddressLowering.cpp b/lib/SILOptimizer/Mandatory/AddressLowering.cpp index 457e81d1bf104..d85b202c970eb 100644 --- a/lib/SILOptimizer/Mandatory/AddressLowering.cpp +++ b/lib/SILOptimizer/Mandatory/AddressLowering.cpp @@ -2857,13 +2857,14 @@ void UseRewriter::emitEndBorrows(SILValue value) { findInnerTransitiveGuaranteedUses(value, &usePoints); SmallVector discoveredBlocks; - PrunedLiveness liveness(&discoveredBlocks); + SSAPrunedLiveness liveness(&discoveredBlocks); + liveness.initializeDef(value); for (auto *use : usePoints) { assert(!use->isLifetimeEnding()); liveness.updateForUse(use->getUser(), /*lifetimeEnding*/ false); } PrunedLivenessBoundary guaranteedBoundary; - guaranteedBoundary.compute(liveness); + liveness.computeBoundary(guaranteedBoundary); guaranteedBoundary.visitInsertionPoints( [&](SILBasicBlock::iterator insertPt) { pass.getBuilder(insertPt).createEndBorrow(pass.genLoc(), value); diff --git a/lib/SILOptimizer/Mandatory/DiagnoseLifetimeIssues.cpp b/lib/SILOptimizer/Mandatory/DiagnoseLifetimeIssues.cpp index 8e9b2cea6574c..7afb598e519bd 100644 --- a/lib/SILOptimizer/Mandatory/DiagnoseLifetimeIssues.cpp +++ b/lib/SILOptimizer/Mandatory/DiagnoseLifetimeIssues.cpp @@ -64,7 +64,7 @@ class DiagnoseLifetimeIssues { static constexpr int maxCallDepth = 8; /// The liveness of the object in question, computed in visitUses. - PrunedLiveness liveness; + SSAPrunedLiveness liveness; /// All weak stores of the object, which are found in visitUses. llvm::SmallVector weakStores; @@ -238,9 +238,11 @@ visitUses(SILValue def, bool updateLivenessAndWeakStores, int callDepth) { return CanEscape; break; case OperandOwnership::Borrow: { - if (updateLivenessAndWeakStores && - !liveness.updateForBorrowingOperand(use)) + if (updateLivenessAndWeakStores + && (liveness.updateForBorrowingOperand(use) + != InnerBorrowKind::Contained)) { return CanEscape; + } BorrowingOperand borrowOper(use); if (borrowOper.hasBorrowIntroducingUser()) { if (auto *beginBorrow = dyn_cast(user)) @@ -305,7 +307,7 @@ getArgumentState(ApplySite ai, Operand *applyOperand, int callDepth) { } /// Returns true if \p inst is outside the pruned \p liveness. -static bool isOutOfLifetime(SILInstruction *inst, PrunedLiveness &liveness) { +static bool isOutOfLifetime(SILInstruction *inst, SSAPrunedLiveness &liveness) { // Check if the lifetime of the stored object ends at the store_weak. // // A more sophisticated analysis would be to check if there are no @@ -325,7 +327,7 @@ void DiagnoseLifetimeIssues::reportDeadStore(SILInstruction *allocationInst) { weakStores.clear(); SILValue storedDef = cast(allocationInst); - liveness.initializeDefBlock(storedDef->getParentBlock()); + liveness.initializeDef(storedDef); // Compute the canonical lifetime of storedDef, like the copy-propagation pass // would do. diff --git a/lib/SILOptimizer/Mandatory/MoveKillsCopyableAddressesChecker.cpp b/lib/SILOptimizer/Mandatory/MoveKillsCopyableAddressesChecker.cpp index c9ad9b02fa110..1700c8fa9c9cd 100644 --- a/lib/SILOptimizer/Mandatory/MoveKillsCopyableAddressesChecker.cpp +++ b/lib/SILOptimizer/Mandatory/MoveKillsCopyableAddressesChecker.cpp @@ -575,20 +575,17 @@ namespace { struct ClosureArgDataflowState { SmallVector livenessWorklist; SmallVector consumingWorklist; - PrunedLiveness livenessForConsumes; + MultiDefPrunedLiveness livenessForConsumes; UseState &useState; public: - ClosureArgDataflowState(UseState &useState) : useState(useState) {} + ClosureArgDataflowState(SILFunction *function, UseState &useState) + : livenessForConsumes(function), useState(useState) {} bool process( SILArgument *arg, ClosureOperandState &state, SmallBlotSetVector &postDominatingConsumingUsers); - void clear() { - livenessForConsumes.clear(); - } - private: /// Perform our liveness dataflow. Returns true if we found any liveness uses /// at all. These we will need to error upon. @@ -752,6 +749,7 @@ void ClosureArgDataflowState::classifyUses(BasicBlockSet &initBlocks, for (auto *user : useState.inits) { if (upwardScanForInit(user, useState)) { LLVM_DEBUG(llvm::dbgs() << " Found init block at: " << *user); + livenessForConsumes.initializeDef(cast(user)); initBlocks.insert(user->getParent()); } } @@ -797,8 +795,6 @@ void ClosureArgDataflowState::classifyUses(BasicBlockSet &initBlocks, bool ClosureArgDataflowState::process( SILArgument *address, ClosureOperandState &state, SmallBlotSetVector &postDominatingConsumingUsers) { - clear(); - SILFunction *fn = address->getFunction(); assert(fn); @@ -865,9 +861,9 @@ bool ClosureArgDataflowState::process( return false; } - SWIFT_DEFER { livenessForConsumes.clear(); }; - auto *frontBlock = &*fn->begin(); - livenessForConsumes.initializeDefBlock(frontBlock); + //!!! FIXME: Why? + // auto *frontBlock = &*fn->begin(); + // livenessForConsumes.initializeDefBlock(frontBlock); for (unsigned i : indices(livenessWorklist)) { if (auto *ptr = livenessWorklist[i]) { @@ -1950,7 +1946,6 @@ struct MoveKillsCopyableAddressesChecker { UseState useState; DataflowState dataflowState; UseState closureUseState; - ClosureArgDataflowState closureUseDataflowState; SILOptFunctionBuilder &funcBuilder; llvm::SmallMapVector applySiteToPromotedArgIndices; @@ -1961,8 +1956,7 @@ struct MoveKillsCopyableAddressesChecker { : fn(fn), useState(), dataflowState(funcBuilder, useState, applySiteToPromotedArgIndices, closureConsumes), - closureUseState(), closureUseDataflowState(closureUseState), - funcBuilder(funcBuilder) {} + closureUseState(), funcBuilder(funcBuilder) {} void cloneDeferCalleeAndRewriteUses( SmallVectorImpl &temporaryStorage, @@ -2085,7 +2079,7 @@ bool MoveKillsCopyableAddressesChecker::performClosureDataflow( if (!visitAccessPathUses(visitor, accessPath, fn)) return false; - SWIFT_DEFER { closureUseDataflowState.clear(); }; + ClosureArgDataflowState closureUseDataflowState(fn, closureUseState); return closureUseDataflowState.process(address, calleeOperandState, closureConsumes); } diff --git a/lib/SILOptimizer/Mandatory/MoveKillsCopyableValuesChecker.cpp b/lib/SILOptimizer/Mandatory/MoveKillsCopyableValuesChecker.cpp index 8072ee6869eb4..0386ca990bbde 100644 --- a/lib/SILOptimizer/Mandatory/MoveKillsCopyableValuesChecker.cpp +++ b/lib/SILOptimizer/Mandatory/MoveKillsCopyableValuesChecker.cpp @@ -58,13 +58,16 @@ struct CheckerLivenessInfo { SmallSetVector consumingUse; SmallSetVector nonLifetimeEndingUsesInLiveOut; SmallVector interiorPointerTransitiveUses; - PrunedLiveness liveness; + DiagnosticPrunedLiveness liveness; CheckerLivenessInfo() : nonLifetimeEndingUsesInLiveOut(), liveness(nullptr, &nonLifetimeEndingUsesInLiveOut) {} - void initDef(SILValue def) { defUseWorklist.insert(def); } + void initDef(SILValue def) { + liveness.initializeDef(def); + defUseWorklist.insert(def); + } /// Compute the liveness for any value currently in the defUseWorklist. /// @@ -149,9 +152,10 @@ bool CheckerLivenessInfo::compute() { // Otherwise, try to update liveness for a borrowing operand // use. This will make it so that we add the end_borrows of the // liveness use. If we have a reborrow here, we will bail. - bool failed = !liveness.updateForBorrowingOperand(use); - if (failed) + if (liveness.updateForBorrowingOperand(use) + != InnerBorrowKind::Contained) { return false; + } } } break; diff --git a/lib/SILOptimizer/SemanticARC/CopyValueOpts.cpp b/lib/SILOptimizer/SemanticARC/CopyValueOpts.cpp index 6b95186702eb2..1686d2ce6005d 100644 --- a/lib/SILOptimizer/SemanticARC/CopyValueOpts.cpp +++ b/lib/SILOptimizer/SemanticARC/CopyValueOpts.cpp @@ -155,7 +155,7 @@ bool SemanticARCOptVisitor::performGuaranteedCopyValueOptimization( // TODO: There may be some way of sinking this into the loop below. // // FIXME: The haveAnyLocalScopes and destroy.empty() checks are relics of - // attempts to handle dead end blocks during areUsesWithinTransitiveScope. If + // attempts to handle dead end blocks during areUsesWithinExtendedScope. If // we don't use dead end blocks at all, they should not be relevant. bool haveAnyLocalScopes = llvm::any_of(borrowScopeIntroducers, [](BorrowedValue borrowScope) { @@ -182,13 +182,13 @@ bool SemanticARCOptVisitor::performGuaranteedCopyValueOptimization( // need to insert an end_borrow since all of our borrow introducers are // non-local scopes. // - // The call to areUsesWithinTransitiveScope cannot consider dead-end blocks. A + // The call to areUsesWithinExtendedScope cannot consider dead-end blocks. A // local borrow scope requires all its inner uses to be inside the borrow // scope, regardless of whether the end of the scope is inside a dead-end // block. { if (llvm::any_of(borrowScopeIntroducers, [&](BorrowedValue borrowScope) { - return !borrowScope.areUsesWithinTransitiveScope( + return !borrowScope.areUsesWithinExtendedScope( lr.getAllConsumingUses(), nullptr); })) { return false; @@ -217,7 +217,7 @@ bool SemanticARCOptVisitor::performGuaranteedCopyValueOptimization( } if (llvm::any_of(borrowScopeIntroducers, [&](BorrowedValue borrowScope) { - return !borrowScope.areUsesWithinTransitiveScope( + return !borrowScope.areUsesWithinExtendedScope( phiArgLR.getAllConsumingUses(), nullptr); })) { return false; diff --git a/lib/SILOptimizer/Transforms/AccessEnforcementOpts.cpp b/lib/SILOptimizer/Transforms/AccessEnforcementOpts.cpp index 084e3d6bd365d..d33be36e96e4e 100644 --- a/lib/SILOptimizer/Transforms/AccessEnforcementOpts.cpp +++ b/lib/SILOptimizer/Transforms/AccessEnforcementOpts.cpp @@ -1009,7 +1009,8 @@ static bool extendOwnership(BeginAccessInst *parentInst, BeginAccessInst *childInst, InstructionDeleter &deleter, DeadEndBlocks &deBlocks) { - GuaranteedOwnershipExtension extension(deleter, deBlocks); + GuaranteedOwnershipExtension extension(deleter, deBlocks, + parentInst->getFunction()); auto status = extension.checkAddressOwnership(parentInst, childInst); switch (status) { case GuaranteedOwnershipExtension::Invalid: diff --git a/lib/SILOptimizer/Transforms/SimplifyCFG.cpp b/lib/SILOptimizer/Transforms/SimplifyCFG.cpp index 0396779153896..c464de85c8998 100644 --- a/lib/SILOptimizer/Transforms/SimplifyCFG.cpp +++ b/lib/SILOptimizer/Transforms/SimplifyCFG.cpp @@ -9,6 +9,24 @@ // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// +/// +/// Note: Unreachable blocks must always be eliminated before simplifying +/// useless phis. Otherwise self-loops will result in invalid SIL: +/// +/// bb1(%phi): +/// apply %use(%phi) +/// %def = apply %getValue() +/// br bb1(%def) +/// +/// When bb1 is unreachable, %phi will be removed as useless: +/// bb1: +/// apply %use(%def) +/// %def = apply %getValue() +/// br bb1(%def) +/// +/// This is considered invalid SIL because SIL has a special SSA dominance rule +/// that does not allow a use above a def in the same block. +//===----------------------------------------------------------------------===// #define DEBUG_TYPE "sil-simplify-cfg" diff --git a/lib/SILOptimizer/UtilityPasses/CMakeLists.txt b/lib/SILOptimizer/UtilityPasses/CMakeLists.txt index f1c19514fdd94..f8e19c3d683a5 100644 --- a/lib/SILOptimizer/UtilityPasses/CMakeLists.txt +++ b/lib/SILOptimizer/UtilityPasses/CMakeLists.txt @@ -26,6 +26,7 @@ target_sources(swiftSILOptimizer PRIVATE LoopRegionPrinter.cpp MemBehaviorDumper.cpp ModulePrinter.cpp + OSSALifetimeAnalysis.cpp RCIdentityDumper.cpp SerializeSILPass.cpp SILDebugInfoGenerator.cpp diff --git a/lib/SILOptimizer/UtilityPasses/OSSALifetimeAnalysis.cpp b/lib/SILOptimizer/UtilityPasses/OSSALifetimeAnalysis.cpp new file mode 100644 index 0000000000000..66131dbdcf81c --- /dev/null +++ b/lib/SILOptimizer/UtilityPasses/OSSALifetimeAnalysis.cpp @@ -0,0 +1,139 @@ +//===--- OSSALifetimeAnalysis.cpp - OSSA lifetime analysis ----------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// +/// +/// This currently runs SSAPrunedLiveness on SSA values, MultiDefPrunedLiveness +/// on phi webs, and MultiDefPrunedLiveness on 'copy_addr [init]' and 'store +/// [init]'. +/// +/// Soon, it will also run OSSALiveness on OSSA values. +//===----------------------------------------------------------------------===// + +#define DEBUG_TYPE "ossa-lifetime-analysis" +#include "swift/SIL/PrunedLiveness.h" +#include "swift/SIL/SILArgument.h" +#include "swift/SIL/SILFunction.h" +#include "swift/SIL/SILInstruction.h" +#include "swift/SIL/SILValue.h" +#include "swift/SIL/ScopedAddressUtils.h" +#include "swift/SILOptimizer/PassManager/Passes.h" +#include "swift/SILOptimizer/PassManager/Transforms.h" +#include "swift/SILOptimizer/Utils/InstructionDeleter.h" +#include "llvm/Support/Debug.h" + +using namespace swift; + +namespace { + +struct OSSALifetimeAnalyzer { + SILFunction *function; + + OSSALifetimeAnalyzer(SILFunction *function): function(function) {} + + void analyzeSSAValue(SILValue value); + + void analyzeScopedAddress(ScopedAddressValue scopedAddress); + + void analyzeValues(ArrayRef values); +}; + +void OSSALifetimeAnalyzer::analyzeSSAValue(SILValue value) { + llvm::outs() << "\nSSA lifetime analysis: " << value; + SmallVector discoveredBlocks; + SSAPrunedLiveness liveness(&discoveredBlocks); + liveness.initializeDef(value); + liveness.computeSimple(); + liveness.print(llvm::outs()); + + PrunedLivenessBoundary boundary; + liveness.computeBoundary(boundary); + boundary.print(llvm::outs()); +} + +void OSSALifetimeAnalyzer::analyzeScopedAddress( + ScopedAddressValue scopedAddress) { + llvm::outs() << "\nScoped address analysis: " << scopedAddress.value; + SmallVector discoveredBlocks; + SSAPrunedLiveness liveness(&discoveredBlocks); + scopedAddress.computeTransitiveLiveness(liveness); + liveness.print(llvm::outs()); + + PrunedLivenessBoundary boundary; + liveness.computeBoundary(boundary); + boundary.print(llvm::outs()); +} + +void OSSALifetimeAnalyzer::analyzeValues(ArrayRef values) { + llvm::outs() << "\nMultiDef lifetime analysis:\n"; + SmallVector discoveredBlocks; + MultiDefPrunedLiveness liveness(function, &discoveredBlocks); + for (SILValue value : values) { + llvm::outs() << " def: " << value; + liveness.initializeDef(value); + } + liveness.computeSimple(); + liveness.print(llvm::outs()); + + PrunedLivenessBoundary boundary; + liveness.computeBoundary(boundary); + boundary.print(llvm::outs()); +} + +class OSSALifetimeAnalysis : public SILModuleTransform { + void run() override; +}; + +// Find and record all debug_value [trace] instructions. Strip them before +// querying liveness so we can test dead values. Alternatively, we could +// override SSAPrunedLiveness::compute above and simply ignore trace +// instructions. +// +// Consider each traced value first as an indivual live range, then consider +// them all together as a single live range. +void OSSALifetimeAnalysis::run() { + for (auto &function : *getModule()) { + llvm::outs() << "\n@" << function.getName() << "\n"; + if (function.empty()) { + llvm::outs() << "\n"; + continue; + } + SmallVector traceValues; + InstructionDeleter deleter; + for (auto &block : function) { + for (SILInstruction *inst : deleter.updatingRange(&block)) { + if (auto *debugValue = dyn_cast(inst)) { + traceValues.push_back(debugValue->getOperand()); + deleter.forceDelete(debugValue); + } + } + } + OSSALifetimeAnalyzer analyzer(&function); + for (auto value : traceValues) { + if (value->getType().isAddress()) { + ScopedAddressValue scopedAddress(value); + if (scopedAddress) { + analyzer.analyzeScopedAddress(scopedAddress); + continue; + } + } + analyzer.analyzeSSAValue(value); + } + if (traceValues.size() > 1) { + analyzer.analyzeValues(traceValues); + } + } +} + +} // end anonymous namespace + +SILTransform *swift::createOSSALifetimeAnalysis() { + return new OSSALifetimeAnalysis(); +} diff --git a/lib/SILOptimizer/Utils/CanonicalOSSALifetime.cpp b/lib/SILOptimizer/Utils/CanonicalOSSALifetime.cpp index 2805da8350639..e9e968808b5a2 100644 --- a/lib/SILOptimizer/Utils/CanonicalOSSALifetime.cpp +++ b/lib/SILOptimizer/Utils/CanonicalOSSALifetime.cpp @@ -107,7 +107,7 @@ void swift::copyLiveUse(Operand *use, InstModCallbacks &instModCallbacks) { //===----------------------------------------------------------------------===// bool CanonicalizeOSSALifetime::computeCanonicalLiveness() { - defUseWorklist.initialize(currentDef); + defUseWorklist.initialize(getCurrentDef()); while (SILValue value = defUseWorklist.pop()) { SILPhiArgument *arg; if ((arg = dyn_cast(value)) && arg->isPhi()) { @@ -166,8 +166,10 @@ bool CanonicalizeOSSALifetime::computeCanonicalLiveness() { recordConsumingUse(use); break; case OperandOwnership::Borrow: - if (!liveness.updateForBorrowingOperand(use)) + if (liveness.updateForBorrowingOperand(use) + != InnerBorrowKind::Contained) { return false; + } break; case OperandOwnership::InteriorPointer: case OperandOwnership::ForwardingBorrow: @@ -186,7 +188,7 @@ bool CanonicalizeOSSALifetime::computeCanonicalLiveness() { defUseWorklist.insert(cast(user)); break; } - if (is_contained(user->getOperandValues(), currentDef)) { + if (is_contained(user->getOperandValues(), getCurrentDef())) { // An adjacent phi consumes the value being reborrowed. Although this // use doesn't end the lifetime, this user does. liveness.updateForUse(user, /*lifetimeEnding*/ true); @@ -336,7 +338,7 @@ void CanonicalizeOSSALifetime::extendLivenessThroughOverlappingAccess() { // // It must be populated first so that we can test membership during the loop // (see findLastConsume). - BasicBlockSetVector blocksToVisit(currentDef->getFunction()); + BasicBlockSetVector blocksToVisit(getCurrentDef()->getFunction()); for (auto *block : consumingBlocks) { blocksToVisit.insert(block); } @@ -440,12 +442,12 @@ void CanonicalizeOSSALifetime::findOrInsertDestroyOnCFGEdge( assert(succBB->getSinglePredecessorBlock() == predBB && "value is live-out on another predBB successor: critical edge?"); - auto *di = findDestroyOnCFGEdge(succBB, currentDef); + auto *di = findDestroyOnCFGEdge(succBB, getCurrentDef()); if (!di) { auto pos = succBB->begin(); SILBuilderWithScope builder(pos); auto loc = RegularLocation::getAutoGeneratedLocation(pos->getLoc()); - di = builder.createDestroyValue(loc, currentDef); + di = builder.createDestroyValue(loc, getCurrentDef()); getCallbacks().createdNewInst(di); } consumes.recordFinalConsume(di); @@ -490,7 +492,7 @@ static void insertDestroyAtInst(SILBasicBlock::iterator pos, // values should be applied to that block summary which can provide the input // to rewriteCopies. void CanonicalizeOSSALifetime::findOrInsertDestroyInBlock(SILBasicBlock *bb) { - auto *defInst = currentDef->getDefiningInstruction(); + auto *defInst = getCurrentDef()->getDefiningInstruction(); DestroyValueInst *existingDestroy = nullptr; auto instIter = bb->getTerminator()->getIterator(); while (true) { @@ -512,8 +514,8 @@ void CanonicalizeOSSALifetime::findOrInsertDestroyInBlock(SILBasicBlock *bb) { findOrInsertDestroyOnCFGEdge(bb, succ); } } else { - insertDestroyAtInst(std::next(instIter), existingDestroy, currentDef, - consumes, getCallbacks()); + insertDestroyAtInst(std::next(instIter), existingDestroy, + getCurrentDef(), consumes, getCallbacks()); } return; case PrunedLiveness::LifetimeEndingUse: @@ -531,22 +533,22 @@ void CanonicalizeOSSALifetime::findOrInsertDestroyInBlock(SILBasicBlock *bb) { if (auto *destroy = dyn_cast(inst)) { auto destroyDef = CanonicalizeOSSALifetime::getCanonicalCopiedDef( destroy->getOperand()); - if (destroyDef == currentDef) { + if (destroyDef == getCurrentDef()) { existingDestroy = destroy; } } } if (instIter == bb->begin()) { - assert(cast(currentDef)->getParent() == bb); - insertDestroyAtInst(instIter, existingDestroy, currentDef, consumes, - getCallbacks()); + assert(cast(getCurrentDef())->getParent() == bb); + insertDestroyAtInst(instIter, existingDestroy, getCurrentDef(), + consumes, getCallbacks()); return; } --instIter; // If the original def is reached, this is a dead live range. Insert a // destroy immediately after the def. if (&*instIter == defInst) { - insertDestroyAtInst(std::next(instIter), existingDestroy, currentDef, + insertDestroyAtInst(std::next(instIter), existingDestroy, getCurrentDef(), consumes, getCallbacks()); return; } @@ -605,7 +607,7 @@ void CanonicalizeOSSALifetime::findOrInsertDestroys() { /// copies and destroys for deletion. Insert new copies for interior uses that /// require ownership of the used operand. void CanonicalizeOSSALifetime::rewriteCopies() { - assert(currentDef->getOwnershipKind() == OwnershipKind::Owned); + assert(getCurrentDef()->getOwnershipKind() == OwnershipKind::Owned); SmallSetVector instsToDelete; defUseWorklist.clear(); @@ -656,8 +658,8 @@ void CanonicalizeOSSALifetime::rewriteCopies() { }; // Perform a def-use traversal, visiting each use operand. - for (auto useIter = currentDef->use_begin(), endIter = currentDef->use_end(); - useIter != endIter;) { + for (auto useIter = getCurrentDef()->use_begin(), + endIter = getCurrentDef()->use_end(); useIter != endIter;) { Operand *use = *useIter++; if (!visitUse(use)) { copyLiveUse(use, getCallbacks()); diff --git a/lib/SILOptimizer/Utils/GenericCloner.cpp b/lib/SILOptimizer/Utils/GenericCloner.cpp index 95cc271873536..4f65d7bd681d9 100644 --- a/lib/SILOptimizer/Utils/GenericCloner.cpp +++ b/lib/SILOptimizer/Utils/GenericCloner.cpp @@ -250,9 +250,13 @@ void GenericCloner::postFixUp(SILFunction *f) { continue; } discoveredBlocks.clear(); - PrunedLiveness storeBorrowLiveness(&discoveredBlocks); - bool success = scopedAddress.computeLiveness(storeBorrowLiveness); - if (success) { + // FIXME: Call OSSA lifetime fixup on all values used within the unreachable + // code. This will recursively fixup nested scopes from the inside out so + // that transitive liveness is not required. + SSAPrunedLiveness storeBorrowLiveness(&discoveredBlocks); + AddressUseKind useKind = + scopedAddress.computeTransitiveLiveness(storeBorrowLiveness); + if (useKind == AddressUseKind::NonEscaping) { scopedAddress.endScopeAtLivenessBoundary(&storeBorrowLiveness); continue; } diff --git a/lib/SILOptimizer/Utils/LexicalDestroyFolding.cpp b/lib/SILOptimizer/Utils/LexicalDestroyFolding.cpp index 4e87828ec7af1..e31e89365da32 100644 --- a/lib/SILOptimizer/Utils/LexicalDestroyFolding.cpp +++ b/lib/SILOptimizer/Utils/LexicalDestroyFolding.cpp @@ -644,8 +644,8 @@ bool findBorroweeUsage(Context const &context, BorroweeUsage &usage) { bool borroweeHasUsesWithinBorrowScope(Context const &context, BorroweeUsage const &usage) { - PrunedLiveness liveness; - context.borrowedValue.computeLiveness(liveness); + MultiDefPrunedLiveness liveness(context.function); + context.borrowedValue.computeTransitiveLiveness(liveness); DeadEndBlocks deadEndBlocks(context.function); return !liveness.areUsesOutsideBoundary(usage.uses, &deadEndBlocks); } diff --git a/lib/SILOptimizer/Utils/OwnershipOptUtils.cpp b/lib/SILOptimizer/Utils/OwnershipOptUtils.cpp index 57187d55dc35d..a4a257ec6d7b7 100644 --- a/lib/SILOptimizer/Utils/OwnershipOptUtils.cpp +++ b/lib/SILOptimizer/Utils/OwnershipOptUtils.cpp @@ -140,12 +140,13 @@ bool swift::computeGuaranteedBoundary(SILValue value, bool noEscape = findInnerTransitiveGuaranteedUses(value, &usePoints); SmallVector discoveredBlocks; - PrunedLiveness liveness(&discoveredBlocks); + SSAPrunedLiveness liveness(&discoveredBlocks); + liveness.initializeDef(value); for (auto *use : usePoints) { assert(!use->isLifetimeEnding()); liveness.updateForUse(use->getUser(), /*lifetimeEnding*/ false); } - boundary.compute(liveness); + liveness.computeBoundary(boundary); return noEscape; } @@ -197,7 +198,7 @@ GuaranteedOwnershipExtension::checkBorrowExtension( return Valid; // arguments have whole-function ownership assert(guaranteedLiveness.empty()); - borrow.computeLiveness(guaranteedLiveness); + borrow.computeTransitiveLiveness(guaranteedLiveness); if (guaranteedLiveness.areUsesWithinBoundary(newUses, &deBlocks)) return Valid; // reuse the borrow scope as-is @@ -239,7 +240,7 @@ GuaranteedOwnershipExtension::checkLifetimeExtension( if (ownershipKind != OwnershipKind::Owned) return Invalid; - ownedLifetime.initializeDefBlock(ownedValue->getParentBlock()); + ownedLifetime.initializeDef(ownedValue); for (Operand *use : ownedValue->getUses()) { auto *user = use->getUser(); if (use->isConsuming()) { @@ -260,17 +261,17 @@ void GuaranteedOwnershipExtension::transform(Status status) { return; case ExtendBorrow: { PrunedLivenessBoundary guaranteedBoundary; - guaranteedBoundary.compute(guaranteedLiveness, ownedConsumeBlocks); + guaranteedLiveness.computeBoundary(guaranteedBoundary, ownedConsumeBlocks); extendLocalBorrow(beginBorrow, guaranteedBoundary, deleter); break; } case ExtendLifetime: { ownedLifetime.extendAcrossLiveness(guaranteedLiveness); PrunedLivenessBoundary ownedBoundary; - ownedBoundary.compute(ownedLifetime, ownedConsumeBlocks); + ownedLifetime.computeBoundary(ownedBoundary, ownedConsumeBlocks); extendOwnedLifetime(beginBorrow->getOperand(), ownedBoundary, deleter); PrunedLivenessBoundary guaranteedBoundary; - guaranteedBoundary.compute(guaranteedLiveness, ownedConsumeBlocks); + guaranteedLiveness.computeBoundary(guaranteedBoundary, ownedConsumeBlocks); extendLocalBorrow(beginBorrow, guaranteedBoundary, deleter); break; } @@ -437,9 +438,9 @@ bool swift::areUsesWithinLexicalValueLifetime(SILValue value, return true; if (auto borrowedValue = BorrowedValue(value)) { - PrunedLiveness liveness; auto *function = value->getFunction(); - borrowedValue.computeLiveness(liveness); + MultiDefPrunedLiveness liveness(function); + borrowedValue.computeTransitiveLiveness(liveness); DeadEndBlocks deadEndBlocks(function); return liveness.areUsesWithinBoundary(uses, &deadEndBlocks); } diff --git a/test/SIL/store_borrow_verify_errors.sil b/test/SIL/store_borrow_verify_errors.sil index bf75ca78d0bd6..2021ed77556a8 100644 --- a/test/SIL/store_borrow_verify_errors.sil +++ b/test/SIL/store_borrow_verify_errors.sil @@ -122,15 +122,19 @@ bb0(%0 : @guaranteed $Klass): return %1 : $() } -// CHECK: Begin Error in function test_load_borrow_store_borrow_nested -// CHECK: SIL verification failed: A store_borrow cannot be nested within another store_borrow to its destination -// CHECK: Verifying instruction: -// CHECK: %0 = argument of bb0 : $Klass // user: %2 -// CHECK: %1 = alloc_stack $Klass // users: %8, %4, %2 -// CHECK: -> %2 = store_borrow %0 to %1 : $*Klass // users: %7, %3 -// CHECK: %3 = load_borrow %2 : $*Klass // users: %6, %4 -// CHECK: end_borrow %2 : $*Klass // id: %7 -// CHECK: End Error in function test_load_borrow_store_borrow_nested +// FIXME: SILVerifier checkStoreBorrowInst needs to be fixed to +// successfully compute liveness of a load_borrow that contains a +// store_borrow. +// +// HECK: Begin Error in function test_load_borrow_store_borrow_nested +// HECK: SIL verification failed: A store_borrow cannot be nested within another store_borrow to its destination +// HECK: Verifying instruction: +// HECK: %0 = argument of bb0 : $Klass // user: %2 +// HECK: %1 = alloc_stack $Klass // users: %8, %4, %2 +// HECK: -> %2 = store_borrow %0 to %1 : $*Klass // users: %7, %3 +// HECK: %3 = load_borrow %2 : $*Klass // users: %6, %4 +// HECK: end_borrow %2 : $*Klass // id: %7 +// HECK: End Error in function test_load_borrow_store_borrow_nested sil [ossa] @test_load_borrow_store_borrow_nested : $@convention(thin) (@guaranteed Klass) -> () { bb0(%0 : @guaranteed $Klass): %stk = alloc_stack $Klass diff --git a/test/SILOptimizer/ossa_incomplete_lifetimes.sil b/test/SILOptimizer/ossa_incomplete_lifetimes.sil new file mode 100644 index 0000000000000..a5e294372c1e7 --- /dev/null +++ b/test/SILOptimizer/ossa_incomplete_lifetimes.sil @@ -0,0 +1,120 @@ +// RUN: %target-sil-opt %s -ossa-lifetime-analysis -o /dev/null | %FileCheck %s +// +// These tests rely on "incomplete" OSSA lifetimes. Incomplete OSSA +// lifetimes are invalid SIL, but the OSSA liveness utilities still +// need to handle them! SILGen and textual SIL is allowed to produce +// incomplete lifetimes. The OSSA liveness utilities need to be able +// to fixup those incomplete lifetimes. So the utilities need to +// handle invalid SIL in order to produce valid SIL. + +sil_stage canonical + +import Builtin + +enum FakeOptional { +case none +case some(T) +} + +enum Never {} + +class C {} + +// CHECK-LABEL: @testDeadTerminatorResult +// CHECK-LABEL: SSA lifetime analysis: %{{.*}} = argument of bb1 : $C +// CHECK: bb1: LiveWithin +// CHECK-NOT: "last user" +// CHECK-NOT: "boundary edge" +// CHECK: dead def: %{{.*}} = argument of bb1 : $C +sil [ossa] @testDeadTerminatorResult : $@convention(thin) (@owned FakeOptional) -> () { +bb0(%0 : @owned $FakeOptional): + switch_enum %0 : $FakeOptional, case #FakeOptional.some!enumelt: bb1, case #FakeOptional.none!enumelt: bb2 + +bb1(%payload : @owned $C): + debug_value [trace] %payload : $C + unreachable + +bb2: + %99 = tuple() + return %99 : $() +} + +// CHECK-LABEL: @testDeadPhi +// CHECK: SSA lifetime analysis: %{{.*}} = argument of bb3 : $C +// CHECK: bb3: LiveWithin +// CHECK: dead def: %{{.*}} = argument of bb3 : $C +sil [ossa] @testDeadPhi : $@convention(thin) (@owned C, @owned C) -> () { +bb0(%0 : @owned $C, %1 : @owned $C): + cond_br undef, bb1, bb2 + +bb1: + br bb3(%0 : $C) + +bb2: + br bb3(%1 : $C) + +bb3(%2 : @owned $C): + debug_value [trace] %2 : $C + unreachable +} + +// CHECK-LABEL: @testDeadInstruction +// CHECK: SSA lifetime analysis: %{{.*}} = copy_value %0 : $C +// CHECK: bb0: LiveWithin +// CHECK: dead def: %{{.*}} = copy_value %0 : $C +sil [ossa] @testDeadInstruction : $@convention(thin) (@guaranteed C) -> () { +bb0(%0 : @guaranteed $C): + %1 = copy_value %0 : $C + debug_value [trace] %1 : $C + unreachable +} + +// A single instruction occurs twice on the same liveness +// boundary. Once as a last use, and once as a dead def. +// This is a particularly problematic corner case. +// +// CHECK-LABEL: @testDeadSelfKill +// CHECK: MultiDef lifetime analysis: +// CHECK: def: %1 = argument of bb1 : $C +// CHECK: def: [[V:%.*]] = move_value %1 : $C +// CHECK: bb1: LiveWithin +// CHECK: lifetime-ending user: [[V]] = move_value %1 : $C +// CHECK: last user: [[V]] = move_value %1 : $C +// CHECK: dead def: [[V]] = move_value %1 : $C +sil [ossa] @testDeadSelfKill : $@convention(thin) () -> () { +bb0: + br bb3 + +bb1(%1 : @owned $C): + debug_value [trace] %1 : $C + %2 = move_value %1 : $C + debug_value [trace] %2 : $C + unreachable + +bb3: + %99 = tuple() + return %99 : $() +} + +sil @genericReturn : $@convention(thin) <τ_0_0> (@guaranteed C) -> @out τ_0_0 + +// The store_borrow scope has an inner load_borrow scope, which is +// incomplete. Since the store_borrow produces an address, the load +// borrow is considered a ScopedAddressUse, and all of its uses, +// including the Apply will contribute to the store_borrow liveness. +// CHECK-LABEL: @testInnerUnreachable +// CHECK: Scoped address analysis: [[SB:%.*]] = store_borrow %0 +// CHECK: bb0: LiveWithin +// CHECK-NEXT: regular user: %{{.*}} = apply +// CHECK-NEXT: last user: %{{.*}} = apply +sil shared [ossa] @testInnerUnreachable : $@convention(thin) (@guaranteed C, @thick Never.Type) -> () { +bb0(%0 : @guaranteed $C, %1 : $@thick Never.Type): + %2 = alloc_stack $C + %3 = store_borrow %0 to %2 : $*C + debug_value [trace] %3 : $*C + %5 = load_borrow %3 : $*C + %6 = function_ref @genericReturn : $@convention(thin) <τ_0_0> (@guaranteed C) -> @out τ_0_0 + %7 = alloc_stack $Never + %8 = apply %6(%7, %5) : $@convention(thin) <τ_0_0> (@guaranteed C) -> @out τ_0_0 + unreachable +} diff --git a/test/SILOptimizer/ossa_lifetime_analysis.sil b/test/SILOptimizer/ossa_lifetime_analysis.sil new file mode 100644 index 0000000000000..301e107d6d93e --- /dev/null +++ b/test/SILOptimizer/ossa_lifetime_analysis.sil @@ -0,0 +1,134 @@ +// RUN: %target-sil-opt %s -ossa-lifetime-analysis -o /dev/null | %FileCheck %s +// +// These tests rely on "incomplete" OSSA lifetimes. Incomplete OSSA +// lifetimes are invalid SIL, but the OSSA liveness utilities still +// need to handle them! SILGen and textual SIL is allowed to produce +// incomplete lifetimes. The OSSA liveness utilities need to be able +// to fixup those incomplete lifetimes. So the utilities need to +// handle invalid SIL in order to produce valid SIL. +// +// For each function, all values used by debug_value [trace] are +// considered live range defs. Liveness is computed from their direct uses. + +sil_stage canonical + +import Builtin + +enum FakeOptional { +case none +case some(T) +} + +class C {} +class D { + var object: C +} + +// CHECK-LABEL: @testSelfLoop +// CHECK: SSA lifetime analysis: [[V:%.*]] = copy_value %0 : $C +// CHECK: bb1: LiveOut +// CHECK: bb2: LiveWithin +// CHECK: lifetime-ending user: br bb1([[V]] : $C) +// CHECK: last user: br bb1([[V]] : $C) +// CHECK-NOT: dead def +sil [ossa] @testSelfLoop : $@convention(thin) (@guaranteed C) -> () { +bb0(%0 : @guaranteed $C): + br bb3 + +bb1(%1 : @owned $C): + destroy_value %1 : $C + %2 = copy_value %0 : $C + debug_value [trace] %2 : $C + br bb2 + +bb2: + br bb1(%2 : $C) + +bb3: + %99 = tuple() + return %99 : $() +} + +// CHECK-LABEL: @testSelfKill +// CHECK:SSA lifetime analysis: [[V:%.*]] = move_value %1 : $C +// CHECK: bb1: LiveOut +// CHECK: bb2: LiveWithin +// CHECK: lifetime-ending user: br bb1([[V]] : $C) +// CHECK: last user: br bb1([[V]] : $C) +// CHECK-NOT: dead def +sil [ossa] @testSelfKill : $@convention(thin) () -> () { +bb0: + br bb3 + +bb1(%1 : @owned $C): + %2 = move_value %1 : $C + debug_value [trace] %2 : $C + br bb2 + +bb2: + br bb1(%2 : $C) + +bb3: + %99 = tuple() + return %99 : $() +} + +// Test a live range that is extended through reborrows, +// considering them new defs. +// (e.g. BorrowedValue::visitTransitiveLifetimeEndingUses) +// +// This live range is not dominated by the original borrow. +// +// CHECK-LABEL: @testReborrow +// CHECK: MultiDef lifetime analysis: +// CHECK: def: [[B:%.*]] = begin_borrow %0 : $C +// CHECK: def: [[RB:%.*]] = argument of bb3 : $C +// CHECK-NEXT: bb2: LiveWithin +// CHECK-NEXT: bb3: LiveWithin +// CHECK-NEXT: lifetime-ending user: br bb3([[B]] : $C) +// CHECK-NEXT: lifetime-ending user: end_borrow [[RB]] : $C +// CHECK-NEXT: last user: br bb3([[B]] : $C) +// CHECK-NEXT: last user: end_borrow [[RB]] : $C +sil [ossa] @testReborrow : $@convention(thin) (@owned C) -> () { +bb0(%0 : @owned $C): + cond_br undef, bb1, bb2 + +bb1: + %borrow1 = begin_borrow %0 : $C + br bb3(%borrow1 : $C) + +bb2: + %borrow2 = begin_borrow %0 : $C + debug_value [trace] %borrow2 : $C + br bb3(%borrow2 : $C) + +bb3(%reborrow : @guaranteed $C): + debug_value [trace] %reborrow : $C + end_borrow %reborrow : $C + br bb4 + +bb4: + destroy_value %0 : $C + %99 = tuple() + return %99 : $() +} + +// CHECK-LABEL: @testGuaranteedForwarding +// CHECK: SSA lifetime analysis: [[C:%.*]] = unchecked_ref_cast %{{.*}} : $D to $C +// CHECK: bb0: LiveWithin +// CHECK: regular user: %{{.*}} = load [copy] +// CHECK: last user: %{{.*}} = load [copy] +sil [ossa] @testGuaranteedForwarding : $@convention(thin) (@owned D) -> () { +bb0(%0 : @owned $D): + %borrow0 = begin_borrow %0 : $D + %c = unchecked_ref_cast %borrow0 : $D to $C + debug_value [trace] %c : $C + %d = unchecked_ref_cast %c : $C to $D + %f = ref_element_addr %d : $D, #D.object + %o = load [copy] %f : $*C + end_borrow %borrow0 : $D + destroy_value %o : $C + destroy_value %0 : $D + %99 = tuple() + return %99 : $() +} diff --git a/test/SILOptimizer/specialize_ossa.sil b/test/SILOptimizer/specialize_ossa.sil index 832c37f6d09bb..1911a19f3ebb5 100644 --- a/test/SILOptimizer/specialize_ossa.sil +++ b/test/SILOptimizer/specialize_ossa.sil @@ -1287,3 +1287,44 @@ bb0(%0 : $Builtin.Int32, %1 : @guaranteed $Builtin.NativeObject): %9999 = tuple() return %9999 : $() } + +sil @genericReturn : $@convention(thin) <τ_0_0> (@guaranteed AnyObject) -> @out τ_0_0 + +// rdar://99874173: assert; PrunedLiveness; No user in LiveWithin block +// +// end_borrow becomes unreachable after specializing genericReturn with Never. +// +// The generic cloner needs to be able to compute liveness to fixup a +// store borrow scope when the only use is in unreachable code. +// specialized testNoReturnSpecialization +// CHECK-LABEL: sil shared [ossa] @$s26testNoReturnSpecializations5NeverO_Tg5 : $@convention(thin) (@guaranteed AnyObject, @thick Never.Type) -> () { +// CHECK: [[SB:%.*]] = store_borrow %0 to %{{.*}} : $*AnyObject +// CHECK: [[LB:%.*]] = load_borrow [[SB]] : $*AnyObject +// CHECK: apply %{{.*}}(%{{.*}}, [[LB]]) : $@convention(thin) <τ_0_0> (@guaranteed AnyObject) -> @out τ_0_0 +// CHECK: end_borrow [[SB]] : $*AnyObject +// CHECK: unreachable +// CHECK: bb1: +// CHECK: end_borrow [[LB]] : $AnyObject +// CHECK: end_borrow [[SB]] : $*AnyObject +// CHECK-LABEL: } // end sil function '$s26testNoReturnSpecializations5NeverO_Tg5' +sil [ossa] @testNoReturnSpecialization : $@convention(thin) (@in_guaranteed AnyObject, @thick T.Type) -> () { +bb0(%0 : $*AnyObject, %1 : $@thick T.Type): + %2 = load_borrow %0 : $*AnyObject + %f = function_ref @genericReturn : $@convention(thin) <τ_0_0> (@guaranteed AnyObject) -> @out τ_0_0 + %out = alloc_stack $T + %dummy = apply %f(%out, %2) : $@convention(thin) <τ_0_0> (@guaranteed AnyObject) -> @out τ_0_0 + destroy_addr %out : $*T + dealloc_stack %out : $*T + end_borrow %2 : $AnyObject + %99 = tuple () + return %99 : $() +} + +sil [ossa] @testCallNoReturnSpecialization : $@convention(thin) (@in_guaranteed AnyObject) -> () { +bb0(%0 : $*AnyObject): + %f = function_ref @testNoReturnSpecialization : $@convention(thin) (@in_guaranteed AnyObject, @thick T.Type) -> () + %m = metatype $@thick Never.Type + %r = apply %f(%0, %m) : $@convention(thin) (@in_guaranteed AnyObject, @thick T.Type) -> () + %t = tuple () + return %t : $() +}