Skip to content

Commit 5441ff1

Browse files
committed
[MoveChecker] Distinguished scope end diagnostics.
There are several kinds of scopes at which it is required that an address be initialized: (1) the whole function -- for inout argument to the function (2) the region a coroutine is active -- for an inout yielded by a coroutine into the function (3) the region of a memory access -- for a `begin_access [modify]`. The move checker enforces that they are initialized at that point by adding instructions at which the field must be live to liveness. Previously, all such scopes used the end of the function as the point at which the memory had to have been reinitialized. Here, the relevant end of scope markers are used instead. More importantly, here the diagnostic is made to vary--the diagnostic, that is, that is issued in the face an address not being initialized at the end of these different kind of scopes.
1 parent 4ea2440 commit 5441ff1

File tree

6 files changed

+223
-52
lines changed

6 files changed

+223
-52
lines changed

include/swift/AST/DiagnosticsSIL.def

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -806,6 +806,12 @@ ERROR(sil_movechecking_capture_consumed, none,
806806
ERROR(sil_movechecking_not_reinitialized_before_end_of_function, none,
807807
"missing reinitialization of %select{inout parameter|closure capture}1 '%0' "
808808
"after consume", (StringRef, bool))
809+
ERROR(sil_movechecking_not_reinitialized_before_end_of_coroutine, none,
810+
"field '%0' was consumed but not reinitialized; "
811+
"the field must be reinitialized during the access", (StringRef))
812+
ERROR(sil_movechecking_not_reinitialized_before_end_of_access, none,
813+
"missing reinitialization of %select{inout parameter|closure capture}1 '%0' "
814+
"after consume while accessing memory", (StringRef, bool))
809815
ERROR(sil_movechecking_value_consumed_in_a_loop, none,
810816
"'%0' consumed in a loop", (StringRef))
811817
ERROR(sil_movechecking_use_after_partial_consume, none,

lib/SILOptimizer/Mandatory/MoveOnlyAddressCheckerUtils.cpp

Lines changed: 78 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -368,15 +368,39 @@ static bool isReinitToInitConvertibleInst(SILInstruction *memInst) {
368368
}
369369
}
370370

371-
/// Returns true if \p value a function argument from an inout argument or a
372-
/// value extracted from a closure captured box that we did not convert to an
373-
/// address.
371+
using ScopeRequiringFinalInit = DiagnosticEmitter::ScopeRequiringFinalInit;
372+
373+
/// If \p markedAddr's operand must be initialized at the end of the scope it
374+
/// introduces, visit those scope ending ends.
375+
///
376+
/// Examples:
377+
/// (1) inout function argument. Must be initialized at function exit.
378+
///
379+
/// sil [ossa] @f : $(inout MOV) -> ()
380+
/// entry(%addr : $*MOV):
381+
/// ...
382+
/// return %t : $() // %addr must be initialized here
383+
///
384+
/// (2) coroutine. Must be initialized at end_apply/abort_apply.
385+
///
386+
/// (%addr, %token) = begin_apply ... -> @yields @inout MOV
387+
/// bbN:
388+
/// end_apply %token // %addr must be initialized here
389+
/// bbM:
390+
/// abort_apply %token // %addr must be initialized here
391+
///
392+
/// (3) modify access. Must be initialized at end_access.
393+
///
394+
/// %addr = begin_access [modify] %location
374395
///
375-
/// These are cases where we want to treat the end of the function as a liveness
376-
/// use to ensure that we reinitialize \p value before the end of the function
377-
/// if we consume \p value in the function body.
378-
static bool isInOutDefThatNeedsEndOfFunctionLiveness(
379-
MarkUnresolvedNonCopyableValueInst *markedAddr) {
396+
/// end_access %addr // %addr must be initialized here
397+
///
398+
/// To enforce this requirement, function exiting instructions are treated as
399+
/// liveness uses of such addresses, ensuring that the address is initialized at
400+
/// that point.
401+
static bool visitScopeEndsRequiringInit(
402+
MarkUnresolvedNonCopyableValueInst *markedAddr,
403+
llvm::function_ref<void(SILInstruction *, ScopeRequiringFinalInit)> visit) {
380404
SILValue operand = markedAddr->getOperand();
381405

382406
// TODO: This should really be a property of the marker instruction.
@@ -403,17 +427,32 @@ static bool isInOutDefThatNeedsEndOfFunctionLiveness(
403427
case SILArgumentConvention::Indirect_InoutAliasable:
404428
case SILArgumentConvention::Pack_Inout:
405429
LLVM_DEBUG(llvm::dbgs() << "Found inout arg: " << *fArg);
430+
SmallVector<SILBasicBlock *, 8> exitBlocks;
431+
markedAddr->getFunction()->findExitingBlocks(exitBlocks);
432+
for (auto *block : exitBlocks) {
433+
visit(block->getTerminator(), ScopeRequiringFinalInit::InoutArgument);
434+
}
406435
return true;
407436
}
408437
}
409438
// Check for yields from a modify coroutine.
410439
if (auto bai =
411440
dyn_cast_or_null<BeginApplyInst>(operand->getDefiningInstruction())) {
441+
for (auto *inst : bai->getTokenResult()->getUsers()) {
442+
assert(isa<EndApplyInst>(inst) || isa<AbortApplyInst>(inst));
443+
visit(inst, ScopeRequiringFinalInit::Coroutine);
444+
}
412445
return true;
413446
}
414447
// Check for modify accesses.
415448
if (auto access = dyn_cast<BeginAccessInst>(operand)) {
416-
return access->getAccessKind() == SILAccessKind::Modify;
449+
if (access->getAccessKind() != SILAccessKind::Modify) {
450+
return false;
451+
}
452+
for (auto *inst : access->getEndAccesses()) {
453+
visit(inst, ScopeRequiringFinalInit::ModifyMemoryAccess);
454+
}
455+
return true;
417456
}
418457

419458
return false;
@@ -534,12 +573,15 @@ struct UseState {
534573
/// The set of drop_deinits of this mark_unresolved_non_copyable_value
535574
llvm::SmallSetVector<SILInstruction *, 2> dropDeinitInsts;
536575

537-
/// A "inout terminator use" is an implicit liveness use of the entire value
538-
/// placed on a terminator. We use this both so we add liveness for the
539-
/// terminator user and so that we can use the set to quickly identify later
540-
/// while emitting diagnostics that a liveness use is a terminator user and
541-
/// emit a specific diagnostic message.
542-
llvm::SmallSetVector<SILInstruction *, 2> implicitEndOfLifetimeLivenessUses;
576+
/// Instructions indicating the end of a scope at which addr must be
577+
/// initialized.
578+
///
579+
/// Adding such instructions to liveness forces the value to be initialized at
580+
/// them as required.
581+
///
582+
/// See visitScopeEndsRequiringInit.
583+
llvm::MapVector<SILInstruction *, ScopeRequiringFinalInit>
584+
scopeEndsRequiringInit;
543585

544586
/// We add debug_values to liveness late after we diagnose, but before we
545587
/// hoist destroys to ensure that we do not hoist destroys out of access
@@ -604,8 +646,13 @@ struct UseState {
604646
/// instruction.
605647
/// 2. In the case of a ref_element_addr or a global, this will contain the
606648
/// end_access.
607-
bool isImplicitEndOfLifetimeLivenessUses(SILInstruction *inst) const {
608-
return implicitEndOfLifetimeLivenessUses.count(inst);
649+
llvm::Optional<ScopeRequiringFinalInit>
650+
isImplicitEndOfLifetimeLivenessUses(SILInstruction *inst) const {
651+
auto iter = scopeEndsRequiringInit.find(inst);
652+
if (iter == scopeEndsRequiringInit.end()) {
653+
return llvm::None;
654+
}
655+
return {iter->second};
609656
}
610657

611658
/// Returns true if the given instruction is within the same block as a reinit
@@ -647,7 +694,7 @@ struct UseState {
647694
reinitInsts.clear();
648695
reinitToValueMultiMap.reset();
649696
dropDeinitInsts.clear();
650-
implicitEndOfLifetimeLivenessUses.clear();
697+
scopeEndsRequiringInit.clear();
651698
debugValue = nullptr;
652699
}
653700

@@ -686,8 +733,8 @@ struct UseState {
686733
llvm::dbgs() << *inst;
687734
}
688735
llvm::dbgs() << "Implicit End Of Lifetime Liveness Users:\n";
689-
for (auto *inst : implicitEndOfLifetimeLivenessUses) {
690-
llvm::dbgs() << *inst;
736+
for (auto pair : scopeEndsRequiringInit) {
737+
llvm::dbgs() << pair.first;
691738
}
692739
llvm::dbgs() << "Debug Value User:\n";
693740
if (debugValue) {
@@ -724,25 +771,20 @@ struct UseState {
724771
initializeLiveness(FieldSensitiveMultiDefPrunedLiveRange &prunedLiveness);
725772

726773
void initializeImplicitEndOfLifetimeLivenessUses() {
727-
if (isInOutDefThatNeedsEndOfFunctionLiveness(address)) {
728-
SmallVector<SILBasicBlock *, 8> exitBlocks;
729-
address->getFunction()->findExitingBlocks(exitBlocks);
730-
for (auto *block : exitBlocks) {
731-
LLVM_DEBUG(llvm::dbgs() << " Adding term as liveness user: "
732-
<< *block->getTerminator());
733-
implicitEndOfLifetimeLivenessUses.insert(block->getTerminator());
734-
}
735-
return;
736-
}
737-
774+
visitScopeEndsRequiringInit(address, [&](auto *inst, auto kind) {
775+
LLVM_DEBUG(llvm::dbgs()
776+
<< " Adding scope end as liveness user: " << *inst);
777+
scopeEndsRequiringInit[inst] = kind;
778+
});
738779
if (address->getCheckKind() == MarkUnresolvedNonCopyableValueInst::
739780
CheckKind::AssignableButNotConsumable) {
740781
if (auto *bai = dyn_cast<BeginAccessInst>(address->getOperand())) {
741782
for (auto *eai : bai->getEndAccesses()) {
742783
LLVM_DEBUG(llvm::dbgs() << " Adding end_access as implicit end of "
743784
"lifetime liveness user: "
744785
<< *eai);
745-
implicitEndOfLifetimeLivenessUses.insert(eai);
786+
scopeEndsRequiringInit[eai] =
787+
ScopeRequiringFinalInit::ModifyMemoryAccess;
746788
}
747789
}
748790
}
@@ -804,7 +846,7 @@ struct UseState {
804846
// An "inout terminator use" is an implicit liveness use of the entire
805847
// value. This is because we need to ensure that our inout value is
806848
// reinitialized along exit paths.
807-
if (implicitEndOfLifetimeLivenessUses.count(inst))
849+
if (scopeEndsRequiringInit.count(inst))
808850
return true;
809851

810852
return false;
@@ -1191,10 +1233,10 @@ void UseState::initializeLiveness(
11911233
// ref_element_addr or global_addr, add a liveness use of the entire value on
11921234
// the implicit end lifetime instruction. For inout this is terminators for
11931235
// ref_element_addr, global_addr it is the end_access instruction.
1194-
for (auto *inst : implicitEndOfLifetimeLivenessUses) {
1195-
liveness.updateForUse(inst, TypeTreeLeafTypeRange(address),
1236+
for (auto pair : scopeEndsRequiringInit) {
1237+
liveness.updateForUse(pair.first, TypeTreeLeafTypeRange(address),
11961238
false /*lifetime ending*/);
1197-
LLVM_DEBUG(llvm::dbgs() << "Added liveness for inoutTermUser: " << *inst;
1239+
LLVM_DEBUG(llvm::dbgs() << "Added liveness for scope end: " << pair.first;
11981240
liveness.print(llvm::dbgs()));
11991241
}
12001242

lib/SILOptimizer/Mandatory/MoveOnlyDiagnostics.cpp

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -669,7 +669,7 @@ void DiagnosticEmitter::emitAddressExclusivityHazardDiagnostic(
669669
void DiagnosticEmitter::emitAddressDiagnostic(
670670
MarkUnresolvedNonCopyableValueInst *markedValue,
671671
SILInstruction *lastLiveUser, SILInstruction *violatingUser,
672-
bool isUseConsuming, bool isInOutEndOfFunction) {
672+
bool isUseConsuming, llvm::Optional<ScopeRequiringFinalInit> scopeKind) {
673673
if (!useWithDiagnostic.insert(violatingUser).second)
674674
return;
675675
registerDiagnosticEmitted(markedValue);
@@ -695,12 +695,24 @@ void DiagnosticEmitter::emitAddressDiagnostic(
695695
return;
696696
}
697697

698-
if (isInOutEndOfFunction) {
699-
diagnose(
700-
astContext, markedValue,
701-
diag::
702-
sil_movechecking_not_reinitialized_before_end_of_function,
703-
varName, isClosureCapture(markedValue));
698+
if (scopeKind.has_value()) {
699+
switch (scopeKind.value()) {
700+
case ScopeRequiringFinalInit::InoutArgument:
701+
diagnose(astContext, markedValue,
702+
diag::sil_movechecking_not_reinitialized_before_end_of_function,
703+
varName, isClosureCapture(markedValue));
704+
break;
705+
case ScopeRequiringFinalInit::Coroutine:
706+
diagnose(astContext, markedValue,
707+
diag::sil_movechecking_not_reinitialized_before_end_of_coroutine,
708+
varName);
709+
break;
710+
case ScopeRequiringFinalInit::ModifyMemoryAccess:
711+
diagnose(astContext, markedValue,
712+
diag::sil_movechecking_not_reinitialized_before_end_of_access,
713+
varName, isClosureCapture(markedValue));
714+
break;
715+
}
704716
diagnose(astContext, violatingUser,
705717
diag::sil_movechecking_consuming_use_here);
706718
return;

lib/SILOptimizer/Mandatory/MoveOnlyDiagnostics.h

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,10 +144,29 @@ class DiagnosticEmitter {
144144
return valuesWithDiagnostics.count(markedValue);
145145
}
146146

147-
void emitAddressDiagnostic(MarkUnresolvedNonCopyableValueInst *markedValue,
148-
SILInstruction *lastLiveUse,
149-
SILInstruction *violatingUse, bool isUseConsuming,
150-
bool isInOutEndOfFunction = false);
147+
/// The kind of scope at the end of which an address must be initialized.
148+
enum class ScopeRequiringFinalInit {
149+
/// The scope for an inout argument.
150+
///
151+
/// The whole function.
152+
InoutArgument,
153+
/// The scope for an address yielded by a coroutine.
154+
///
155+
/// It begins at the begin_apply and ends at all corresponding
156+
/// end_apply/abort_apply instructions.
157+
Coroutine,
158+
/// The scope for an address done through an access scope marker.
159+
///
160+
/// It begins at the begin_access and ends at all corresponding end_access
161+
/// instructions.
162+
ModifyMemoryAccess,
163+
};
164+
165+
void emitAddressDiagnostic(
166+
MarkUnresolvedNonCopyableValueInst *markedValue,
167+
SILInstruction *lastLiveUse, SILInstruction *violatingUse,
168+
bool isUseConsuming,
169+
llvm::Optional<ScopeRequiringFinalInit> scopeKind = llvm::None);
151170
void emitInOutEndOfFunctionDiagnostic(
152171
MarkUnresolvedNonCopyableValueInst *markedValue,
153172
SILInstruction *violatingUse);
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// RUN: %empty-directory(%t)
2+
// RUN: split-file %s %t
3+
4+
// RUN: %target-swift-frontend \
5+
// RUN: %t/Library.swift \
6+
// RUN: -emit-module \
7+
// RUN: -enable-library-evolution \
8+
// RUN: -enable-experimental-feature MoveOnlyPartialConsumption \
9+
// RUN: -module-name Library \
10+
// RUN: -emit-module-path %t/Library.swiftmodule
11+
12+
// RUN: %target-swift-frontend \
13+
// RUN: %t/Client.swift \
14+
// RUN: -emit-sil -verify \
15+
// RUN: -debug-diagnostic-names \
16+
// RUN: -sil-verify-all \
17+
// RUN: -enable-experimental-feature MoveOnlyPartialConsumption \
18+
// RUN: -I %t
19+
20+
21+
//--- Library.swift
22+
23+
public struct Ur : ~Copyable {
24+
deinit {}
25+
}
26+
public func consume(_ ur: consuming Ur) {}
27+
public func borrow(_ ur: borrowing Ur) {}
28+
29+
@frozen
30+
public struct AggFrozen : ~Copyable {
31+
public var field1: Ur
32+
public var field2: Ur
33+
}
34+
public func consume(_ a: consuming AggFrozen) {}
35+
public func borrow(_ ur: borrowing AggFrozen) {}
36+
37+
@frozen
38+
public struct AggFrozenDeiniting : ~Copyable {
39+
public var field1: Ur
40+
public var field2: Ur
41+
deinit {}
42+
}
43+
public func consume(_ a: consuming AggFrozenDeiniting) {}
44+
public func borrow(_ ur: borrowing AggFrozenDeiniting) {}
45+
46+
public struct AggResilient : ~Copyable {
47+
public var field1: Ur
48+
public var field2: Ur
49+
}
50+
public func consume(_ a: consuming AggResilient) {}
51+
public func borrow(_ ur: borrowing AggResilient) {}
52+
53+
public struct AggResilientDeiniting : ~Copyable {
54+
public var field1: Ur
55+
public var field2: Ur
56+
deinit {}
57+
}
58+
public func consume(_ a: consuming AggResilientDeiniting) {}
59+
public func borrow(_ ur: borrowing AggResilientDeiniting) {}
60+
61+
//--- Client.swift
62+
63+
import Library
64+
65+
struct AggLocalDeiniting : ~Copyable {
66+
var field1: Ur
67+
var field2: Ur
68+
deinit {}
69+
}
70+
71+
struct AggLocal : ~Copyable {
72+
var field1: Ur
73+
var field2: Ur
74+
}
75+
76+
func consumeField1_AggFrozen(_ a: consuming AggFrozen) {
77+
consume(a.field1)
78+
}
79+
80+
func consumeField1_AggFrozenDeiniting(_ a: consuming AggFrozenDeiniting) {
81+
consume(a.field1) // expected-error{{cannot partially consume 'a' when it has a deinitializer}}
82+
}
83+
84+
func consumeField1_AggResilient(_ a: consuming AggResilient) {
85+
consume(a.field1) // expected-error{{field 'a.field1' was consumed but not reinitialized; the field must be reinitialized during the access}}
86+
// expected-note@-1{{consumed here}}
87+
}
88+
89+
func consumeField1_AggResilientDeiniting(_ a: consuming AggResilientDeiniting) {
90+
consume(a.field1) // expected-error{{field 'a.field1' was consumed but not reinitialized; the field must be reinitialized during the access}}
91+
// expected-note@-1{{consumed here}}
92+
}

0 commit comments

Comments
 (0)