Skip to content

Commit 631bcab

Browse files
authored
Merge pull request #67205 from jckarter/allocbox-to-stack-move-only-closure-remnants
Arrange for closure bodies promoted by AllocBoxToStack to have their originals removed by MoveOnlyChecker.
2 parents 65d819e + 3d5285b commit 631bcab

File tree

7 files changed

+141
-81
lines changed

7 files changed

+141
-81
lines changed

include/swift/AST/SemanticAttrs.def

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -128,22 +128,24 @@ SEMANTICS_ATTR(LIFETIMEMANAGEMENT_COPY, "lifetimemanagement.copy")
128128
SEMANTICS_ATTR(NO_PERFORMANCE_ANALYSIS, "no_performance_analysis")
129129

130130
// A flag used to turn off moveonly diagnostics on a function due to an earlier
131-
// pass that ran.
132-
//
133-
// DISCUSSION: This is used in a few situations:
134-
//
135-
// 1. When allocbox to stack specializes a function, we do not want to emit move
136-
// errors twice, once on the specialized and once on the original
137-
// function. Instead, we put this on the original function.
138-
//
139-
// 2. If we emit a diagnose invalid escaping captures error due to an inout
140-
// being escaped into an escaping closure, we do not want to emit move errors in
141-
// the closure. This is because SILGen today assumes that we will error in such
142-
// cases and thus does not emit markers in said function for the inout. This
143-
// then causes us to emit spurious "found a copy of a noncopyable value" errors
144-
// that may cause the user to think there is a bug in the compiler.
131+
// pass that ran. If we emit a diagnose invalid escaping captures error due to
132+
// an inout being escaped into an escaping closure, we do not want to emit move
133+
// errors in the closure. This is because SILGen today assumes that we will error
134+
// in such cases and thus does not emit markers in said function for the inout.
135+
// This then causes us to emit spurious "found a copy of a noncopyable value"
136+
// errors that may cause the user to think there is a bug in the compiler.
145137
SEMANTICS_ATTR(NO_MOVEONLY_DIAGNOSTICS, "sil.optimizer.moveonly.diagnostic.ignore")
146138

139+
// Tell the move-only checker to delete this function body instead of performing
140+
// diagnostics if the function is not used at the time of checking.
141+
// When allocbox to stack specializes a function, we do not want to emit move
142+
// errors twice, once on the specialized and once on the original
143+
// function. The semantics of the closure with regards to move-only values may
144+
// also be dependent on stack promotion. Therefore, we mark the original with
145+
// this attribute, so that after it's promoted, the original function gets deleted
146+
// instead of raising spurious diagnostics.
147+
SEMANTICS_ATTR(MOVEONLY_DELETE_IF_UNUSED, "sil.optimizer.moveonly.delete_if_unused")
148+
147149
// Force the use of the frame pointer for the specified function
148150
SEMANTICS_ATTR(USE_FRAME_POINTER, "use_frame_pointer")
149151

lib/SILOptimizer/Mandatory/MoveOnlyChecker.cpp

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,9 +157,58 @@ class MoveOnlyCheckerPass : public SILFunctionTransform {
157157
assert(fn->getModule().getStage() == SILStage::Raw &&
158158
"Should only run on Raw SIL");
159159

160+
// If an earlier pass asked us to eliminate the function body if it's
161+
// unused, and the function is in fact unused, do that now.
162+
if (fn->hasSemanticsAttr(semantics::MOVEONLY_DELETE_IF_UNUSED)) {
163+
if (fn->getRefCount() == 0
164+
&& !isPossiblyUsedExternally(fn->getLinkage(),
165+
fn->getModule().isWholeModule())) {
166+
LLVM_DEBUG(llvm::dbgs() << "===> Deleting unused function " << fn->getName() << "'s body that was marked for deletion\n");
167+
// Remove all non-entry blocks.
168+
auto entryBB = fn->begin();
169+
auto nextBB = std::next(entryBB);
170+
171+
while (nextBB != fn->end()) {
172+
auto thisBB = nextBB;
173+
++nextBB;
174+
thisBB->eraseFromParent();
175+
}
176+
177+
// Rewrite the entry block to only contain an unreachable.
178+
auto loc = entryBB->begin()->getLoc();
179+
entryBB->eraseAllInstructions(fn->getModule());
180+
{
181+
SILBuilder b(&*entryBB);
182+
b.createUnreachable(loc);
183+
}
184+
185+
// If the function has shared linkage, reduce this version to private
186+
// linkage, because we don't want the deleted-body form to win in any
187+
// ODR shootouts.
188+
if (fn->getLinkage() == SILLinkage::Shared) {
189+
fn->setLinkage(SILLinkage::Private);
190+
}
191+
192+
invalidateAnalysis(SILAnalysis::InvalidationKind::FunctionBody);
193+
return;
194+
}
195+
// If the function wasn't unused, let it continue into diagnostics.
196+
// This would come up if a closure function somehow was used in different
197+
// functions with different escape analysis results. This shouldn't really
198+
// be possible, and we should try harder to make it impossible, but if
199+
// it does happen now, the least bad thing to do is to proceed with
200+
// move checking. This will either succeed and make sure the original
201+
// function contains valid SIL, or raise errors relating to the use
202+
// of the captures in an escaping way, which is the right thing to do
203+
// if they are in fact escapable.
204+
LLVM_DEBUG(llvm::dbgs() << "===> Function " << fn->getName()
205+
<< " was marked to be deleted, but has uses. Continuing with move checking\n");
206+
207+
}
208+
160209
// If an earlier pass told use to not emit diagnostics for this function,
161210
// clean up any copies, invalidate the analysis, and return early.
162-
if (getFunction()->hasSemanticsAttr(semantics::NO_MOVEONLY_DIAGNOSTICS)) {
211+
if (fn->hasSemanticsAttr(semantics::NO_MOVEONLY_DIAGNOSTICS)) {
163212
if (cleanupNonCopyableCopiesAfterEmittingDiagnostic(getFunction()))
164213
invalidateAnalysis(SILAnalysis::InvalidationKind::Instructions);
165214
return;
@@ -169,7 +218,7 @@ class MoveOnlyCheckerPass : public SILFunctionTransform {
169218
<< "===> MoveOnly Checker. Visiting: " << fn->getName() << '\n');
170219

171220
MoveOnlyChecker checker(
172-
getFunction(), getAnalysis<DominanceAnalysis>()->get(getFunction()),
221+
fn, getAnalysis<DominanceAnalysis>()->get(fn),
173222
getAnalysis<PostOrderAnalysis>());
174223

175224
checker.checkObjects();
@@ -180,12 +229,12 @@ class MoveOnlyCheckerPass : public SILFunctionTransform {
180229
// non-copyable copies into explicit variants below and let the user
181230
// recompile.
182231
if (!checker.diagnosticEmitter.emittedDiagnostic()) {
183-
emitCheckerMissedCopyOfNonCopyableTypeErrors(getFunction(),
232+
emitCheckerMissedCopyOfNonCopyableTypeErrors(fn,
184233
checker.diagnosticEmitter);
185234
}
186235

187236
checker.madeChange |=
188-
cleanupNonCopyableCopiesAfterEmittingDiagnostic(getFunction());
237+
cleanupNonCopyableCopiesAfterEmittingDiagnostic(fn);
189238

190239
if (checker.madeChange)
191240
invalidateAnalysis(SILAnalysis::InvalidationKind::Instructions);

lib/SILOptimizer/Transforms/AllocBoxToStack.cpp

Lines changed: 5 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1047,9 +1047,9 @@ specializeApplySite(SILOptFunctionBuilder &FuncBuilder, ApplySite Apply,
10471047
ClonedFn = Cloner.getCloned();
10481048
pass.T->addFunctionToPassManagerWorklist(ClonedFn, F);
10491049

1050-
// Set the moveonly ignore flag so we do not emit an error on the original
1051-
// function even though it is still around.
1052-
F->addSemanticsAttr(semantics::NO_MOVEONLY_DIAGNOSTICS);
1050+
// Set the moveonly delete-if-unused flag so we do not emit an error on the
1051+
// original once we promote all its current uses.
1052+
F->addSemanticsAttr(semantics::MOVEONLY_DELETE_IF_UNUSED);
10531053

10541054
// If any of our promoted callee arg indices were originally noncopyable let
10551055
// boxes, convert them from having escaping to having non-escaping
@@ -1213,51 +1213,9 @@ static void rewriteApplySites(AllocBoxToStackState &pass) {
12131213
auto *FRI = cast<FunctionRefInst>(Apply.getCallee());
12141214
Apply.getInstruction()->eraseFromParent();
12151215

1216-
if (FRI->use_empty()) {
1217-
auto referencedFn = FRI->getReferencedFunction();
1216+
// TODO: Erase from module if there are no more uses.
1217+
if (FRI->use_empty())
12181218
FRI->eraseFromParent();
1219-
1220-
// TODO: Erase from module if there are no more uses.
1221-
// If the function has no remaining references, it should eventually
1222-
// be deleted. We can't do that from a function pass, since the function
1223-
// is still queued up for other passes to run after this one, but we
1224-
// can at least gut the implementation, since subsequent passes that
1225-
// rely on stack promotion to occur (particularly closure lifetime
1226-
// fixup and move-only checking) may not be able to proceed in a
1227-
// sensible way on the now non-canonical original implementation.
1228-
if (referencedFn->getRefCount() == 0
1229-
&& !isPossiblyUsedExternally(referencedFn->getLinkage(),
1230-
referencedFn->getModule().isWholeModule())) {
1231-
LLVM_DEBUG(llvm::dbgs() << "*** Deleting original function " << referencedFn->getName() << "'s body since it is unused");
1232-
// Remove all non-entry blocks.
1233-
auto entryBB = referencedFn->begin();
1234-
auto nextBB = std::next(entryBB);
1235-
1236-
while (nextBB != referencedFn->end()) {
1237-
auto thisBB = nextBB;
1238-
++nextBB;
1239-
thisBB->eraseFromParent();
1240-
}
1241-
1242-
// Rewrite the entry block to only contain an unreachable.
1243-
auto loc = entryBB->begin()->getLoc();
1244-
entryBB->eraseAllInstructions(referencedFn->getModule());
1245-
{
1246-
SILBuilder b(&*entryBB);
1247-
b.createUnreachable(loc);
1248-
}
1249-
1250-
// Refresh the CFG in case we removed any function calls.
1251-
pass.CFGChanged = true;
1252-
1253-
// If the function has shared linkage, reduce this version to private
1254-
// linkage, because we don't want the deleted-body form to win in any
1255-
// ODR shootouts.
1256-
if (referencedFn->getLinkage() == SILLinkage::Shared) {
1257-
referencedFn->setLinkage(SILLinkage::Private);
1258-
}
1259-
}
1260-
}
12611219
}
12621220
}
12631221

test/SILOptimizer/allocbox_to_stack.sil

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ bb0(%0 : $Int):
398398
}
399399

400400
// CHECK-LABEL: sil private @$s6struct8useStack1tySi_tFSiycfU_Tf0s_n
401-
// CHECK-LABEL: sil private [_semantics "sil.optimizer.moveonly.diagnostic.ignore"] @$s6struct8useStack1tySi_tFSiycfU_
401+
// CHECK-LABEL: sil private [_semantics "sil.optimizer.moveonly.delete_if_unused"] @$s6struct8useStack1tySi_tFSiycfU_
402402
// struct.(useStack (t : Swift.Int) -> ()).(closure #1)
403403
sil private @$s6struct8useStack1tySi_tFSiycfU_ : $@convention(thin) (@owned <τ_0_0> { var τ_0_0 } <Int>) -> Int {
404404
bb0(%0 : $<τ_0_0> { var τ_0_0 } <Int>):
@@ -651,13 +651,9 @@ bb0(%0 : $Int, %1 : $*S<T>):
651651
return %9 : $Bool
652652
}
653653

654-
// This closure body gets specialized with unboxed capture parameters, so
655-
// the original function body is stubbed out once the call sites are all
656-
// specialized.
657-
// CHECK-LABEL: sil private [_semantics "sil.optimizer.moveonly.diagnostic.ignore"] @closure1
658-
// CHECK: bb0
659-
// CHECK-NEXT: unreachable
654+
// CHECK-LABEL: sil shared [_semantics "sil.optimizer.moveonly.delete_if_unused"] @closure1
660655
sil shared @closure1 : $@convention(thin) <T where T : Count> (Int, @owned <τ_0_0 : Count> { var S<τ_0_0> } <T>) -> Bool {
656+
// CHECK: bb0
661657
bb0(%0 : $Int, %1 : $<τ_0_0 where τ_0_0 : Count> { var S<τ_0_0> } <T>):
662658
%2 = project_box %1 : $<τ_0_0 where τ_0_0 : Count> { var S<τ_0_0> } <T>, 0
663659
%3 = function_ref @inner : $@convention(thin) (@owned @callee_owned () -> Bool) -> Bool
@@ -668,6 +664,7 @@ bb0(%0 : $Int, %1 : $<τ_0_0 where τ_0_0 : Count> { var S<τ_0_0> } <T>):
668664
%7 = partial_apply %4<T>(%0, %5) : $@convention(thin) <τ_0_0 where τ_0_0 : Count> (Int, @owned <τ_0_0 : Count> { var S<τ_0_0> } <τ_0_0>) -> Bool
669665
%8 = apply %3(%7) : $@convention(thin) (@owned @callee_owned () -> Bool) -> Bool
670666
strong_release %1 : $<τ_0_0 where τ_0_0 : Count> { var S<τ_0_0> } <T>
667+
// CHECK: return
671668
return %8 : $Bool
672669
}
673670

@@ -676,7 +673,7 @@ bb0(%0 : $Int, %1 : $<τ_0_0 where τ_0_0 : Count> { var S<τ_0_0> } <T>):
676673
// CHECK: return
677674
// CHECK-NOT: bb1
678675

679-
// CHECK-LABEL: sil shared [_semantics "sil.optimizer.moveonly.diagnostic.ignore"] @closure2
676+
// CHECK-LABEL: sil shared [_semantics "sil.optimizer.moveonly.delete_if_unused"] @closure2
680677
sil shared @closure2 : $@convention(thin) <T where T : Count> (Int, @owned <τ_0_0 : Count> { var S<τ_0_0> } <T>) -> Bool {
681678
// CHECK: bb0
682679
bb0(%0 : $Int, %1 : $<τ_0_0 where τ_0_0 : Count> { var S<τ_0_0> } <T>):

test/SILOptimizer/allocbox_to_stack_ownership.sil

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,7 @@ bb0(%0 : $Int):
394394
}
395395

396396
// CHECK-LABEL: sil private [transparent] [ossa] @$s6struct8useStack1tySi_tFSiycfU_Tf0s_n
397-
// CHECK-LABEL: sil private [transparent] [_semantics "sil.optimizer.moveonly.diagnostic.ignore"] [ossa] @$s6struct8useStack1tySi_tFSiycfU_
397+
// CHECK-LABEL: sil private [transparent] [_semantics "sil.optimizer.moveonly.delete_if_unused"] [ossa] @$s6struct8useStack1tySi_tFSiycfU_
398398
// struct.(useStack (t : Swift.Int) -> ()).(closure #1)
399399
sil private [transparent] [ossa] @$s6struct8useStack1tySi_tFSiycfU_ : $@convention(thin) (@owned <τ_0_0> { var τ_0_0 } <Int>) -> Int {
400400
bb0(%0 : @owned $<τ_0_0> { var τ_0_0 } <Int>):
@@ -751,13 +751,9 @@ bb0(%0 : $Int, %1 : $*S<T>):
751751
return %9 : $Bool
752752
}
753753

754-
// This closure body gets specialized with unboxed capture parameters, so
755-
// the original function body is stubbed out once the call sites are all
756-
// specialized.
757-
// CHECK-LABEL: sil private [_semantics "sil.optimizer.moveonly.diagnostic.ignore"] [ossa] @closure1
758-
// CHECK: bb0
759-
// CHECK: unreachable
754+
// CHECK-LABEL: sil shared [_semantics "sil.optimizer.moveonly.delete_if_unused"] [ossa] @closure1
760755
sil shared [ossa] @closure1 : $@convention(thin) <T where T : Count> (Int, @owned <τ_0_0 : Count> { var S<τ_0_0> } <T>) -> Bool {
756+
// CHECK: bb0
761757
bb0(%0 : $Int, %1 : @owned $<τ_0_0 where τ_0_0 : Count> { var S<τ_0_0> } <T>):
762758
%2 = project_box %1 : $<τ_0_0 where τ_0_0 : Count> { var S<τ_0_0> } <T>, 0
763759
%3 = function_ref @inner : $@convention(thin) (@owned @callee_owned () -> Bool) -> Bool
@@ -768,6 +764,7 @@ bb0(%0 : $Int, %1 : @owned $<τ_0_0 where τ_0_0 : Count> { var S<τ_0_0> } <T>)
768764
%7 = partial_apply %4<T>(%0, %5) : $@convention(thin) <τ_0_0 where τ_0_0 : Count> (Int, @owned <τ_0_0 : Count> { var S<τ_0_0> } <τ_0_0>) -> Bool
769765
%8 = apply %3(%7) : $@convention(thin) (@owned @callee_owned () -> Bool) -> Bool
770766
destroy_value %1 : $<τ_0_0 where τ_0_0 : Count> { var S<τ_0_0> } <T>
767+
// CHECK: return
771768
return %8 : $Bool
772769
}
773770

@@ -776,7 +773,7 @@ bb0(%0 : $Int, %1 : @owned $<τ_0_0 where τ_0_0 : Count> { var S<τ_0_0> } <T>)
776773
// CHECK: return
777774
// CHECK-NOT: bb1
778775

779-
// CHECK-LABEL: sil shared [_semantics "sil.optimizer.moveonly.diagnostic.ignore"] [ossa] @closure2
776+
// CHECK-LABEL: sil shared [_semantics "sil.optimizer.moveonly.delete_if_unused"] [ossa] @closure2
780777
sil shared [ossa] @closure2 : $@convention(thin) <T where T : Count> (Int, @owned <τ_0_0 : Count> { var S<τ_0_0> } <T>) -> Bool {
781778
// CHECK: bb0
782779
bb0(%0 : $Int, %1 : @owned $<τ_0_0 where τ_0_0 : Count> { var S<τ_0_0> } <T>):
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// RUN: %target-sil-opt -module-name main -sil-move-only-checker -enable-sil-verify-all %s | %FileCheck %s
2+
3+
// No uses, shared linkage. Can delete and make private
4+
// CHECK-LABEL: sil private{{.*}} @unused_shared
5+
// CHECK: unreachable
6+
sil shared [_semantics "sil.optimizer.moveonly.delete_if_unused"] [ossa] @unused_shared : $@convention(thin) () -> () {
7+
entry:
8+
return undef : $()
9+
}
10+
11+
// No uses, private linkage. Can delete
12+
// CHECK-LABEL: sil private{{.*}} @unused_private
13+
// CHECK: unreachable
14+
sil private [_semantics "sil.optimizer.moveonly.delete_if_unused"] [ossa] @unused_private : $@convention(thin) () -> () {
15+
entry:
16+
return undef : $()
17+
}
18+
19+
// Public linkage. Leave as is
20+
// CHECK-LABEL: sil {{.*}} @unused_public
21+
// CHECK: [[F:%.*]] = function_ref @still_used
22+
// CHECK: apply [[F]]()
23+
// CHECK: return undef : $()
24+
sil public [_semantics "sil.optimizer.moveonly.delete_if_unused"] [ossa] @unused_public : $@convention(thin) () -> () {
25+
entry:
26+
%f = function_ref @still_used : $@convention(thin) () -> ()
27+
apply %f() : $@convention(thin) () -> ()
28+
return undef : $()
29+
}
30+
31+
// Private linkage but used. Leave as is
32+
// CHECK-LABEL: sil private{{.*}} @still_used
33+
// CHECK: return undef : $()
34+
sil private [_semantics "sil.optimizer.moveonly.delete_if_unused"] [ossa] @still_used : $@convention(thin) () -> () {
35+
entry:
36+
return undef : $()
37+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// RUN: %target-swift-frontend -emit-sil -verify %s
2+
// RUN: %target-swift-frontend -emit-sil -O -verify %s
3+
4+
// rdar://110675352
5+
6+
struct NonCopyableStruct: ~Copyable {}
7+
8+
var globFn: () -> () = {}
9+
func forceEscaping(_ esc: @escaping () -> ()) {
10+
globFn = esc
11+
}
12+
13+
func closureDiagnosticsSimple() {
14+
var s = NonCopyableStruct()
15+
let f = {
16+
_ = consume s
17+
s = NonCopyableStruct()
18+
}
19+
f()
20+
}

0 commit comments

Comments
 (0)