Skip to content

Commit 48ca690

Browse files
committed
ClosureLifetimeFixup: support the startAsyncLet builtin.
When the closure of startAsyncLet is no-escaping, the captured values (= the partial_apply arguments) must be kept alive until the endAsyncLet builtin. ClosureLifetimeFixup adds the generated mark_dependence as a second operand to endAsyncLet, which keeps all the arguments alive until this point.
1 parent 444c35c commit 48ca690

File tree

5 files changed

+134
-74
lines changed

5 files changed

+134
-74
lines changed

include/swift/AST/Builtins.def

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -793,6 +793,9 @@ BUILTIN_MISC_OPERATION(StartAsyncLet, "startAsyncLet", "", Special)
793793
/// asyncLetEnd(): (Builtin.RawPointer) -> Void
794794
///
795795
/// Ends and destroys an async-let.
796+
/// The ClosureLifetimeFixup pass adds a second operand to the builtin to
797+
/// ensure that optimizations keep the stack-allocated closure arguments alive
798+
/// until the endAsyncLet.
796799
BUILTIN_MISC_OPERATION_WITH_SILGEN(EndAsyncLet, "endAsyncLet", "", Special)
797800

798801
/// createAsyncTaskFuture(): (

lib/IRGen/GenBuiltin.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,9 @@ void irgen::emitBuiltinCall(IRGenFunction &IGF, const BuiltinInfo &Builtin,
244244

245245
if (Builtin.ID == BuiltinValueKind::EndAsyncLet) {
246246
emitEndAsyncLet(IGF, args.claimNext());
247+
// Ignore a second operand which is inserted by ClosureLifetimeFixup and
248+
// only used for dependency tracking.
249+
(void)args.claimAll();
247250
return;
248251
}
249252

lib/SILOptimizer/Mandatory/ClosureLifetimeFixup.cpp

Lines changed: 113 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,61 @@ static SILValue insertMarkDependenceForCapturedArguments(PartialApplyInst *pai,
363363
return curr;
364364
}
365365

366+
/// Returns the (single) "endAsyncLet" builtin if \p startAsyncLet is a
367+
/// "startAsyncLet" builtin.
368+
static BuiltinInst *getEndAsyncLet(BuiltinInst *startAsyncLet) {
369+
if (startAsyncLet->getBuiltinKind() != BuiltinValueKind::StartAsyncLet)
370+
return nullptr;
371+
372+
BuiltinInst *endAsyncLet = nullptr;
373+
for (Operand *op : startAsyncLet->getUses()) {
374+
auto *endBI = dyn_cast<BuiltinInst>(op->getUser());
375+
if (endBI && endBI->getBuiltinKind() == BuiltinValueKind::EndAsyncLet) {
376+
// At this stage of the pipeline, it's always the case that a
377+
// startAsyncLet has an endAsyncLet: that's how SILGen generates it.
378+
// Just to be on the safe side, do this check.
379+
if (endAsyncLet)
380+
return nullptr;
381+
endAsyncLet = endBI;
382+
}
383+
}
384+
return endAsyncLet;
385+
}
386+
387+
/// Call the \p insertFn with a builder at all insertion points after
388+
/// a closure is used by \p closureUser.
389+
static void insertAfterClosureUser(SILInstruction *closureUser,
390+
function_ref<void(SILBuilder &)> insertFn) {
391+
// Don't insert any destroy or deallocation right before an unreachable.
392+
// It's not needed an will only add up to code size.
393+
auto insertAtNonUnreachable = [&](SILBuilder &builder) {
394+
if (isa<UnreachableInst>(builder.getInsertionPoint()))
395+
return;
396+
insertFn(builder);
397+
};
398+
399+
if (auto *startAsyncLet = dyn_cast<BuiltinInst>(closureUser)) {
400+
BuiltinInst *endAsyncLet = getEndAsyncLet(startAsyncLet);
401+
assert(endAsyncLet);
402+
SILBuilderWithScope builder(std::next(endAsyncLet->getIterator()));
403+
insertAtNonUnreachable(builder);
404+
return;
405+
}
406+
FullApplySite fas = FullApplySite::isa(closureUser);
407+
assert(fas);
408+
fas.insertAfterFullEvaluation(insertAtNonUnreachable);
409+
}
410+
411+
static SILValue skipConvert(SILValue v) {
412+
auto *cvt = dyn_cast<ConvertFunctionInst>(v);
413+
if (!cvt)
414+
return v;
415+
auto *pa = dyn_cast<PartialApplyInst>(cvt->getOperand());
416+
if (!pa || !pa->hasOneUse())
417+
return v;
418+
return pa;
419+
}
420+
366421
/// Rewrite a partial_apply convert_escape_to_noescape sequence with a single
367422
/// apply/try_apply user to a partial_apply [stack] terminated with a
368423
/// dealloc_stack placed after the apply.
@@ -386,12 +441,15 @@ static SILValue insertMarkDependenceForCapturedArguments(PartialApplyInst *pai,
386441
/// dealloc_stack still needs to be balanced with other dealloc_stacks i.e the
387442
/// caller needs to use the StackNesting utility to update the dealloc_stack
388443
/// nesting.
389-
static bool tryRewriteToPartialApplyStack(
390-
SILLocation &loc, PartialApplyInst *origPA,
391-
ConvertEscapeToNoEscapeInst *cvt, SILInstruction *singleApplyUser,
392-
SILBasicBlock::iterator &advanceIfDelete,
444+
static SILValue tryRewriteToPartialApplyStack(
445+
ConvertEscapeToNoEscapeInst *cvt,
446+
SILInstruction *closureUser, SILBasicBlock::iterator &advanceIfDelete,
393447
llvm::DenseMap<SILInstruction *, SILInstruction *> &memoized) {
394-
448+
449+
auto *origPA = dyn_cast<PartialApplyInst>(skipConvert(cvt->getOperand()));
450+
if (!origPA)
451+
return SILValue();
452+
395453
auto *convertOrPartialApply = cast<SingleValueInstruction>(origPA);
396454
if (cvt->getOperand() != origPA)
397455
convertOrPartialApply = cast<ConvertFunctionInst>(cvt->getOperand());
@@ -415,7 +473,7 @@ static bool tryRewriteToPartialApplyStack(
415473
continue;
416474
}
417475
if (singleNonDebugNonRefCountUser)
418-
return false;
476+
return SILValue();
419477
singleNonDebugNonRefCountUser = user;
420478
}
421479

@@ -480,39 +538,15 @@ static bool tryRewriteToPartialApplyStack(
480538
}
481539
}
482540

483-
// Insert destroys of arguments after the apply and the dealloc_stack.
484-
if (auto *apply = dyn_cast<ApplyInst>(singleApplyUser)) {
485-
auto insertPt = std::next(SILBasicBlock::iterator(apply));
486-
// Don't insert dealloc_stacks at unreachable.
487-
if (isa<UnreachableInst>(*insertPt))
488-
return true;
489-
SILBuilderWithScope b3(insertPt);
490-
b3.createDeallocStack(loc, newPA);
491-
insertDestroyOfCapturedArguments(newPA, b3);
541+
// Insert destroys of arguments after the closure user and the dealloc_stack.
542+
insertAfterClosureUser(closureUser, [newPA](SILBuilder &builder) {
543+
auto loc = RegularLocation(builder.getInsertionPointLoc());
544+
builder.createDeallocStack(loc, newPA);
545+
insertDestroyOfCapturedArguments(newPA, builder);
492546
// dealloc_stack of the in_guaranteed capture is inserted
493-
insertDeallocOfCapturedArguments(newPA, b3);
494-
} else if (auto *tai = dyn_cast<TryApplyInst>(singleApplyUser)) {
495-
for (auto *succBB : tai->getSuccessorBlocks()) {
496-
SILBuilderWithScope b3(succBB->begin());
497-
b3.createDeallocStack(loc, newPA);
498-
insertDestroyOfCapturedArguments(newPA, b3);
499-
// dealloc_stack of the in_guaranteed capture is inserted
500-
insertDeallocOfCapturedArguments(newPA, b3);
501-
}
502-
} else {
503-
llvm_unreachable("Unknown FullApplySite instruction kind");
504-
}
505-
return true;
506-
}
507-
508-
static SILValue skipConvert(SILValue v) {
509-
auto *cvt = dyn_cast<ConvertFunctionInst>(v);
510-
if (!cvt)
511-
return v;
512-
auto *pa = dyn_cast<PartialApplyInst>(cvt->getOperand());
513-
if (!pa || !pa->hasOneUse())
514-
return v;
515-
return pa;
547+
insertDeallocOfCapturedArguments(newPA, builder);
548+
});
549+
return closure;
516550
}
517551

518552
static bool tryExtendLifetimeToLastUse(
@@ -525,45 +559,52 @@ static bool tryExtendLifetimeToLastUse(
525559
if (!singleUser)
526560
return false;
527561

528-
// Handle an apply.
529-
if (auto singleApplyUser = FullApplySite::isa(singleUser)) {
530-
// FIXME: Don't know how-to handle begin_apply/end_apply yet.
531-
if (isa<BeginApplyInst>(singleApplyUser.getInstruction())) {
562+
// Handle apply instructions and startAsyncLet.
563+
BuiltinInst *endAsyncLet = nullptr;
564+
if (FullApplySite::isa(singleUser)) {
565+
// TODO: Enable begin_apply/end_apply. It should work, but is not tested yet.
566+
if (isa<BeginApplyInst>(singleUser))
532567
return false;
533-
}
534-
535-
auto loc = RegularLocation::getAutoGeneratedLocation();
536-
auto origPA = dyn_cast<PartialApplyInst>(skipConvert(cvt->getOperand()));
537-
if (origPA && tryRewriteToPartialApplyStack(
538-
loc, origPA, cvt, singleApplyUser.getInstruction(),
539-
advanceIfDelete, memoized))
540-
return true;
568+
} else if (auto *bi = dyn_cast<BuiltinInst>(singleUser)) {
569+
endAsyncLet = getEndAsyncLet(bi);
570+
if (!endAsyncLet)
571+
return false;
572+
} else {
573+
return false;
574+
}
541575

542-
// Insert a copy at the convert_escape_to_noescape [not_guaranteed] and
543-
// change the instruction to the guaranteed form.
544-
auto escapingClosure = cvt->getOperand();
545-
auto *closureCopy =
546-
SILBuilderWithScope(cvt).createCopyValue(loc, escapingClosure);
547-
cvt->setLifetimeGuaranteed();
548-
cvt->setOperand(closureCopy);
549-
550-
// Insert a destroy after the apply.
551-
if (auto *apply = dyn_cast<ApplyInst>(singleApplyUser.getInstruction())) {
552-
auto insertPt = std::next(SILBasicBlock::iterator(apply));
553-
SILBuilderWithScope(insertPt).createDestroyValue(loc, closureCopy);
554-
555-
} else if (auto *tai =
556-
dyn_cast<TryApplyInst>(singleApplyUser.getInstruction())) {
557-
for (auto *succBB : tai->getSuccessorBlocks()) {
558-
SILBuilderWithScope(succBB->begin())
559-
.createDestroyValue(loc, closureCopy);
560-
}
561-
} else {
562-
llvm_unreachable("Unknown FullApplySite instruction kind");
576+
if (SILValue closure = tryRewriteToPartialApplyStack(cvt, singleUser,
577+
advanceIfDelete, memoized)) {
578+
if (auto *cfi = dyn_cast<ConvertFunctionInst>(closure))
579+
closure = cfi->getOperand();
580+
if (endAsyncLet && isa<MarkDependenceInst>(closure)) {
581+
// Add the top-level mark_dependence (which keeps the closure arguments
582+
// alive) as a second operand to the endAsyncLet builtin.
583+
// This ensures that the closure arguments are kept alive until the
584+
// endAsyncLet builtin.
585+
assert(endAsyncLet->getNumOperands() == 1);
586+
SILBuilderWithScope builder(endAsyncLet);
587+
builder.createBuiltin(endAsyncLet->getLoc(), endAsyncLet->getName(),
588+
endAsyncLet->getType(), endAsyncLet->getSubstitutions(),
589+
{endAsyncLet->getOperand(0), closure});
590+
endAsyncLet->eraseFromParent();
563591
}
564592
return true;
565593
}
566-
return false;
594+
595+
// Insert a copy at the convert_escape_to_noescape [not_guaranteed] and
596+
// change the instruction to the guaranteed form.
597+
auto escapingClosure = cvt->getOperand();
598+
auto *closureCopy =
599+
SILBuilderWithScope(cvt).createCopyValue(cvt->getLoc(), escapingClosure);
600+
cvt->setLifetimeGuaranteed();
601+
cvt->setOperand(closureCopy);
602+
603+
insertAfterClosureUser(singleUser, [closureCopy](SILBuilder &builder) {
604+
auto loc = RegularLocation(builder.getInsertionPointLoc());
605+
builder.createDestroyValue(loc, closureCopy);
606+
});
607+
return true;
567608
}
568609

569610
/// Ensure the lifetime of the closure across a two step optional conversion

test/SILGen/rethrows.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ func test1() throws {
6565
// CHECK-NEXT: [[RESULT:%.*]] = tuple ()
6666
// CHECK-NEXT: return [[RESULT]]
6767
// CHECK: [[ERROR]]([[T0:%.*]] : $Error):
68-
// CHECK-NEXT: strong_release [[T1]]
6968
// CHECK-NEXT: unreachable
7069
func test2() {
7170
rethrower(nonthrower)

test/SILOptimizer/closure_lifetime_fixup.swift

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// RUN: %empty-directory(%t)
22
// RUN: %target-swift-frontend %S/../Inputs/resilient_struct.swift -enable-library-evolution -emit-module -emit-module-path %t/resilient_struct.swiftmodule
33
// RUN: %target-swift-frontend %S/../Inputs/resilient_enum.swift -I %t -enable-library-evolution -emit-module -emit-module-path %t/resilient_enum.swiftmodule
4-
// RUN: %target-swift-frontend %s -sil-verify-all -emit-sil -disable-copy-propagation -I %t -o - | %FileCheck %s
4+
// RUN: %target-swift-frontend %s -sil-verify-all -enable-experimental-concurrency -emit-sil -disable-copy-propagation -I %t -o - | %FileCheck %s
55

66
// Using -disable-copy-propagation to pattern match against older SIL
77
// output. At least until -enable-copy-propagation has been around
@@ -63,6 +63,20 @@ public func testGeneric(c: C) {
6363
use_closureGeneric { return c.returnInt() }
6464
}
6565

66+
// CHECK-LABEL: sil @$s22closure_lifetime_fixup12testAsyncLetyS2SYaF : $@convention(thin) @async (@guaranteed String) -> @owned String {
67+
// CHECK: [[PA:%.*]] = partial_apply [callee_guaranteed] [on_stack]
68+
// CHECK: [[MD:%.*]] = mark_dependence [[PA]]
69+
// CHECK: [[CONV:%.*]] = convert_function [[MD]]
70+
// CHECK: [[BAL:%.*]] = builtin "startAsyncLet"<String>([[CONV]]
71+
// CHECK: builtin "endAsyncLet"([[BAL]] : $Builtin.RawPointer, [[MD]]
72+
// CHECK: } // end sil function '$s22closure_lifetime_fixup12testAsyncLetyS2SYaF'
73+
74+
public func testAsyncLet(_ n: String) async -> String {
75+
async let first = n
76+
let result = await first
77+
return result
78+
}
79+
6680
public protocol P {
6781
associatedtype Element
6882
subscript<U>(a: (Element) -> U, b: (U) -> Element) -> U { get set }

0 commit comments

Comments
 (0)