Skip to content

Commit ccd08ca

Browse files
committed
Add a completeOSSALifetime utility
Add local lifetime-ending operations to any owned or borrowed value. This puts a single value into valid OSSA form so that linear lifetime checking will pass. Also adds UnreachableLifetimeCompletion which fixes OSSA after converting a code path to unreachable (e.g. DiagnoseUnreachable and MandatoryInlining).
1 parent 60d0dd6 commit ccd08ca

File tree

3 files changed

+380
-0
lines changed

3 files changed

+380
-0
lines changed
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
//===--- OwnershipLifetimeCompletion.h ------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
///
13+
/// OSSA lifetime completion adds lifetime ending instructions to make
14+
/// linear lifetimes complete.
15+
///
16+
/// Completion is bottom-up recursive over nested borrow scopes. Additionally,
17+
/// this may be extended to support dependent owned lifetimes in the future to
18+
/// handle owned non-escaping values.
19+
///
20+
/// Lexical lifetimes can only be incomplete as a result of dead-end blocks. In
21+
/// this case, their lifetime ends immediately before the dead-end block.
22+
///
23+
/// Nonlexical lifetimes can be incomplete for any reason. Their lifetime ends
24+
/// at the liveness boundary.
25+
///
26+
//===----------------------------------------------------------------------===//
27+
28+
#ifndef SWIFT_SILOPTIMIZER_UTILS_OSSSALIFETIMECOMPLETION_H
29+
#define SWIFT_SILOPTIMIZER_UTILS_OSSSALIFETIMECOMPLETION_H
30+
31+
#include "swift/SIL/NodeDatastructures.h"
32+
#include "swift/SIL/OwnershipLiveness.h"
33+
#include "swift/SIL/SILFunction.h"
34+
35+
namespace swift {
36+
37+
enum class LifetimeCompletion { NoLifetime, AlreadyComplete, WasCompleted };
38+
39+
class OSSALifetimeCompletion {
40+
// If domInfo is nullptr, then InteriorLiveness never assumes dominance. As a
41+
// result it may report extra unenclosedPhis. In that case, any attempt to
42+
// create a new phi would result in an immediately redundant phi.
43+
const DominanceInfo *domInfo = nullptr;
44+
45+
// Cache intructions already handled by the recursive algorithm to avoid
46+
// recomputing their lifetimes.
47+
ValueSet completedValues;
48+
49+
public:
50+
OSSALifetimeCompletion(SILFunction *function, const DominanceInfo *domInfo)
51+
: domInfo(domInfo), completedValues(function) {}
52+
53+
/// Insert a lifetime-ending instruction on every path to complete the OSSA
54+
/// lifetime of \p value. Lifetime completion is only relevant for owned
55+
/// values or borrow introducers.
56+
///
57+
/// Returns true if any new instructions were created to complete the
58+
/// lifetime.
59+
///
60+
/// TODO: We also need to complete scoped addresses (e.g. store_borrow)!
61+
LifetimeCompletion completeOSSALifetime(SILValue value) {
62+
if (value->getOwnershipKind() == OwnershipKind::None)
63+
return LifetimeCompletion::NoLifetime;
64+
65+
if (value->getOwnershipKind() != OwnershipKind::Owned) {
66+
BorrowedValue borrowedValue(value);
67+
if (!borrowedValue)
68+
return LifetimeCompletion::NoLifetime;
69+
70+
if (!borrowedValue.isLocalScope())
71+
return LifetimeCompletion::AlreadyComplete;
72+
}
73+
if (!completedValues.insert(value))
74+
return LifetimeCompletion::AlreadyComplete;
75+
76+
return analyzeAndUpdateLifetime(value)
77+
? LifetimeCompletion::WasCompleted
78+
: LifetimeCompletion::AlreadyComplete;
79+
}
80+
81+
protected:
82+
bool analyzeAndUpdateLifetime(SILValue value);
83+
};
84+
85+
//===----------------------------------------------------------------------===//
86+
// UnreachableLifetimeCompletion
87+
//===----------------------------------------------------------------------===//
88+
89+
/// Fixup OSSA before deleting an unreachable code path.
90+
///
91+
/// Only needed when a code path reaches a no-return function, making the
92+
/// path now partially unreachable. Conditional branch folding requires no fixup
93+
/// because it causes the entire path to become unreachable.
94+
class UnreachableLifetimeCompletion {
95+
SILFunction *function;
96+
97+
// If domInfo is nullptr, lifetime completion may attempt to recreate
98+
// redundant phis, which should be immediately discarded.
99+
const DominanceInfo *domInfo = nullptr;
100+
101+
BasicBlockSetVector unreachableBlocks;
102+
InstructionSet unreachableInsts; // not including those in unreachableBlocks
103+
ValueSetVector incompleteValues;
104+
bool updatingLifetimes = false;
105+
106+
public:
107+
UnreachableLifetimeCompletion(SILFunction *function, DominanceInfo *domInfo)
108+
: function(function), unreachableBlocks(function),
109+
unreachableInsts(function), incompleteValues(function) {}
110+
111+
/// Record information about this unreachable instruction and return true if
112+
/// ends any simple OSSA lifetimes.
113+
///
114+
/// Note: this must be called in forward order so that lifetime completion
115+
/// runs from the inside out.
116+
void visitUnreachableInst(SILInstruction *instruction);
117+
118+
void visitUnreachableBlock(SILBasicBlock *block) {
119+
unreachableBlocks.insert(block);
120+
}
121+
122+
/// Complete the lifetime of any value defined outside of the unreachable
123+
/// region that was previously destroyed in the unreachable region.
124+
bool completeLifetimes();
125+
};
126+
127+
} // namespace swift
128+
129+
#endif

lib/SIL/Utils/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ target_sources(swiftSIL PRIVATE
1111
MemAccessUtils.cpp
1212
MemoryLocations.cpp
1313
OptimizationRemark.cpp
14+
OSSALifetimeCompletion.cpp
1415
OwnershipLiveness.cpp
1516
OwnershipUtils.cpp
1617
PrettyStackTrace.cpp
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
//===--- OSSALifetimeCompletion.cpp ---------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
///
13+
/// OSSA lifetime completion adds lifetime ending instructions to make
14+
/// linear lifetimes complete.
15+
///
16+
/// Interior liveness handles the following cases naturally:
17+
///
18+
/// When completing the lifetime if the initial value, %v1, transitively
19+
/// include all dominated reborrows. %phi1 in this example:
20+
///
21+
/// %v1 = ...
22+
/// cond_br bb1, bb2
23+
/// bb1:
24+
/// %b1 = begin_borrow %v1
25+
/// br bb3(%b1)
26+
/// bb2:
27+
/// %b2 = begin_borrow %v1
28+
/// br bb3(%b2)
29+
/// bb3(%phi1):
30+
/// %u1 = %phi1
31+
/// end_borrow %phi1
32+
/// %k1 = destroy_value %v1 // must be below end_borrow %phi1
33+
///
34+
/// When completing the lifetime for a (%phi2) transitively include all inner
35+
/// adjacent reborrows (%phi1):
36+
///
37+
/// bb1:
38+
/// %v1 = ...
39+
/// %b1 = begin_borrow %v1
40+
/// br bb3(%b1, %v1)
41+
/// bb2:
42+
/// %v2 = ...
43+
/// %b2 = begin_borrow %v2
44+
/// br bb3(%b2, %v2)
45+
/// bb3(%phi1, %phi2):
46+
/// %u1 = %phi1
47+
/// end_borrow %phi1
48+
/// %k1 = destroy_value %phi1
49+
///
50+
//===----------------------------------------------------------------------===//
51+
52+
#include "swift/SIL/OSSALifetimeCompletion.h"
53+
#include "swift/SIL/SILBuilder.h"
54+
#include "swift/SIL/SILInstruction.h"
55+
56+
using namespace swift;
57+
58+
static SILInstruction *endOSSALifetime(SILValue value, SILBuilder &builder) {
59+
auto loc =
60+
RegularLocation::getAutoGeneratedLocation(builder.getInsertionPointLoc());
61+
if (value->getOwnershipKind() == OwnershipKind::Owned) {
62+
return builder.createDestroyValue(loc, value);
63+
}
64+
return builder.createEndBorrow(loc, value);
65+
}
66+
67+
static bool endLifetimeAtBoundary(SILValue value,
68+
const SSAPrunedLiveness &liveness) {
69+
PrunedLivenessBoundary boundary;
70+
liveness.computeBoundary(boundary);
71+
72+
bool changed = false;
73+
for (SILInstruction *lastUser : boundary.lastUsers) {
74+
if (liveness.isInterestingUser(lastUser)
75+
!= PrunedLiveness::LifetimeEndingUse) {
76+
changed = true;
77+
SILBuilderWithScope::insertAfter(lastUser, [value](SILBuilder &builder) {
78+
endOSSALifetime(value, builder);
79+
});
80+
}
81+
}
82+
for (SILBasicBlock *edge : boundary.boundaryEdges) {
83+
changed = true;
84+
SILBuilderWithScope builder(edge->begin());
85+
endOSSALifetime(value, builder);
86+
}
87+
for (SILNode *deadDef : boundary.deadDefs) {
88+
SILInstruction *next = nullptr;
89+
if (auto *deadInst = dyn_cast<SILInstruction>(deadDef)) {
90+
next = deadInst->getNextInstruction();
91+
} else {
92+
next = cast<ValueBase>(deadDef)->getNextInstruction();
93+
}
94+
changed = true;
95+
SILBuilderWithScope builder(next);
96+
endOSSALifetime(value, builder);
97+
}
98+
return changed;
99+
}
100+
101+
static bool endLifetimeAtUnreachableBlocks(SILValue value,
102+
const SSAPrunedLiveness &liveness) {
103+
PrunedLivenessBoundary boundary;
104+
liveness.computeBoundary(boundary);
105+
106+
BasicBlockWorklist deadEndBlocks(value->getFunction());
107+
for (SILInstruction *lastUser : boundary.lastUsers) {
108+
if (liveness.isInterestingUser(lastUser)
109+
!= PrunedLiveness::LifetimeEndingUse) {
110+
deadEndBlocks.push(lastUser->getParent());
111+
}
112+
}
113+
for (SILBasicBlock *edge : boundary.boundaryEdges) {
114+
deadEndBlocks.push(edge);
115+
}
116+
for (SILNode *deadDef : boundary.deadDefs) {
117+
deadEndBlocks.push(deadDef->getParentBlock());
118+
}
119+
// Forward CFG walk from the non-lifetime-ending boundary to the unreachable
120+
// instructions.
121+
bool changed = false;
122+
while (auto *block = deadEndBlocks.pop()) {
123+
if (block->succ_empty()) {
124+
// This assert will fail unless there are already lifetime-ending
125+
// instruction on all paths to normal function exits.
126+
auto *unreachable = cast<UnreachableInst>(block->getTerminator());
127+
SILBuilderWithScope builder(unreachable);
128+
endOSSALifetime(value, builder);
129+
changed = true;
130+
}
131+
for (auto *successor : block->getSuccessorBlocks()) {
132+
deadEndBlocks.push(successor);
133+
}
134+
}
135+
return changed;
136+
}
137+
138+
/// End the lifetime of \p value at unreachable instructions.
139+
///
140+
/// Returns true if any new instructions were created to complete the lifetime.
141+
///
142+
/// This is only meant to cleanup lifetimes that lead to dead-end blocks. After
143+
/// recursively completing all nested scopes, it then simply ends the lifetime
144+
/// at the Unreachable instruction.
145+
bool OSSALifetimeCompletion::analyzeAndUpdateLifetime(SILValue value) {
146+
// Called for inner borrows, inner adjacent reborrows, inner reborrows, and
147+
// scoped addresses.
148+
auto handleInnerScope = [this](SILValue innerBorrowedValue) {
149+
completeOSSALifetime(innerBorrowedValue);
150+
};
151+
InteriorLiveness liveness(value);
152+
liveness.compute(domInfo, handleInnerScope);
153+
154+
bool changed = false;
155+
if (value->isLexical()) {
156+
changed |= endLifetimeAtUnreachableBlocks(value, liveness.getLiveness());
157+
} else {
158+
changed |= endLifetimeAtBoundary(value, liveness.getLiveness());
159+
}
160+
// TODO: Rebuild outer adjacent phis on demand (SILGen does not currently
161+
// produce guaranteed phis). See FindEnclosingDefs &
162+
// findSuccessorDefsFromPredDefs. If no enclosing phi is found, we can create
163+
// it here and use updateSSA to recursively populate phis.
164+
assert(liveness.getUnenclosedPhis().empty());
165+
return changed;
166+
}
167+
168+
// TODO: create a fast check for 'mayEndLifetime(SILInstruction *)'. Verify that
169+
// it returns true for every instruction that has a lifetime-ending operand.
170+
void UnreachableLifetimeCompletion::visitUnreachableInst(
171+
SILInstruction *instruction) {
172+
auto *block = instruction->getParent();
173+
bool inReachableBlock = !unreachableBlocks.contains(block);
174+
// If this instruction's block is already marked unreachable, and
175+
// updatingLifetimes is not yet set, then this instruction will be visited
176+
// again later when propagating unreachable blocks.
177+
if (!inReachableBlock && !updatingLifetimes)
178+
return;
179+
180+
for (Operand &operand : instruction->getAllOperands()) {
181+
if (!operand.isLifetimeEnding())
182+
continue;
183+
184+
SILValue value = operand.get();
185+
SILBasicBlock *defBlock = value->getParentBlock();
186+
if (unreachableBlocks.contains(defBlock))
187+
continue;
188+
189+
auto *def = value->getDefiningInstruction();
190+
if (def && unreachableInsts.contains(def))
191+
continue;
192+
193+
// The operand's definition is still reachable and its lifetime ends on a
194+
// newly unreachable path.
195+
//
196+
// Note: The arguments of a no-return try_apply may still appear reachable
197+
// here because the try_apply itself is never visited as unreachable, hence
198+
// its successor blocks are not marked . But it
199+
// seems harmless to recompute their lifetimes.
200+
201+
// Insert this unreachable instruction in unreachableInsts if its parent
202+
// block is not already marked unreachable.
203+
if (inReachableBlock) {
204+
unreachableInsts.insert(instruction);
205+
}
206+
incompleteValues.insert(value);
207+
208+
// Add unreachable successors to the forward traversal worklist.
209+
if (auto *term = dyn_cast<TermInst>(instruction)) {
210+
for (auto *succBlock : term->getSuccessorBlocks()) {
211+
if (llvm::all_of(succBlock->getPredecessorBlocks(),
212+
[&](SILBasicBlock *predBlock) {
213+
if (predBlock == block)
214+
return true;
215+
216+
return unreachableBlocks.contains(predBlock);
217+
})) {
218+
unreachableBlocks.insert(succBlock);
219+
}
220+
}
221+
}
222+
}
223+
}
224+
225+
bool UnreachableLifetimeCompletion::completeLifetimes() {
226+
assert(!updatingLifetimes && "don't call this more than once");
227+
updatingLifetimes = true;
228+
229+
// Now that all unreachable terminator instructions have been visited,
230+
// propagate unreachable blocks.
231+
for (auto blockIt = unreachableBlocks.begin();
232+
blockIt != unreachableBlocks.end(); ++blockIt) {
233+
auto *block = *blockIt;
234+
for (auto &instruction : *block) {
235+
visitUnreachableInst(&instruction);
236+
}
237+
}
238+
239+
OSSALifetimeCompletion completion(function, domInfo);
240+
241+
bool changed = false;
242+
for (auto value : incompleteValues) {
243+
if (completion.completeOSSALifetime(value)
244+
== LifetimeCompletion::WasCompleted) {
245+
changed = true;
246+
}
247+
}
248+
return changed;
249+
}
250+

0 commit comments

Comments
 (0)