Skip to content

Commit d23858f

Browse files
committed
inject hop_to_executor in async actor inits
Because `self` can be used unrestricted after it is fully-initialized in an async actor init, we perform a hop_to_executor(self) immediately after `self` is fully-initialized on all paths within the initializer. If we did not, then code could race with the initializer if it, e.g., spawns a task from the initializer and mutates actor state that the initializer then reads. By hopping to the executor, we prevent that task from running until _after_ the initializer completes.
1 parent 70b7b34 commit d23858f

File tree

3 files changed

+178
-14
lines changed

3 files changed

+178
-14
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: 176 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,21 @@ namespace {
451451
void doIt();
452452

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

455470
void emitSelfConsumedDiagnostic(SILInstruction *Inst);
456471

@@ -476,7 +491,6 @@ namespace {
476491
bool *FailedSelfUse = nullptr,
477492
bool *FullyUninitialized = nullptr);
478493

479-
480494
void handleStoreUse(unsigned UseID);
481495
void handleLoadUse(const DIMemoryUse &Use);
482496
void handleLoadForTypeOfSelfUse(DIMemoryUse &Use);
@@ -487,12 +501,11 @@ namespace {
487501
bool diagnoseReturnWithoutInitializingStoredProperties(
488502
const SILInstruction *Inst, SILLocation loc, const DIMemoryUse &Use);
489503

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

497510
void reportIllegalUseForActorInit(const DIMemoryUse &Use,
498511
ActorInitKind ActorKind,
@@ -783,6 +796,155 @@ void LifetimeChecker::diagnoseInitError(const DIMemoryUse &Use,
783796
diagnose(Module, TheMemory.getLoc(), diag::variable_defined_here, isLet);
784797
}
785798

799+
/// Injects a hop_to_executor instruction after the specified insertion point.
800+
static void injectHopToExecutorAfter(SILLocation loc,
801+
SILBasicBlock::iterator insertPt,
802+
SILValue actor, bool needsBorrow = true) {
803+
804+
LLVM_DEBUG(llvm::dbgs() << "hop-injector: inserting hop after " << *insertPt);
805+
806+
// While insertAfter can handle terminators, it cannot handle ones that lead
807+
// to a block with multiple predecessors. I don't expect that a terminator
808+
// could initialize a stored property at all: a try_apply passed the property
809+
// as an inout would not be a valid use until _after_ the property has been
810+
// initialized.
811+
assert(!isa<TermInst>(*insertPt) && "unexpected hop-inject after terminator");
812+
813+
SILBuilderWithScope::insertAfter(&*insertPt, [&](SILBuilder &b) {
814+
if (needsBorrow)
815+
actor = b.createBeginBorrow(loc, actor);
816+
817+
b.createHopToExecutor(loc, actor, /*mandatory=*/false);
818+
819+
if (needsBorrow)
820+
b.createEndBorrow(loc, actor);
821+
});
822+
}
823+
824+
void LifetimeChecker::injectActorHopForBlock(
825+
SILLocation loc, SILBasicBlock *block,
826+
AvailabilitySet const &liveInAvailability) {
827+
// Tracks status of each element of TheMemory as we scan through the block,
828+
// starting with the initial availability at the block's entry-point.
829+
AvailabilitySet localAvail = liveInAvailability;
830+
831+
auto bbi = block->begin(); // our cursor and eventual insertion-point.
832+
const auto bbe = block->end();
833+
for (; bbi != bbe; ++bbi) {
834+
auto *inst = &*bbi;
835+
836+
auto result = NonLoadUses.find(inst);
837+
if (result == NonLoadUses.end())
838+
continue; // not a possible store
839+
840+
// Mark the tuple elements involved in this use as defined.
841+
for (unsigned use : result->second) {
842+
auto const &instUse = Uses[use];
843+
for (unsigned i = instUse.FirstElement;
844+
i < instUse.FirstElement + instUse.NumElements; ++i)
845+
localAvail.set(i, DIKind::Yes);
846+
}
847+
848+
// Stop if we found the instruction that initializes TheMemory.
849+
if (localAvail.isAllYes())
850+
break;
851+
}
852+
853+
// Make sure we found the initializing use of TheMemory.
854+
assert(bbi != bbe && "this block is not initializing?");
855+
856+
injectHopToExecutorAfter(loc, bbi, TheMemory.getUninitializedValue());
857+
}
858+
859+
void LifetimeChecker::injectActorHops() {
860+
auto ctor = TheMemory.getActorInitSelf();
861+
862+
// Must be `self` within an actor's designated initializer.
863+
if (!ctor)
864+
return;
865+
866+
// If the initializer has restricted use of `self`, then no hops are needed.
867+
if (isRestrictedActorInitSelf())
868+
return;
869+
870+
// Even if there are no stored properties to initialize, we still need a hop.
871+
// We insert this directly after the mark_uninitialized instruction, so
872+
// that it happens as early as `self` is available.`
873+
if (TheMemory.getNumElements() == 0) {
874+
auto *selfDef = TheMemory.getUninitializedValue();
875+
return injectHopToExecutorAfter(ctor, selfDef->getIterator(), selfDef);
876+
}
877+
878+
// Returns true iff a block returns normally from the initializer,
879+
// which means that it returns `self` in some way (perhaps optional-wrapped).
880+
auto returnsSelf = [](SILBasicBlock &block) -> bool {
881+
// This check relies on the fact that failable initializers are emitted by
882+
// SILGen to perform their return in a fresh block with either:
883+
// 1. No non-load uses of `self` (e.g., failing case)
884+
// 2. An all-Yes in-availability. (e.g., success case)
885+
return block.getTerminator()->getTermKind() == TermKind::ReturnInst;
886+
};
887+
888+
/////
889+
// Step 1: Find initializing blocks, which are blocks that contain a store
890+
// to TheMemory that fully-initializes it, and build the Map.
891+
892+
// We determine whether a block is "initializing" by inspecting the "in" and
893+
// "out" availability sets of the block. If the block goes from No / Partial
894+
// "in" to Yes "out", then some instruction in the block caused TheMemory to
895+
// become fully-initialized, so we record that block and its in-availability
896+
// to scan the block more precisely later in the next Step.
897+
for (auto &block : F) {
898+
auto &info = getBlockInfo(&block);
899+
900+
if (!info.HasNonLoadUse) {
901+
LLVM_DEBUG(llvm::dbgs()
902+
<< "hop-injector: rejecting bb" << block.getDebugID()
903+
<< " b/c no non-load uses.\n");
904+
continue; // could not be an initializing block.
905+
}
906+
907+
// Determine if this `block` is initializing, that is:
908+
//
909+
// InAvailability ≡ merge(OutAvailability(predecessors(block)))
910+
// ≠ Yes
911+
// AND
912+
// OutAvailability(block) = Yes OR returnsSelf(block)
913+
//
914+
// A block with no predecessors has in-avail of non-Yes.
915+
// A block with no successors has an out-avail of non-Yes, since
916+
// availability is not computed for it.
917+
918+
auto outSet = info.OutAvailability;
919+
if (!outSet.isAllYes() && !returnsSelf(block)) {
920+
LLVM_DEBUG(llvm::dbgs()
921+
<< "hop-injector: rejecting bb" << block.getDebugID()
922+
<< " b/c non-Yes OUT avail\n");
923+
continue; // then this block never sees TheMemory initialized.
924+
}
925+
926+
AvailabilitySet inSet(outSet.size());
927+
auto const &predecessors = block.getPredecessorBlocks();
928+
for (const auto &pred : predecessors)
929+
inSet.mergeIn(getBlockInfo(pred).OutAvailability);
930+
931+
if (inSet.isAllYes()) {
932+
LLVM_DEBUG(llvm::dbgs()
933+
<< "hop-injector: rejecting bb" << block.getDebugID()
934+
<< " b/c all-Yes IN avail\n");
935+
continue; // then this block always sees TheMemory initialized.
936+
}
937+
938+
LLVM_DEBUG(llvm::dbgs() << "hop-injector: bb" << block.getDebugID()
939+
<< " is initializing block with in-availability: "
940+
<< inSet << "\n");
941+
942+
// Step 2: Scan the initializing block to find the first non-load use that
943+
// fully-initializes TheMemory, and insert the hop there.
944+
injectActorHopForBlock(ctor, &block, inSet);
945+
}
946+
}
947+
786948
void LifetimeChecker::doIt() {
787949
// With any escapes tallied up, we can work through all the uses, checking
788950
// for definitive initialization, promoting loads, rewriting assigns, and
@@ -851,6 +1013,9 @@ void LifetimeChecker::doIt() {
8511013
// If we emitted an error, there is no reason to proceed with load promotion.
8521014
if (!EmittedErrorLocs.empty()) return;
8531015

1016+
// Insert hop_to_executor instructions for actor initializers, if needed.
1017+
injectActorHops();
1018+
8541019
// If the memory object has nontrivial type, then any destroy/release of the
8551020
// memory object will destruct the memory. If the memory (or some element
8561021
// thereof) is not initialized on some path, the bad things happen. Process
@@ -893,7 +1058,7 @@ void LifetimeChecker::handleLoadUse(const DIMemoryUse &Use) {
8931058
if (!isInitializedAtUse(Use, &IsSuperInitComplete, &FailedSelfUse))
8941059
return handleLoadUseFailure(Use, IsSuperInitComplete, FailedSelfUse);
8951060
// Check if it involves 'self' in a restricted actor init.
896-
if (isRestrictedActorInitSelf(Use, &ActorKind))
1061+
if (isRestrictedActorInitSelf(&ActorKind))
8971062
return handleLoadUseFailureForActorInit(Use, ActorKind);
8981063
}
8991064

@@ -1223,7 +1388,7 @@ void LifetimeChecker::handleInOutUse(const DIMemoryUse &Use) {
12231388
}
12241389

12251390
// 'self' cannot be passed 'inout' from some kinds of actor initializers.
1226-
if (isRestrictedActorInitSelf(Use, &ActorKind))
1391+
if (isRestrictedActorInitSelf(&ActorKind))
12271392
reportIllegalUseForActorInit(Use, ActorKind, "be passed 'inout'",
12281393
/*suggestConvenienceInit=*/false);
12291394

@@ -1399,7 +1564,7 @@ void LifetimeChecker::handleEscapeUse(const DIMemoryUse &Use) {
13991564
&FullyUninitialized)) {
14001565

14011566
// no escaping uses of 'self' are allowed in restricted actor inits.
1402-
if (isRestrictedActorInitSelf(Use, &ActorKind))
1567+
if (isRestrictedActorInitSelf(&ActorKind))
14031568
reportIllegalUseForActorInit(Use, ActorKind, "be captured by a closure",
14041569
/*suggestConvenienceInit=*/true);
14051570

@@ -1786,18 +1951,17 @@ bool LifetimeChecker::diagnoseReturnWithoutInitializingStoredProperties(
17861951
return true;
17871952
}
17881953

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

17921956
auto result = [&](ActorInitKind k, bool isRestricted) -> bool {
1793-
if (Kind)
1794-
*Kind = k;
1957+
if (kind)
1958+
*kind = k;
17951959
return isRestricted;
17961960
};
17971961

17981962
// Currently: being synchronous, or global-actor isolated, means the actor's
17991963
// self is restricted within the init.
1800-
if (auto *ctor = TheMemory.isActorInitSelf()) {
1964+
if (auto *ctor = TheMemory.getActorInitSelf()) {
18011965
if (getActorIsolation(ctor).isGlobalActor()) // global-actor isolated?
18021966
return result(ActorInitKind::GlobalActorIsolated, true);
18031967
else if (!ctor->hasAsync()) // synchronous?

0 commit comments

Comments
 (0)