Skip to content

Commit 3d5285b

Browse files
committed
Arrange for closure bodies promoted by AllocBoxToStack to have their originals removed by MoveOnlyChecker.
This is an improvement of #67031 which avoids deleting the closure function body during AllocBoxToStack, which still breaks pass invariants by modifying functions other than the currently-analyzed function. As a function pass, AllocBoxToStack also doesn't really know with certainty whether the original closure function is unused after stack promotion or not. We still want to eliminate the original when it may contain invalid SIL for move-only values that rely on the escape analysis for correct semantics, so rather than mark the original function to be *ignored* during move-only checking, mark it to be *deleted* by move-only checking if the function is in fact unused at that point. If the marked function is still used, we let it pass through move-only checking normally, which may cause redundant diagnostics but is the right thing to do since code is still potentially using the closure with escaping semantics. We should rearrange things to make this situation impossible in the future. rdar://110675352
1 parent 8d97421 commit 3d5285b

File tree

7 files changed

+135
-27
lines changed

7 files changed

+135
-27
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: 3 additions & 3 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

test/SILOptimizer/allocbox_to_stack.sil

Lines changed: 3 additions & 3 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,7 +651,7 @@ bb0(%0 : $Int, %1 : $*S<T>):
651651
return %9 : $Bool
652652
}
653653

654-
// CHECK-LABEL: sil shared [_semantics "sil.optimizer.moveonly.diagnostic.ignore"] @closure1
654+
// CHECK-LABEL: sil shared [_semantics "sil.optimizer.moveonly.delete_if_unused"] @closure1
655655
sil shared @closure1 : $@convention(thin) <T where T : Count> (Int, @owned <τ_0_0 : Count> { var S<τ_0_0> } <T>) -> Bool {
656656
// CHECK: bb0
657657
bb0(%0 : $Int, %1 : $<τ_0_0 where τ_0_0 : Count> { var S<τ_0_0> } <T>):
@@ -673,7 +673,7 @@ bb0(%0 : $Int, %1 : $<τ_0_0 where τ_0_0 : Count> { var S<τ_0_0> } <T>):
673673
// CHECK: return
674674
// CHECK-NOT: bb1
675675

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

test/SILOptimizer/allocbox_to_stack_ownership.sil

Lines changed: 3 additions & 3 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,7 +751,7 @@ bb0(%0 : $Int, %1 : $*S<T>):
751751
return %9 : $Bool
752752
}
753753

754-
// CHECK-LABEL: sil shared [_semantics "sil.optimizer.moveonly.diagnostic.ignore"] [ossa] @closure1
754+
// CHECK-LABEL: sil shared [_semantics "sil.optimizer.moveonly.delete_if_unused"] [ossa] @closure1
755755
sil shared [ossa] @closure1 : $@convention(thin) <T where T : Count> (Int, @owned <τ_0_0 : Count> { var S<τ_0_0> } <T>) -> Bool {
756756
// CHECK: bb0
757757
bb0(%0 : $Int, %1 : @owned $<τ_0_0 where τ_0_0 : Count> { var S<τ_0_0> } <T>):
@@ -773,7 +773,7 @@ bb0(%0 : $Int, %1 : @owned $<τ_0_0 where τ_0_0 : Count> { var S<τ_0_0> } <T>)
773773
// CHECK: return
774774
// CHECK-NOT: bb1
775775

776-
// 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
777777
sil shared [ossa] @closure2 : $@convention(thin) <T where T : Count> (Int, @owned <τ_0_0 : Count> { var S<τ_0_0> } <T>) -> Bool {
778778
// CHECK: bb0
779779
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)