Skip to content

Commit 841e4ee

Browse files
authored
Merge pull request #35918 from eeckstein/diagnose-lifetime-issues
SILOptimizer: add a diagnostics pass to warn about lifetime issues with weak references.
2 parents 75fd1fe + a17f8c2 commit 841e4ee

File tree

11 files changed

+358
-17
lines changed

11 files changed

+358
-17
lines changed

include/swift/AST/DiagnosticsSIL.def

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,9 @@ WARNING(switch_on_a_constant,none,
279279
NOTE(unreachable_code_note,none, "will never be executed", ())
280280
WARNING(warn_infinite_recursive_call,none,
281281
"function call causes an infinite recursion", ())
282+
WARNING(warn_dead_weak_store,none,
283+
"weak reference will always be nil because the referenced object is "
284+
"deallocated here", ())
282285

283286
// 'transparent' diagnostics
284287
ERROR(circular_transparent,none,

include/swift/SILOptimizer/PassManager/Passes.def

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,8 @@ PASS(DiagnoseInfiniteRecursion, "diagnose-infinite-recursion",
154154
"Diagnose Infinitely-Recursive Code")
155155
PASS(DiagnoseInvalidEscapingCaptures, "diagnose-invalid-escaping-captures",
156156
"Diagnose Invalid Escaping Captures")
157+
PASS(DiagnoseLifetimeIssues, "diagnose-lifetime-issues",
158+
"Diagnose Lifetime Issues")
157159
PASS(DiagnoseStaticExclusivity, "diagnose-static-exclusivity",
158160
"Static Enforcement of Law of Exclusivity")
159161
PASS(DiagnoseUnreachable, "diagnose-unreachable",

include/swift/SILOptimizer/Utils/PrunedLiveness.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,10 @@ class PrunedLiveness {
219219
/// live range vs. a nested borrow scope within the extended live range.
220220
void updateForUse(SILInstruction *user, bool lifetimeEnding);
221221

222+
/// Updates the liveness for a whole borrow scope, beginning at \p op.
223+
/// Returns false if this cannot be done.
224+
bool updateForBorrowingOperand(Operand *op);
225+
222226
PrunedLiveBlocks::IsLive getBlockLiveness(SILBasicBlock *bb) const {
223227
return liveBlocks.getBlockLiveness(bb);
224228
}

lib/SILOptimizer/Mandatory/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ target_sources(swiftSILOptimizer PRIVATE
1010
DataflowDiagnostics.cpp
1111
DiagnoseInfiniteRecursion.cpp
1212
DiagnoseInvalidEscapingCaptures.cpp
13+
DiagnoseLifetimeIssues.cpp
1314
DiagnoseStaticExclusivity.cpp
1415
DiagnoseUnreachable.cpp
1516
Differentiation.cpp
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
//==-------- DiagnoseLifetimeIssues.cpp - Diagnose lifetime issues ---------==//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2021 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+
// This file implements a diagnostic pass that prints a warning if an object is
14+
// stored to a weak property (or is weakly captured) and destroyed before the
15+
// property (or captured reference) is ever used again.
16+
// This can happen if the programmer relies on the lexical scope to keep an
17+
// object alive, but copy-propagation can shrink the object's lifetime to its
18+
// last use.
19+
// For example:
20+
//
21+
// func test() {
22+
// let k = Klass()
23+
// // k is deallocated immediatly after the closure capture (a store_weak).
24+
// functionWithClosure({ [weak k] in
25+
// // crash!
26+
// k!.foo()
27+
// })
28+
// }
29+
//
30+
//===----------------------------------------------------------------------===//
31+
32+
#define DEBUG_TYPE "diagnose-lifetime-issues"
33+
#include "swift/AST/DiagnosticsSIL.h"
34+
#include "swift/SIL/ApplySite.h"
35+
#include "swift/SIL/BasicBlockBits.h"
36+
#include "swift/SILOptimizer/PassManager/Transforms.h"
37+
#include "swift/SILOptimizer/Utils/PrunedLiveness.h"
38+
#include "swift/Demangling/Demangler.h"
39+
#include "llvm/Support/Debug.h"
40+
41+
using namespace swift;
42+
43+
namespace {
44+
45+
/// Performs the analysis and prints the warnings.
46+
class DiagnoseLifetimeIssues {
47+
PrunedLiveness liveness;
48+
49+
/// Reuse a general worklist for def-use traversal.
50+
SmallSetVector<SILValue, 8> defUseWorklist;
51+
52+
bool computeCanonicalLiveness(SILValue def);
53+
54+
bool isDeadStore(StoreWeakInst *store);
55+
56+
public:
57+
DiagnoseLifetimeIssues() {}
58+
59+
void diagnose(SILFunction *function);
60+
};
61+
62+
/// Returns true if def is an owned value resulting from an object allocation.
63+
static bool isAllocation(SILValue def) {
64+
if (def.getOwnershipKind() != OwnershipKind::Owned)
65+
return false;
66+
67+
if (isa<AllocRefInst>(def))
68+
return true;
69+
70+
// Check if it's a call to an allocating initializer.
71+
if (auto *applyInst = dyn_cast<ApplyInst>(def)) {
72+
SILFunction *callee = applyInst->getReferencedFunctionOrNull();
73+
if (!callee)
74+
return false;
75+
76+
Demangle::StackAllocatedDemangler<1024> demangler;
77+
Demangle::Node *root = demangler.demangleSymbol(callee->getName());
78+
return root && root->getKind() == Demangle::Node::Kind::Global &&
79+
root->getFirstChild()->getKind() == Demangle::Node::Kind::Allocator;
80+
}
81+
82+
return false;
83+
}
84+
85+
/// Computes the canoncial lifetime of \p def, like the copy-propagation pass
86+
/// would do.
87+
/// The only difference is that we are treating enum instructions (taking a
88+
/// payload) like copies. Enums are important because the operand of a
89+
/// store_weak is always an Optional.
90+
bool DiagnoseLifetimeIssues::computeCanonicalLiveness(SILValue def) {
91+
defUseWorklist.clear();
92+
defUseWorklist.insert(def);
93+
while (!defUseWorklist.empty()) {
94+
SILValue value = defUseWorklist.pop_back_val();
95+
for (Operand *use : value->getUses()) {
96+
auto *user = use->getUser();
97+
98+
// Recurse through copies and enums.
99+
if (isa<CopyValueInst>(user) || isa<EnumInst>(user)) {
100+
defUseWorklist.insert(cast<SingleValueInstruction>(user));
101+
continue;
102+
}
103+
switch (use->getOperandOwnership()) {
104+
case OperandOwnership::NonUse:
105+
break;
106+
case OperandOwnership::TrivialUse:
107+
llvm_unreachable("this operand cannot handle ownership");
108+
109+
// Conservatively treat a conversion to an unowned value as a pointer
110+
// escape. Is it legal to canonicalize ForwardingUnowned?
111+
case OperandOwnership::ForwardingUnowned:
112+
case OperandOwnership::PointerEscape:
113+
return false;
114+
case OperandOwnership::InstantaneousUse:
115+
case OperandOwnership::UnownedInstantaneousUse:
116+
case OperandOwnership::BitwiseEscape:
117+
liveness.updateForUse(user, /*lifetimeEnding*/ false);
118+
break;
119+
case OperandOwnership::ForwardingConsume:
120+
// TODO: handle forwarding instructions, e.g. casts.
121+
return false;
122+
case OperandOwnership::DestroyingConsume:
123+
// destroy_value does not force pruned liveness (but store etc. does).
124+
if (!isa<DestroyValueInst>(user))
125+
return false;
126+
break;
127+
case OperandOwnership::Borrow:
128+
if (!liveness.updateForBorrowingOperand(use))
129+
return false;
130+
break;
131+
case OperandOwnership::InteriorPointer:
132+
case OperandOwnership::ForwardingBorrow:
133+
case OperandOwnership::EndBorrow:
134+
case OperandOwnership::Reborrow:
135+
llvm_unreachable("operand kind cannot take an owned value");
136+
}
137+
}
138+
}
139+
return true;
140+
}
141+
142+
/// Gets the underlying definition of \p val, looking through copy_value and
143+
/// enum instructions.
144+
static SILValue getCanonicalDef(SILValue val) {
145+
while (true) {
146+
if (auto *copyInst = dyn_cast<CopyValueInst>(val)) {
147+
SILValue copySrc = copyInst->getOperand();
148+
if (copySrc.getOwnershipKind() != OwnershipKind::Owned)
149+
return val;
150+
val = copySrc;
151+
continue;
152+
}
153+
if (auto *enumInst = dyn_cast<EnumInst>(val)) {
154+
if (enumInst->hasOperand()) {
155+
val = enumInst->getOperand();
156+
continue;
157+
}
158+
}
159+
return val;
160+
}
161+
}
162+
163+
/// Returns true if the stored object of \p store is never loaded within the
164+
/// lifetime of the stored object.
165+
bool DiagnoseLifetimeIssues::isDeadStore(StoreWeakInst *store) {
166+
SILValue storedDef = getCanonicalDef(store->getSrc());
167+
168+
// Only for allocations we know that a destroy will actually deallocate the
169+
// object. Otherwise the object could be kept alive by other references and
170+
// we would issue a false alarm.
171+
if (!isAllocation(storedDef))
172+
return false;
173+
174+
liveness.initializeDefBlock(storedDef->getParentBlock());
175+
if (!computeCanonicalLiveness(storedDef))
176+
return false;
177+
178+
179+
// Check if the lifetime of the stored object ends at the store_weak.
180+
//
181+
// A more sophisticated analysis would be to check if there are no
182+
// (potential) loads from the store's destination address after the store,
183+
// but within the object's liferange. But without a good alias analysis (and
184+
// we don't want to use AliasAnalysis in a mandatory pass) it's practially
185+
// impossible that a use of the object is not a potential load. So we would
186+
// always see a potential load if the lifetime of the object goes beyond the
187+
// store_weak.
188+
189+
SILBasicBlock *storeBlock = store->getParent();
190+
if (liveness.getBlockLiveness(storeBlock) != PrunedLiveBlocks::LiveWithin)
191+
return false;
192+
193+
// If there are any uses after the store_weak, it means that the liferange of
194+
// the object goes beyond the store_weak.
195+
for (SILInstruction &inst : make_range(std::next(store->getIterator()),
196+
storeBlock->end())) {
197+
switch (liveness.isInterestingUser(&inst)) {
198+
case PrunedLiveness::NonUser:
199+
break;
200+
case PrunedLiveness::NonLifetimeEndingUse:
201+
case PrunedLiveness::LifetimeEndingUse:
202+
return false;
203+
}
204+
}
205+
return true;
206+
}
207+
208+
/// Prints warnings for dead weak stores in \p function.
209+
void DiagnoseLifetimeIssues::diagnose(SILFunction *function) {
210+
for (SILBasicBlock &block : *function) {
211+
for (SILInstruction &inst : block) {
212+
if (auto *stWeak = dyn_cast<StoreWeakInst>(&inst)) {
213+
if (isDeadStore(stWeak)) {
214+
function->getModule().getASTContext().Diags.diagnose(
215+
stWeak->getLoc().getSourceLoc(), diag::warn_dead_weak_store);
216+
}
217+
liveness.clear();
218+
continue;
219+
}
220+
}
221+
}
222+
}
223+
224+
//===----------------------------------------------------------------------===//
225+
// The function pass
226+
//===----------------------------------------------------------------------===//
227+
228+
class DiagnoseLifetimeIssuesPass : public SILFunctionTransform {
229+
public:
230+
DiagnoseLifetimeIssuesPass() {}
231+
232+
private:
233+
void run() override {
234+
SILFunction *function = getFunction();
235+
// Don't rerun diagnostics on deserialized functions.
236+
if (function->wasDeserializedCanonical())
237+
return;
238+
239+
if (!function->hasOwnership())
240+
return;
241+
242+
DiagnoseLifetimeIssues diagnoser;
243+
diagnoser.diagnose(function);
244+
}
245+
};
246+
247+
} // end anonymous namespace
248+
249+
SILTransform *swift::createDiagnoseLifetimeIssues() {
250+
return new DiagnoseLifetimeIssuesPass();
251+
}

lib/SILOptimizer/PassManager/PassPipeline.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ static void addMandatoryDiagnosticOptPipeline(SILPassPipelinePlan &P) {
154154
P.addDiagnoseInfiniteRecursion();
155155
P.addYieldOnceCheck();
156156
P.addEmitDFDiagnostics();
157+
P.addDiagnoseLifetimeIssues();
157158

158159
// Canonical swift requires all non cond_br critical edges to be split.
159160
P.addSplitNonCondBrCriticalEdges();

lib/SILOptimizer/Utils/CanonicalOSSALifetime.cpp

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -408,22 +408,8 @@ bool CanonicalizeOSSALifetime::computeCanonicalLiveness() {
408408
recordConsumingUse(use);
409409
break;
410410
case OperandOwnership::Borrow:
411-
// A nested borrow scope is considered a use-point at each scope ending
412-
// instruction.
413-
//
414-
// TODO: Handle reborrowed copies by considering the extended borrow
415-
// scope. Temporarily bail-out on reborrows because we can't handle uses
416-
// that aren't dominated by currentDef.
417-
if (!BorrowingOperand(use).visitScopeEndingUses(
418-
[this](Operand *end) {
419-
if (end->getOperandOwnership() == OperandOwnership::Reborrow) {
420-
return false;
421-
}
422-
liveness.updateForUse(end->getUser(), /*lifetimeEnding*/ false);
423-
return true;
424-
})) {
411+
if (!liveness.updateForBorrowingOperand(use))
425412
return false;
426-
}
427413
break;
428414
case OperandOwnership::InteriorPointer:
429415
case OperandOwnership::ForwardingBorrow:

lib/SILOptimizer/Utils/PrunedLiveness.cpp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
#include "swift/SILOptimizer/Utils/PrunedLiveness.h"
14+
#include "swift/SIL/OwnershipUtils.h"
1415

1516
using namespace swift;
1617

@@ -93,3 +94,25 @@ void PrunedLiveness::updateForUse(SILInstruction *user, bool lifetimeEnding) {
9394
if (!iterAndSuccess.second)
9495
iterAndSuccess.first->second &= lifetimeEnding;
9596
}
97+
98+
bool PrunedLiveness::updateForBorrowingOperand(Operand *op) {
99+
assert(op->getOperandOwnership() == OperandOwnership::Borrow);
100+
101+
// A nested borrow scope is considered a use-point at each scope ending
102+
// instruction.
103+
//
104+
// TODO: Handle reborrowed copies by considering the extended borrow
105+
// scope. Temporarily bail-out on reborrows because we can't handle uses
106+
// that aren't dominated by currentDef.
107+
if (!BorrowingOperand(op).visitScopeEndingUses(
108+
[this](Operand *end) {
109+
if (end->getOperandOwnership() == OperandOwnership::Reborrow) {
110+
return false;
111+
}
112+
updateForUse(end->getUser(), /*lifetimeEnding*/ false);
113+
return true;
114+
})) {
115+
return false;
116+
}
117+
return true;
118+
}

lib/Sema/MiscDiagnostics.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2050,6 +2050,9 @@ static void diagnoseUnownedImmediateDeallocationImpl(ASTContext &ctx,
20502050
if (varDecl->getDeclContext()->isTypeContext())
20512051
storageKind = SK_Property;
20522052

2053+
// TODO: The DiagnoseLifetimeIssuesPass prints a similiar warning in this
2054+
// situation. We should only print one warning.
2055+
20532056
ctx.Diags.diagnose(diagLoc, diag::unowned_assignment_immediate_deallocation,
20542057
varDecl->getName(), ownershipAttr->get(),
20552058
unsigned(storageKind))

test/SILOptimizer/definite_init_diagnostics.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ func test2() {
107107
weak var w1 : SomeClass?
108108
_ = w1 // ok: default-initialized
109109

110+
// expected-warning@+4 {{weak reference will always be nil because the referenced object is deallocated here}}
110111
// expected-warning@+3 {{instance will be immediately deallocated because variable 'w2' is 'weak'}}
111112
// expected-note@+2 {{a strong reference is required to prevent the instance from being deallocated}}
112113
// expected-note@+1 {{'w2' declared here}}
@@ -1336,7 +1337,7 @@ func testDontDiagnoseUnownedImmediateDeallocationThroughStrong() {
13361337
weak var c1: SomeClass?
13371338
do {
13381339
let tmp = SomeClass()
1339-
c1 = tmp
1340+
c1 = tmp // expected-warning {{weak reference will always be nil because the referenced object is deallocated here}}
13401341
}
13411342

13421343
unowned let c2: SomeClass
@@ -1347,7 +1348,7 @@ func testDontDiagnoseUnownedImmediateDeallocationThroughStrong() {
13471348

13481349
weak var c3: SomeClass?
13491350
let c3Tmp = SomeClass()
1350-
c3 = c3Tmp
1351+
c3 = c3Tmp // expected-warning {{weak reference will always be nil because the referenced object is deallocated here}}
13511352

13521353
unowned let c4: SomeClass
13531354
let c4Tmp = SomeClass()

0 commit comments

Comments
 (0)