Skip to content

Commit 6dfc9c5

Browse files
committed
emit error on implicit destruction of self in discard context
As part of SE-390, you're required to write either: - `consume self` - pass self as a `consuming` parameter to a function - `discard self` before the function ends in a context that contains a `discard self` somewhere. This prevents people from accidentally invoking the deinit due to implicit destruction of `self` before exiting the function. rdar://106099027 (cherry picked from commit 88d35a0)
1 parent a710276 commit 6dfc9c5

File tree

7 files changed

+703
-12
lines changed

7 files changed

+703
-12
lines changed

include/swift/AST/DiagnosticsSIL.def

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -776,7 +776,11 @@ ERROR(sil_movechecking_notconsumable_but_assignable_was_consumed, none,
776776
ERROR(sil_movechecking_cannot_destructure_has_deinit, none,
777777
"cannot partially consume '%0' when it has a deinitializer",
778778
(StringRef))
779+
ERROR(sil_movechecking_discard_missing_consume_self, none,
780+
"must consume 'self' before exiting method that discards self", ())
779781

782+
NOTE(sil_movechecking_discard_self_here, none,
783+
"discarded self here", ())
780784
NOTE(sil_movechecking_partial_consume_here, none,
781785
"partially consumed here", ())
782786
NOTE(sil_movechecking_consuming_use_here, none,

lib/SILOptimizer/Mandatory/MoveOnlyAddressCheckerUtils.cpp

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,9 @@ struct UseState {
691691
/// [assign] that are reinits that we will convert to inits and true reinits.
692692
llvm::SmallMapVector<SILInstruction *, TypeTreeLeafTypeRange, 4> reinitInsts;
693693

694+
/// The set of drop_deinits of this mark_must_check
695+
SmallSetVector<SILInstruction *, 2> dropDeinitInsts;
696+
694697
/// A "inout terminator use" is an implicit liveness use of the entire value
695698
/// placed on a terminator. We use this both so we add liveness for the
696699
/// terminator user and so that we can use the set to quickly identify later
@@ -712,6 +715,31 @@ struct UseState {
712715
return inoutTermUsers.count(inst);
713716
}
714717

718+
/// Returns true if the given instruction is within the same block as a reinit
719+
/// and precedes a reinit instruction in that block.
720+
bool precedesReinitInSameBlock(SILInstruction *inst) const {
721+
SILBasicBlock *block = inst->getParent();
722+
SmallSetVector<SILInstruction *, 8> sameBlockReinits;
723+
724+
// First, search for all reinits that are within the same block.
725+
for (auto &reinit : reinitInsts) {
726+
if (reinit.first->getParent() != block)
727+
continue;
728+
sameBlockReinits.insert(reinit.first);
729+
}
730+
731+
if (sameBlockReinits.empty())
732+
return false;
733+
734+
// Walk down from the given instruction to see if we encounter a reinit.
735+
for (auto ii = std::next(inst->getIterator()); ii != block->end(); ++ii) {
736+
if (sameBlockReinits.contains(&*ii))
737+
return true;
738+
}
739+
740+
return false;
741+
}
742+
715743
void clear() {
716744
address = nullptr;
717745
destroys.clear();
@@ -721,6 +749,7 @@ struct UseState {
721749
takeInsts.clear();
722750
initInsts.clear();
723751
reinitInsts.clear();
752+
dropDeinitInsts.clear();
724753
inoutTermUsers.clear();
725754
debugValue = nullptr;
726755
}
@@ -755,6 +784,10 @@ struct UseState {
755784
for (auto pair : reinitInsts) {
756785
llvm::dbgs() << *pair.first;
757786
}
787+
llvm::dbgs() << "DropDeinits:\n";
788+
for (auto *inst : dropDeinitInsts) {
789+
llvm::dbgs() << *inst;
790+
}
758791
llvm::dbgs() << "InOut Term Users:\n";
759792
for (auto *inst : inoutTermUsers) {
760793
llvm::dbgs() << *inst;
@@ -1737,6 +1770,12 @@ bool GatherUsesVisitor::visitUse(Operand *op) {
17371770
LLVM_DEBUG(llvm::dbgs() << "Running copy propagation!\n");
17381771
moveChecker.changed |= moveChecker.canonicalizer.canonicalize();
17391772

1773+
// Export the drop_deinit's discovered by the ObjectChecker into the
1774+
// AddressChecker to preserve it for later use. We need to do this since
1775+
// the ObjectChecker's state gets cleared after running on this LoadInst.
1776+
for (auto *dropDeinit : moveChecker.canonicalizer.getDropDeinitUses())
1777+
moveChecker.addressUseState.dropDeinitInsts.insert(dropDeinit);
1778+
17401779
// If we are asked to perform no_consume_or_assign checking or
17411780
// assignable_but_not_consumable checking, if we found any consumes of our
17421781
// load, then we need to emit an error.
@@ -2458,10 +2497,41 @@ void MoveOnlyAddressCheckerPImpl::rewriteUses(
24582497
FieldSensitiveMultiDefPrunedLiveRange &liveness,
24592498
const FieldSensitivePrunedLivenessBoundary &boundary) {
24602499
LLVM_DEBUG(llvm::dbgs() << "MoveOnlyAddressChecker Rewrite Uses!\n");
2461-
// First remove all destroy_addr that have not been claimed.
2500+
2501+
/// Whether the marked value appeared in a discard statement.
2502+
const bool isDiscardingContext = !addressUseState.dropDeinitInsts.empty();
2503+
2504+
// Process destroys
24622505
for (auto destroyPair : addressUseState.destroys) {
2463-
if (!consumes.claimConsume(destroyPair.first, destroyPair.second)) {
2506+
/// Is this destroy instruction a final consuming use?
2507+
bool isFinalConsume =
2508+
consumes.claimConsume(destroyPair.first, destroyPair.second);
2509+
2510+
// Remove destroys that are not the final consuming use.
2511+
if (!isFinalConsume) {
24642512
destroyPair.first->eraseFromParent();
2513+
continue;
2514+
}
2515+
2516+
// Otherwise, if we're in a discarding context, flag this final destroy_addr
2517+
// as a point where we're missing an explicit `consume self`. The reasoning
2518+
// here is that if a destroy of self is the final consuming use,
2519+
// then these are the points where we implicitly destroy self to clean-up
2520+
// that self var before exiting the scope. An explicit 'consume self'
2521+
// that is thrown away is a consume of this mark_must_check'd var and not a
2522+
// destroy of it, according to the use classifier.
2523+
if (isDiscardingContext) {
2524+
2525+
// Since the boundary computations treat a newly-added destroy prior to
2526+
// a reinit within that same block as a "final consuming use", exclude
2527+
// such destroys-before-reinit. We are only interested in the final
2528+
// destroy of a var, not intermediate destroys of the var.
2529+
if (addressUseState.precedesReinitInSameBlock(destroyPair.first))
2530+
continue;
2531+
2532+
auto *dropDeinit = addressUseState.dropDeinitInsts.front();
2533+
diagnosticEmitter.emitMissingConsumeInDiscardingContext(destroyPair.first,
2534+
dropDeinit);
24652535
}
24662536
}
24672537

lib/SILOptimizer/Mandatory/MoveOnlyDiagnostics.cpp

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include "MoveOnlyDiagnostics.h"
1616

1717
#include "swift/AST/DiagnosticsSIL.h"
18+
#include "swift/AST/Stmt.h"
1819
#include "swift/Basic/Defer.h"
1920
#include "swift/SIL/BasicBlockBits.h"
2021
#include "swift/SIL/BasicBlockDatastructures.h"
@@ -190,6 +191,125 @@ void DiagnosticEmitter::emitCheckedMissedCopyError(SILInstruction *copyInst) {
190191
diag::sil_movechecking_bug_missed_copy);
191192
}
192193

194+
void DiagnosticEmitter::emitMissingConsumeInDiscardingContext(
195+
SILInstruction *leftoverDestroy,
196+
SILInstruction *discard) {
197+
assert(isa<DropDeinitInst>(discard));
198+
199+
// A good location is one that has some connection with the original source
200+
// and corresponds to an exit of the function.
201+
auto hasGoodLocation = [](SILInstruction *si) -> bool {
202+
if (!si)
203+
return false;
204+
205+
SILLocation loc = si->getLoc();
206+
if (loc.isNull())
207+
return false;
208+
209+
switch (loc.getKind()) {
210+
case SILLocation::ReturnKind:
211+
case SILLocation::ImplicitReturnKind:
212+
return true;
213+
214+
case SILLocation::RegularKind: {
215+
Stmt *stmt = loc.getAsASTNode<Stmt>();
216+
if (!stmt)
217+
return true; // For non-statements, assume it is exiting the func.
218+
219+
// Prefer statements that can possibly lead to an exit of the function.
220+
// This is determined by whether the statement causes an exit of a
221+
// lexical scope; so a 'break' counts but not a 'continue'.
222+
switch (stmt->getKind()) {
223+
case StmtKind::Throw:
224+
case StmtKind::Return:
225+
case StmtKind::Yield:
226+
case StmtKind::Break:
227+
case StmtKind::Fail:
228+
case StmtKind::PoundAssert:
229+
return true;
230+
231+
case StmtKind::Continue:
232+
case StmtKind::Brace:
233+
case StmtKind::Defer:
234+
case StmtKind::If:
235+
case StmtKind::Guard:
236+
case StmtKind::While:
237+
case StmtKind::Do:
238+
case StmtKind::DoCatch:
239+
case StmtKind::RepeatWhile:
240+
case StmtKind::ForEach:
241+
case StmtKind::Switch:
242+
case StmtKind::Case:
243+
case StmtKind::Fallthrough:
244+
case StmtKind::Discard:
245+
return false;
246+
};
247+
}
248+
249+
case SILLocation::InlinedKind:
250+
case SILLocation::MandatoryInlinedKind:
251+
case SILLocation::CleanupKind:
252+
case SILLocation::ArtificialUnreachableKind:
253+
return false;
254+
};
255+
};
256+
257+
// An instruction corresponding to the logical place where the value is
258+
// destroyed. Ideally an exit point of the function reachable from here or
259+
// some relevant statement.
260+
SILInstruction *destroyPoint = leftoverDestroy;
261+
if (!hasGoodLocation(destroyPoint)) {
262+
// Search for a nearby function exit reachable from this destroy. We do this
263+
// because the move checker may have injected or hoisted an existing
264+
// destroy from leaf blocks to some earlier point. For example, if 'd'
265+
// represents a destroy of self, then we may have this CFG:
266+
//
267+
// before: after:
268+
// . d
269+
// / \ / \
270+
// d d . .
271+
//
272+
BasicBlockSet visited(destroyPoint->getFunction());
273+
std::deque<SILBasicBlock *> bfsWorklist = {destroyPoint->getParent()};
274+
while (auto *bb = bfsWorklist.front()) {
275+
visited.insert(bb);
276+
bfsWorklist.pop_front();
277+
278+
TermInst *term = bb->getTerminator();
279+
280+
// Looking for a block that exits the function or terminates the program.
281+
if (term->isFunctionExiting() || term->isProgramTerminating()) {
282+
SILInstruction *candidate = term;
283+
284+
// Walk backwards until we find an instruction with any source location.
285+
// Sometimes a terminator like 'unreachable' may not have one, but one
286+
// of the preceding instructions will.
287+
while (candidate && candidate->getLoc().isNull())
288+
candidate = candidate->getPreviousInstruction();
289+
290+
if (candidate && candidate->getLoc()) {
291+
destroyPoint = candidate;
292+
break;
293+
}
294+
}
295+
296+
for (auto *nextBB : term->getSuccessorBlocks())
297+
if (!visited.contains(nextBB))
298+
bfsWorklist.push_back(nextBB);
299+
}
300+
}
301+
302+
assert(destroyPoint->getLoc() && "missing loc!");
303+
assert(discard->getLoc() && "missing loc!");
304+
305+
diagnose(leftoverDestroy->getFunction()->getASTContext(),
306+
destroyPoint,
307+
diag::sil_movechecking_discard_missing_consume_self);
308+
309+
diagnose(discard->getFunction()->getASTContext(), discard,
310+
diag::sil_movechecking_discard_self_here);
311+
}
312+
193313
//===----------------------------------------------------------------------===//
194314
// MARK: Object Diagnostics
195315
//===----------------------------------------------------------------------===//

lib/SILOptimizer/Mandatory/MoveOnlyDiagnostics.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,12 @@ class DiagnosticEmitter {
119119
/// way to file a bug.
120120
void emitCheckedMissedCopyError(SILInstruction *copyInst);
121121

122+
/// Assuming the given instruction represents the implicit destruction of
123+
/// 'self', emits an error saying that you needed to explicitly 'consume self'
124+
/// here because you're in a discarding context.
125+
void emitMissingConsumeInDiscardingContext(SILInstruction *leftoverDestroy,
126+
SILInstruction *dropDeinit);
127+
122128
void emitCheckerDoesntUnderstandDiagnostic(MarkMustCheckInst *markedValue);
123129
void emitObjectGuaranteedDiagnostic(MarkMustCheckInst *markedValue);
124130
void emitObjectOwnedDiagnostic(MarkMustCheckInst *markedValue);

lib/SILOptimizer/Mandatory/MoveOnlyObjectCheckerUtils.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,20 @@ struct OSSACanonicalizer {
116116
return !isa<PartialApplyInst>(user);
117117
});
118118
}
119+
120+
struct DropDeinitFilter {
121+
bool operator()(SILInstruction *inst) const {
122+
return isa<DropDeinitInst>(inst);
123+
}
124+
};
125+
using DropDeinitIter =
126+
llvm::filter_iterator<SILInstruction *const *, DropDeinitFilter>;
127+
using DropDeinitRange = iterator_range<DropDeinitIter>;
128+
129+
/// Returns a range of final uses of the mark_must_check that are drop_deinit
130+
DropDeinitRange getDropDeinitUses() const {
131+
return llvm::make_filter_range(consumingBoundaryUsers, DropDeinitFilter());
132+
}
119133
};
120134

121135
/// Search for candidate object mark_must_checks. If we find one that does not

test/SILGen/discard.swift

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ func invokedDeinit() {}
6868
consuming func tryDestroy(doDiscard: Bool) throws {
6969
if doDiscard {
7070
discard self
71+
} else {
72+
_ = consume self
7173
}
7274
throw E.err
7375
}
@@ -136,17 +138,19 @@ final class Wallet {
136138

137139
consuming func changeTicket(inWallet wallet: Wallet? = nil) {
138140
if let existingWallet = wallet {
139-
discard self
140141
self = .within(existingWallet)
142+
_ = consume self
143+
} else {
144+
discard self
141145
}
142146
}
143-
// As of now, we allow reinitialization after discard. Not sure if this is intended.
147+
144148
// CHECK-LABEL: sil hidden [ossa] @$s4test6TicketO06changeB08inWalletyAA0E0CSg_tF : $@convention(method) (@guaranteed Optional<Wallet>, @owned Ticket) -> () {
145149
// CHECK: [[SELF_REF:%.*]] = project_box [[SELF_BOX:%.*]] : ${ var Ticket }, 0
146-
// CHECK: switch_enum {{.*}} : $Optional<Wallet>, case #Optional.some!enumelt: [[HAVE_WALLET_BB:bb.*]], case #Optional.none!enumelt: {{.*}}
150+
// CHECK: switch_enum {{.*}} : $Optional<Wallet>, case #Optional.some!enumelt: {{.*}}, case #Optional.none!enumelt: [[NO_WALLET_BB:bb[0-9]+]]
147151
//
148152
// >> now we begin the destruction sequence, which involves pattern matching on self to destroy its innards
149-
// CHECK: [[HAVE_WALLET_BB]]({{%.*}} : @owned $Wallet):
153+
// CHECK: [[NO_WALLET_BB]]
150154
// CHECK: [[SELF_ACCESS:%.*]] = begin_access [read] [unknown] {{%.*}} : $*Ticket
151155
// CHECK: [[SELF_MMC:%.*]] = mark_must_check [no_consume_or_assign] [[SELF_ACCESS]]
152156
// CHECK: [[SELF_COPY:%.*]] = load [copy] [[SELF_MMC]] : $*Ticket
@@ -157,13 +161,7 @@ final class Wallet {
157161
// CHECK: [[TICKET_WITHIN]]([[PREV_SELF_WALLET:%.*]] : @owned $Wallet):
158162
// CHECK: destroy_value [[PREV_SELF_WALLET]] : $Wallet
159163
// CHECK: br [[JOIN_POINT]]
160-
// >> from here on we are reinitializing self.
161164
// CHECK: [[JOIN_POINT]]:
162-
// CHECK: [[NEW_SELF_VAL:%.*]] = enum $Ticket, #Ticket.within!enumelt, {{.*}} : $Wallet
163-
// CHECK: [[SELF_ACCESS2:%.*]] = begin_access [modify] [unknown] [[SELF_REF]] : $*Ticket
164-
// CHECK: [[SELF_MMC2:%.*]] = mark_must_check [assignable_but_not_consumable] [[SELF_ACCESS2]] : $*Ticket
165-
// CHECK: assign [[NEW_SELF_VAL]] to [[SELF_MMC2]] : $*Ticket
166-
// CHECK: end_access [[SELF_ACCESS2]] : $*Ticket
167165

168166
deinit {
169167
print("destroying ticket")

0 commit comments

Comments
 (0)