Skip to content

Commit d39e50d

Browse files
committed
[wip] 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
1 parent 381f292 commit d39e50d

File tree

5 files changed

+139
-2
lines changed

5 files changed

+139
-2
lines changed

include/swift/AST/DiagnosticsSIL.def

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

783+
NOTE(sil_movechecking_discard_self_here, none,
784+
"discarded self here", ())
781785
NOTE(sil_movechecking_partial_consume_here, none,
782786
"partially consumed here", ())
783787
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: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,49 @@ void DiagnosticEmitter::emitCheckedMissedCopyError(SILInstruction *copyInst) {
190190
diag::sil_movechecking_bug_missed_copy);
191191
}
192192

193+
void DiagnosticEmitter::emitMissingConsumeInDiscardingContext(
194+
SILInstruction *leftoverDestroy,
195+
SILInstruction *discard) {
196+
assert(isa<DropDeinitInst>(discard));
197+
198+
// An instruction corresponding to the logical place where the value is
199+
// destroyed. Ideally an exit point of the function reachable from here.
200+
// If for some reason we can't find an exit, then just use the original.
201+
SILInstruction *logicalDestroyLocation = leftoverDestroy;
202+
203+
// Search for a function exit reachable from this destroy. We do this because
204+
// the move checker may have injected or hoisted an existing destroy from leaf
205+
// blocks to some earlier point. For example, if 'd' represents a destroy of
206+
// self, then we may have this CFG:
207+
//
208+
// before: after:
209+
// . d
210+
// / \ / \
211+
// d d . .
212+
//
213+
BasicBlockWorklist worklist(logicalDestroyLocation->getFunction());
214+
worklist.push(logicalDestroyLocation->getParent());
215+
while (auto *bb = worklist.pop()) {
216+
TermInst *term = bb->getTerminator();
217+
218+
// Looking for a block that exits the function or terminates the program
219+
if (term->getNumSuccessors() == 0) {
220+
logicalDestroyLocation = term;
221+
break;
222+
}
223+
224+
for (auto *nextBB : term->getSuccessorBlocks())
225+
worklist.pushIfNotVisited(nextBB);
226+
}
227+
228+
diagnose(leftoverDestroy->getFunction()->getASTContext(),
229+
logicalDestroyLocation,
230+
diag::sil_movechecking_discard_missing_consume_self);
231+
232+
diagnose(discard->getFunction()->getASTContext(), discard,
233+
diag::sil_movechecking_discard_self_here);
234+
}
235+
193236
//===----------------------------------------------------------------------===//
194237
// MARK: Object Diagnostics
195238
//===----------------------------------------------------------------------===//

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
@@ -117,6 +117,20 @@ struct OSSACanonicalizer {
117117
return !isa<PartialApplyInst>(user);
118118
});
119119
}
120+
121+
struct DropDeinitFilter {
122+
bool operator()(SILInstruction *inst) const {
123+
return isa<DropDeinitInst>(inst);
124+
}
125+
};
126+
using DropDeinitIter =
127+
llvm::filter_iterator<SILInstruction *const *, DropDeinitFilter>;
128+
using DropDeinitRange = iterator_range<DropDeinitIter>;
129+
130+
/// Returns a range of final uses of the mark_must_check that are drop_deinit
131+
DropDeinitRange getDropDeinitUses() const {
132+
return llvm::make_filter_range(consumingBoundaryUsers, DropDeinitFilter());
133+
}
120134
};
121135

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

0 commit comments

Comments
 (0)