Skip to content

Commit a17f8c2

Browse files
committed
SILOptimizer: add a diagnostics pass to warn about lifetime issues with weak references.
The DiagnoseLifetimeIssuesPass pass prints a warning if an object is stored to a weak property (or is weakly captured) and destroyed before the property (or captured reference) is ever used again. This can happen if the programmer relies on the lexical scope to keep an object alive, but copy-propagation can shrink the object's lifetime to its last use. For example: func test() { let k = Klass() // k is deallocated immediately after the closure capture (a store_weak). functionWithClosure({ [weak k] in // crash! k!.foo() }) } Unfortunately this pass can only catch simple cases, but it's better than nothing. rdar://73910632
1 parent ea9ce7e commit a17f8c2

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)