Skip to content

Commit 22dd46f

Browse files
authored
Merge pull request #38490 from kavon/5.5-flowsensitive-init-hop
[5.5] Inject `hop_to_executor` after `self` is fully initialized in unrestricted actor inits
2 parents 1a29c96 + 887e9f0 commit 22dd46f

File tree

7 files changed

+582
-19
lines changed

7 files changed

+582
-19
lines changed

lib/SILOptimizer/Mandatory/DIMemoryUseCollector.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@ bool DIMemoryObjectInfo::isElementLetProperty(unsigned Element) const {
432432
return false;
433433
}
434434

435-
ConstructorDecl *DIMemoryObjectInfo::isActorInitSelf() const {
435+
ConstructorDecl *DIMemoryObjectInfo::getActorInitSelf() const {
436436
// is it 'self'?
437437
if (!MemoryInst->isVar())
438438
if (auto decl =

lib/SILOptimizer/Mandatory/DIMemoryUseCollector.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ class DIMemoryObjectInfo {
166166

167167
/// Returns the initializer if the memory use is 'self' and appears in an
168168
/// actor's designated initializer. Otherwise, returns nullptr.
169-
ConstructorDecl *isActorInitSelf() const;
169+
ConstructorDecl *getActorInitSelf() const;
170170

171171
/// True if this memory object is the 'self' of a derived class initializer.
172172
bool isDerivedClassSelf() const { return MemoryInst->isDerivedClassSelf(); }

lib/SILOptimizer/Mandatory/DefiniteInitialization.cpp

Lines changed: 220 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@
1717
#include "swift/AST/Expr.h"
1818
#include "swift/AST/Stmt.h"
1919
#include "swift/ClangImporter/ClangModule.h"
20-
#include "swift/SIL/BasicBlockData.h"
2120
#include "swift/SIL/BasicBlockBits.h"
22-
#include "swift/SIL/SILValue.h"
21+
#include "swift/SIL/BasicBlockData.h"
2322
#include "swift/SIL/InstructionUtils.h"
23+
#include "swift/SIL/MemAccessUtils.h"
2424
#include "swift/SIL/SILArgument.h"
2525
#include "swift/SIL/SILBuilder.h"
26+
#include "swift/SIL/SILValue.h"
2627
#include "swift/SILOptimizer/PassManager/Passes.h"
2728
#include "swift/SILOptimizer/PassManager/Transforms.h"
2829
#include "swift/SILOptimizer/Utils/CFGOptUtils.h"
@@ -451,6 +452,21 @@ namespace {
451452
void doIt();
452453

453454
private:
455+
/// Injects `hop_to_executor` instructions into the function after
456+
/// `self` becomes fully initialized, only if the current function
457+
/// is an actor initializer that requires this, and if TheMemory
458+
/// corresponds to `self`.
459+
void injectActorHops();
460+
461+
/// Given an initializing block and the live-in availability of TheMemory,
462+
/// this function injects a `hop_to_executor` instruction soon after the
463+
/// first non-load use of TheMemory that fully-initializes it.
464+
/// An "initializing block" is one that definitely contains a such a
465+
/// non-load use, e.g., because its live-in set is *not* all-Yes, but its
466+
/// live-out set is all-Yes.
467+
void injectActorHopForBlock(SILLocation loc,
468+
SILBasicBlock *initializingBlock,
469+
AvailabilitySet const &liveInAvailability);
454470

455471
void emitSelfConsumedDiagnostic(SILInstruction *Inst);
456472

@@ -476,7 +492,6 @@ namespace {
476492
bool *FailedSelfUse = nullptr,
477493
bool *FullyUninitialized = nullptr);
478494

479-
480495
void handleStoreUse(unsigned UseID);
481496
void handleLoadUse(const DIMemoryUse &Use);
482497
void handleLoadForTypeOfSelfUse(DIMemoryUse &Use);
@@ -487,12 +502,11 @@ namespace {
487502
bool diagnoseReturnWithoutInitializingStoredProperties(
488503
const SILInstruction *Inst, SILLocation loc, const DIMemoryUse &Use);
489504

490-
/// Returns true iff the use involves 'self' in a restricted kind of
505+
/// Returns true iff TheMemory involves 'self' in a restricted kind of
491506
/// actor initializer. If a non-null Kind pointer was passed in,
492507
/// then the specific kind of restricted actor initializer will be
493508
/// written out. Otherwise, the None initializer kind will be written out.
494-
bool isRestrictedActorInitSelf(const DIMemoryUse& Use,
495-
ActorInitKind *Kind = nullptr) const;
509+
bool isRestrictedActorInitSelf(ActorInitKind *kind = nullptr) const;
496510

497511
void reportIllegalUseForActorInit(const DIMemoryUse &Use,
498512
ActorInitKind ActorKind,
@@ -783,6 +797,196 @@ void LifetimeChecker::diagnoseInitError(const DIMemoryUse &Use,
783797
diagnose(Module, TheMemory.getLoc(), diag::variable_defined_here, isLet);
784798
}
785799

800+
/// Injects a hop_to_executor instruction after the specified insertion point.
801+
static void injectHopToExecutorAfter(SILLocation loc,
802+
SILBasicBlock::iterator insertPt,
803+
SILValue actor, bool needsBorrow = true) {
804+
805+
LLVM_DEBUG(llvm::dbgs() << "hop-injector: requested insertion after "
806+
<< *insertPt);
807+
808+
// While insertAfter can handle terminators, it cannot handle ones that lead
809+
// to a block with multiple predecessors. I don't expect that a terminator
810+
// could initialize a stored property at all: a try_apply passed the property
811+
// as an inout would not be a valid use until _after_ the property has been
812+
// initialized.
813+
assert(!isa<TermInst>(*insertPt) && "unexpected hop-inject after terminator");
814+
815+
auto injectAfter = [&](SILInstruction *insertPt) -> void {
816+
LLVM_DEBUG(llvm::dbgs() << "hop-injector: injecting after " << *insertPt);
817+
SILBuilderWithScope::insertAfter(insertPt, [&](SILBuilder &b) {
818+
if (needsBorrow)
819+
actor = b.createBeginBorrow(loc, actor);
820+
821+
b.createHopToExecutor(loc, actor);
822+
823+
if (needsBorrow)
824+
b.createEndBorrow(loc, actor);
825+
});
826+
};
827+
828+
//////
829+
// NOTE: We prefer to inject a hop outside of any access regions, so that
830+
// the dynamic access-set is empty. This is a best-effort to avoid injecting
831+
// it inside of a region, but does not account for overlapping accesses, etc.
832+
// But, I cannot think of a way to create an overlapping access with a stored
833+
// property when it is first initialized, because it's not valid to pass those
834+
// inout or capture them in a closure. - kavon
835+
836+
SILInstruction *cur = &*insertPt;
837+
BeginAccessInst *access = nullptr;
838+
839+
// Finds begin_access instructions that need hops placed after its end_access.
840+
auto getBeginAccess = [](SILValue v) -> BeginAccessInst * {
841+
return dyn_cast<BeginAccessInst>(getAccessScope(v));
842+
};
843+
844+
// If this insertion-point is after a store-like instruction, look for a
845+
// begin_access corresponding to the destination.
846+
if (auto *store = dyn_cast<StoreInst>(cur)) {
847+
access = getBeginAccess(store->getDest());
848+
} else if (auto *assign = dyn_cast<AssignInst>(cur)) {
849+
access = getBeginAccess(assign->getDest());
850+
}
851+
852+
// If we found a begin_access, then we need to inject the hop after
853+
// all of the corresponding end_accesses.
854+
if (access) {
855+
for (auto *endAccess : access->getEndAccesses())
856+
injectAfter(endAccess);
857+
858+
return;
859+
}
860+
861+
//////
862+
// Otherwise, we just put the hop after the original insertion point.
863+
return injectAfter(cur);
864+
}
865+
866+
void LifetimeChecker::injectActorHopForBlock(
867+
SILLocation loc, SILBasicBlock *block,
868+
AvailabilitySet const &liveInAvailability) {
869+
// Tracks status of each element of TheMemory as we scan through the block,
870+
// starting with the initial availability at the block's entry-point.
871+
AvailabilitySet localAvail = liveInAvailability;
872+
873+
auto bbi = block->begin(); // our cursor and eventual insertion-point.
874+
const auto bbe = block->end();
875+
for (; bbi != bbe; ++bbi) {
876+
auto *inst = &*bbi;
877+
878+
auto result = NonLoadUses.find(inst);
879+
if (result == NonLoadUses.end())
880+
continue; // not a possible store
881+
882+
// Mark the tuple elements involved in this use as defined.
883+
for (unsigned use : result->second) {
884+
auto const &instUse = Uses[use];
885+
for (unsigned i = instUse.FirstElement;
886+
i < instUse.FirstElement + instUse.NumElements; ++i)
887+
localAvail.set(i, DIKind::Yes);
888+
}
889+
890+
// Stop if we found the instruction that initializes TheMemory.
891+
if (localAvail.isAllYes())
892+
break;
893+
}
894+
895+
// Make sure we found the initializing use of TheMemory.
896+
assert(bbi != bbe && "this block is not initializing?");
897+
898+
injectHopToExecutorAfter(loc, bbi, TheMemory.getUninitializedValue());
899+
}
900+
901+
void LifetimeChecker::injectActorHops() {
902+
auto ctor = TheMemory.getActorInitSelf();
903+
904+
// Must be `self` within an actor's designated initializer.
905+
if (!ctor)
906+
return;
907+
908+
// If the initializer has restricted use of `self`, then no hops are needed.
909+
if (isRestrictedActorInitSelf())
910+
return;
911+
912+
// Even if there are no stored properties to initialize, we still need a hop.
913+
// We insert this directly after the mark_uninitialized instruction, so
914+
// that it happens as early as `self` is available.`
915+
if (TheMemory.getNumElements() == 0) {
916+
auto *selfDef = TheMemory.getUninitializedValue();
917+
return injectHopToExecutorAfter(ctor, selfDef->getIterator(), selfDef);
918+
}
919+
920+
// Returns true iff a block returns normally from the initializer,
921+
// which means that it returns `self` in some way (perhaps optional-wrapped).
922+
auto returnsSelf = [](SILBasicBlock &block) -> bool {
923+
// This check relies on the fact that failable initializers are emitted by
924+
// SILGen to perform their return in a fresh block with either:
925+
// 1. No non-load uses of `self` (e.g., failing case)
926+
// 2. An all-Yes in-availability. (e.g., success case)
927+
return block.getTerminator()->getTermKind() == TermKind::ReturnInst;
928+
};
929+
930+
/////
931+
// Step 1: Find initializing blocks, which are blocks that contain a store
932+
// to TheMemory that fully-initializes it, and build the Map.
933+
934+
// We determine whether a block is "initializing" by inspecting the "in" and
935+
// "out" availability sets of the block. If the block goes from No / Partial
936+
// "in" to Yes "out", then some instruction in the block caused TheMemory to
937+
// become fully-initialized, so we record that block and its in-availability
938+
// to scan the block more precisely later in the next Step.
939+
for (auto &block : F) {
940+
auto &info = getBlockInfo(&block);
941+
942+
if (!info.HasNonLoadUse) {
943+
LLVM_DEBUG(llvm::dbgs()
944+
<< "hop-injector: rejecting bb" << block.getDebugID()
945+
<< " b/c no non-load uses.\n");
946+
continue; // could not be an initializing block.
947+
}
948+
949+
// Determine if this `block` is initializing, that is:
950+
//
951+
// InAvailability ≡ merge(OutAvailability(predecessors(block)))
952+
// ≠ Yes
953+
// AND
954+
// OutAvailability(block) = Yes OR returnsSelf(block)
955+
//
956+
// A block with no predecessors has in-avail of non-Yes.
957+
// A block with no successors has an out-avail of non-Yes, since
958+
// availability is not computed for it.
959+
960+
auto outSet = info.OutAvailability;
961+
if (!outSet.isAllYes() && !returnsSelf(block)) {
962+
LLVM_DEBUG(llvm::dbgs()
963+
<< "hop-injector: rejecting bb" << block.getDebugID()
964+
<< " b/c non-Yes OUT avail\n");
965+
continue; // then this block never sees TheMemory initialized.
966+
}
967+
968+
AvailabilitySet inSet(outSet.size());
969+
auto const &predecessors = block.getPredecessorBlocks();
970+
for (auto *pred : predecessors)
971+
inSet.mergeIn(getBlockInfo(pred).OutAvailability);
972+
973+
if (inSet.isAllYes()) {
974+
LLVM_DEBUG(llvm::dbgs()
975+
<< "hop-injector: rejecting bb" << block.getDebugID()
976+
<< " b/c all-Yes IN avail\n");
977+
continue; // then this block always sees TheMemory initialized.
978+
}
979+
980+
LLVM_DEBUG(llvm::dbgs() << "hop-injector: bb" << block.getDebugID()
981+
<< " is initializing block with in-availability: "
982+
<< inSet << "\n");
983+
984+
// Step 2: Scan the initializing block to find the first non-load use that
985+
// fully-initializes TheMemory, and insert the hop there.
986+
injectActorHopForBlock(ctor, &block, inSet);
987+
}
988+
}
989+
786990
void LifetimeChecker::doIt() {
787991
// With any escapes tallied up, we can work through all the uses, checking
788992
// for definitive initialization, promoting loads, rewriting assigns, and
@@ -851,6 +1055,9 @@ void LifetimeChecker::doIt() {
8511055
// If we emitted an error, there is no reason to proceed with load promotion.
8521056
if (!EmittedErrorLocs.empty()) return;
8531057

1058+
// Insert hop_to_executor instructions for actor initializers, if needed.
1059+
injectActorHops();
1060+
8541061
// If the memory object has nontrivial type, then any destroy/release of the
8551062
// memory object will destruct the memory. If the memory (or some element
8561063
// thereof) is not initialized on some path, the bad things happen. Process
@@ -893,7 +1100,7 @@ void LifetimeChecker::handleLoadUse(const DIMemoryUse &Use) {
8931100
if (!isInitializedAtUse(Use, &IsSuperInitComplete, &FailedSelfUse))
8941101
return handleLoadUseFailure(Use, IsSuperInitComplete, FailedSelfUse);
8951102
// Check if it involves 'self' in a restricted actor init.
896-
if (isRestrictedActorInitSelf(Use, &ActorKind))
1103+
if (isRestrictedActorInitSelf(&ActorKind))
8971104
return handleLoadUseFailureForActorInit(Use, ActorKind);
8981105
}
8991106

@@ -1223,7 +1430,7 @@ void LifetimeChecker::handleInOutUse(const DIMemoryUse &Use) {
12231430
}
12241431

12251432
// 'self' cannot be passed 'inout' from some kinds of actor initializers.
1226-
if (isRestrictedActorInitSelf(Use, &ActorKind))
1433+
if (isRestrictedActorInitSelf(&ActorKind))
12271434
reportIllegalUseForActorInit(Use, ActorKind, "be passed 'inout'",
12281435
/*suggestConvenienceInit=*/false);
12291436

@@ -1399,7 +1606,7 @@ void LifetimeChecker::handleEscapeUse(const DIMemoryUse &Use) {
13991606
&FullyUninitialized)) {
14001607

14011608
// no escaping uses of 'self' are allowed in restricted actor inits.
1402-
if (isRestrictedActorInitSelf(Use, &ActorKind))
1609+
if (isRestrictedActorInitSelf(&ActorKind))
14031610
reportIllegalUseForActorInit(Use, ActorKind, "be captured by a closure",
14041611
/*suggestConvenienceInit=*/true);
14051612

@@ -1786,18 +1993,17 @@ bool LifetimeChecker::diagnoseReturnWithoutInitializingStoredProperties(
17861993
return true;
17871994
}
17881995

1789-
bool LifetimeChecker::isRestrictedActorInitSelf(const DIMemoryUse& Use,
1790-
ActorInitKind *Kind) const {
1996+
bool LifetimeChecker::isRestrictedActorInitSelf(ActorInitKind *kind) const {
17911997

17921998
auto result = [&](ActorInitKind k, bool isRestricted) -> bool {
1793-
if (Kind)
1794-
*Kind = k;
1999+
if (kind)
2000+
*kind = k;
17952001
return isRestricted;
17962002
};
17972003

17982004
// Currently: being synchronous, or global-actor isolated, means the actor's
17992005
// self is restricted within the init.
1800-
if (auto *ctor = TheMemory.isActorInitSelf()) {
2006+
if (auto *ctor = TheMemory.getActorInitSelf()) {
18012007
if (getActorIsolation(ctor).isGlobalActor()) // global-actor isolated?
18022008
return result(ActorInitKind::GlobalActorIsolated, true);
18032009
else if (!ctor->hasAsync()) // synchronous?

stdlib/public/runtime/Exclusivity.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ class AccessSet {
202202
}
203203

204204
void remove(Access *access) {
205+
assert(Head && "removal from empty AccessSet");
205206
auto cur = Head;
206207
// Fast path: stack discipline.
207208
if (cur == access) {

0 commit comments

Comments
 (0)