Skip to content

Commit d0d64a1

Browse files
authored
Merge pull request #21239 from atrick/5.0-fix-devirt-enum
[5.0] Fix SILCombine to devirtualize existentials wrapped in enum.
2 parents 7ad4ce8 + 8a09616 commit d0d64a1

File tree

7 files changed

+185
-8
lines changed

7 files changed

+185
-8
lines changed

include/swift/SILOptimizer/Utils/Existential.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ SILValue findInitExistentialFromGlobalAddrAndApply(GlobalAddrInst *GAI,
3535
/// alloc_stack user \p ASIUser.
3636
/// If the value is copied from another stack location, \p isCopied is set to
3737
/// true.
38-
SILValue getAddressOfStackInit(AllocStackInst *ASI, SILInstruction *ASIUser,
38+
SILValue getAddressOfStackInit(SILValue allocStackAddr, SILInstruction *ASIUser,
3939
bool &isCopied);
4040

4141
/// Find the init_existential, which could be used to determine a concrete

include/swift/SILOptimizer/Utils/Local.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,13 @@ SILValue castValueToABICompatibleType(SILBuilder *B, SILLocation Loc,
127127
SILValue Value,
128128
SILType SrcTy,
129129
SILType DestTy);
130+
/// Peek through trivial Enum initialization, typically for pointless
131+
/// Optionals.
132+
///
133+
/// The returned InitEnumDataAddr dominates the given
134+
/// UncheckedTakeEnumDataAddrInst.
135+
InitEnumDataAddrInst *
136+
findInitAddressForTrivialEnum(UncheckedTakeEnumDataAddrInst *UTEDAI);
130137

131138
/// Returns a project_box if it is the next instruction after \p ABI and
132139
/// and has \p ABI as operand. Otherwise it creates a new project_box right

lib/SILOptimizer/SILCombiner/SILCombinerApplyVisitors.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -729,6 +729,13 @@ static bool canReplaceCopiedArg(FullApplySite Apply,
729729
SILValue existentialAddr =
730730
cast<InitExistentialAddrInst>(InitExistential)->getOperand();
731731

732+
// If we peeked through an InitEnumDataAddr or some such, then don't assume we
733+
// can reuse the copied value. It's likely destroyed by
734+
// UncheckedTakeEnumDataInst before the copy.
735+
auto *ASI = dyn_cast<AllocStackInst>(existentialAddr);
736+
if (!ASI)
737+
return false;
738+
732739
// Return true only if the given value is guaranteed to be initialized across
733740
// the given call site.
734741
//

lib/SILOptimizer/Utils/Existential.cpp

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13+
#include "swift/SILOptimizer/Utils/Existential.h"
1314
#include "swift/AST/Module.h"
1415
#include "swift/AST/ProtocolConformance.h"
15-
#include "swift/SILOptimizer/Utils/Existential.h"
16-
#include "swift/SILOptimizer/Utils/CFG.h"
17-
#include "swift/SIL/InstructionUtils.h"
1816
#include "swift/SIL/BasicBlockUtils.h"
17+
#include "swift/SIL/InstructionUtils.h"
18+
#include "swift/SILOptimizer/Utils/CFG.h"
19+
#include "swift/SILOptimizer/Utils/Local.h"
1920
#include "llvm/ADT/SmallPtrSet.h"
2021

2122
using namespace swift;
@@ -122,11 +123,14 @@ SILValue swift::findInitExistentialFromGlobalAddrAndApply(GlobalAddrInst *GAI,
122123
/// alloc_stack user \p ASIUser.
123124
/// If the value is copied from another stack location, \p isCopied is set to
124125
/// true.
125-
SILValue swift::getAddressOfStackInit(AllocStackInst *ASI,
126+
///
127+
/// allocStackAddr may either itself be an AllocStackInst or an
128+
/// InitEnumDataAddrInst that projects the value of an AllocStackInst.
129+
SILValue swift::getAddressOfStackInit(SILValue allocStackAddr,
126130
SILInstruction *ASIUser, bool &isCopied) {
127131
SILInstruction *SingleWrite = nullptr;
128132
// Check that this alloc_stack is initialized only once.
129-
for (auto Use : ASI->getUses()) {
133+
for (auto Use : allocStackAddr->getUses()) {
130134
auto *User = Use->getUser();
131135

132136
// Ignore instructions which don't write to the stack location.
@@ -138,7 +142,7 @@ SILValue swift::getAddressOfStackInit(AllocStackInst *ASI,
138142
continue;
139143
}
140144
if (auto *CAI = dyn_cast<CopyAddrInst>(User)) {
141-
if (CAI->getDest() == ASI) {
145+
if (CAI->getDest() == allocStackAddr) {
142146
if (SingleWrite)
143147
return SILValue();
144148
SingleWrite = CAI;
@@ -168,8 +172,13 @@ SILValue swift::getAddressOfStackInit(AllocStackInst *ASI,
168172

169173
// A very simple dominance check. As ASI is an operand of ASIUser,
170174
// SingleWrite dominates ASIUser if it is in the same block as ASI or ASIUser.
175+
//
176+
// If allocStack holds an Optional, then ASI is an InitEnumDataAddrInst
177+
// projection and not strictly an operand of ASIUser. We rely on the guarantee
178+
// that this InitEnumDataAddrInst must occur before the InjectEnumAddrInst
179+
// that was the source of the existential address.
171180
SILBasicBlock *BB = SingleWrite->getParent();
172-
if (BB != ASI->getParent() && BB != ASIUser->getParent())
181+
if (BB != allocStackAddr->getParentBlock() && BB != ASIUser->getParent())
173182
return SILValue();
174183

175184
if (auto *CAI = dyn_cast<CopyAddrInst>(SingleWrite)) {
@@ -179,6 +188,21 @@ SILValue swift::getAddressOfStackInit(AllocStackInst *ASI,
179188
SILValue CAISrc = CAI->getSrc();
180189
if (auto *ASI = dyn_cast<AllocStackInst>(CAISrc))
181190
return getAddressOfStackInit(ASI, CAI, isCopied);
191+
192+
// Recognize a stack location holding an Optional.
193+
// %stack_adr = alloc_stack
194+
// %data_adr = init_enum_data_addr %stk_adr
195+
// %enum_adr = inject_enum_addr %stack_adr
196+
// %copy_src = unchecked_take_enum_data_addr %enum_adr
197+
// Replace %copy_src with %data_adr and recurse.
198+
//
199+
// TODO: a general Optional elimination sil-combine could
200+
// supersede this check.
201+
if (auto *UTEDAI = dyn_cast<UncheckedTakeEnumDataAddrInst>(CAISrc)) {
202+
if (InitEnumDataAddrInst *IEDAI = findInitAddressForTrivialEnum(UTEDAI))
203+
return getAddressOfStackInit(IEDAI, CAI, isCopied);
204+
}
205+
182206
// Check if the CAISrc is a global_addr.
183207
if (auto *GAI = dyn_cast<GlobalAddrInst>(CAISrc)) {
184208
return findInitExistentialFromGlobalAddrAndCopyAddr(GAI, CAI);

lib/SILOptimizer/Utils/Local.cpp

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,51 @@ ProjectBoxInst *swift::getOrCreateProjectBox(AllocBoxInst *ABI, unsigned Index){
653653
return B.createProjectBox(ABI->getLoc(), ABI, Index);
654654
}
655655

656+
// Peek through trivial Enum initialization, typically for pointless
657+
// Optionals.
658+
//
659+
// Given an UncheckedTakeEnumDataAddrInst, check that there are no
660+
// other uses of the Enum value and return the address used to initialized the
661+
// enum's payload:
662+
//
663+
// %stack_adr = alloc_stack
664+
// %data_adr = init_enum_data_addr %stk_adr
665+
// %enum_adr = inject_enum_addr %stack_adr
666+
// %copy_src = unchecked_take_enum_data_addr %enum_adr
667+
// dealloc_stack %stack_adr
668+
// (No other uses of %stack_adr.)
669+
InitEnumDataAddrInst *
670+
swift::findInitAddressForTrivialEnum(UncheckedTakeEnumDataAddrInst *UTEDAI) {
671+
auto *ASI = dyn_cast<AllocStackInst>(UTEDAI->getOperand());
672+
if (!ASI)
673+
return nullptr;
674+
675+
SILInstruction *singleUser = nullptr;
676+
for (auto use : ASI->getUses()) {
677+
auto *user = use->getUser();
678+
if (user == UTEDAI)
679+
continue;
680+
681+
// As long as there's only one UncheckedTakeEnumDataAddrInst and one
682+
// InitEnumDataAddrInst, we don't care how many InjectEnumAddr and
683+
// DeallocStack users there are.
684+
if (isa<InjectEnumAddrInst>(user) || isa<DeallocStackInst>(user))
685+
continue;
686+
687+
if (singleUser)
688+
return nullptr;
689+
690+
singleUser = user;
691+
}
692+
if (!singleUser)
693+
return nullptr;
694+
695+
// Assume, without checking, that the returned InitEnumDataAddr dominates the
696+
// given UncheckedTakeEnumDataAddrInst, because that's how SIL is defined. I
697+
// don't know where this is actually verified.
698+
return dyn_cast<InitEnumDataAddrInst>(singleUser);
699+
}
700+
656701
//===----------------------------------------------------------------------===//
657702
// String Concatenation Optimizer
658703
//===----------------------------------------------------------------------===//

test/SILOptimizer/devirt_specialized_conformance.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,35 @@ func driver() {
4242
}
4343

4444
driver()
45+
46+
// <rdar://problem/46322928> Failure to devirtualize a protocol method
47+
// applied to an opened existential blocks implemention of
48+
// DataProtocol.
49+
public protocol ContiguousBytes {
50+
func withUnsafeBytes<R>(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R
51+
}
52+
53+
extension Array : ContiguousBytes {}
54+
extension ContiguousArray : ContiguousBytes {}
55+
56+
@inline(never)
57+
func takesPointer(_ p: UnsafeRawBufferPointer) {}
58+
59+
// In specialized testWithUnsafeBytes<A>(_:), the conditional case and call to withUnsafeBytes must be eliminated.
60+
// Normally, we expect Array.withUnsafeBytes to be inlined so we would see:
61+
// [[TAKES_PTR:%.*]] = function_ref @$s30devirt_specialized_conformance12takesPointeryySWF : $@convention(thin) (UnsafeRawBufferPointer) -> ()
62+
// apply [[TAKES_PTR]](%{{.*}}) : $@convention(thin) (UnsafeRawBufferPointer) -> ()
63+
// But the inlining isn't consistent across builds with and without debug info.
64+
//
65+
// CHECK-LABEL: sil shared [noinline] @$s30devirt_specialized_conformance19testWithUnsafeBytesyyxlFSayypG_Tg5 : $@convention(thin) (@guaranteed Array<Any>) -> () {
66+
// CHECK: bb0
67+
// CHECK-NOT: witness_method
68+
// CHECK-LABEL: } // end sil function '$s30devirt_specialized_conformance19testWithUnsafeBytesyyxlFSayypG_Tg5'
69+
@inline(never)
70+
func testWithUnsafeBytes<T>(_ t: T) {
71+
if let cb = t as? ContiguousBytes {
72+
cb.withUnsafeBytes { takesPointer($0) }
73+
}
74+
}
75+
76+
testWithUnsafeBytes([])

test/SILOptimizer/sil_combine_concrete_existential.sil

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,3 +437,65 @@ bb0:
437437
%v = tuple ()
438438
return %v : $()
439439
}
440+
441+
// <rdar://problem/46322928> Failure to devirtualize a protocol method
442+
// applied to an opened existential blocks implemention of
443+
// DataProtocol.
444+
public protocol ContiguousBytes {
445+
func withUnsafeBytes<R>(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R
446+
}
447+
448+
extension Array : ContiguousBytes {
449+
}
450+
451+
// specialized thunk for @callee_guaranteed (@unowned UnsafeRawBufferPointer) -> (@error @owned Error)
452+
sil [transparent] [reabstraction_thunk] @testDevirtExistentialEnumThunk : $@convention(thin) (UnsafeRawBufferPointer) -> (@out (), @error Error)
453+
454+
// Test that the witness method's opened archetype is replaced by a
455+
// concrete conformance.
456+
//
457+
// Note that the enum is destroyed by unchecked_take_enum_data_addr
458+
// before the apply (and the extracted data is destroy by the copy
459+
// itself), so SILCombine cannot replace the call's self
460+
// argument. Instead, we expect a later devirtualizer pass to just
461+
// insert a cast of the opened existential.
462+
//
463+
// CHECK-LABEL: sil shared [noinline] @testDevirtExistentialEnum : $@convention(thin) (@guaranteed Array<Int>) -> () {
464+
// CHECK: [[ALLOC_EXISTENTIAL:%.*]] = alloc_stack $ContiguousBytes
465+
// CHECK: [[ALLOC_ENUM:%.*]] = alloc_stack $Optional<ContiguousBytes>
466+
// CHECK: [[ENUM:%.*]] = init_enum_data_addr [[ALLOC_ENUM]] : $*Optional<ContiguousBytes>, #Optional.some!enumelt.1
467+
// CHECK: [[INIT_DATA:%.*]] = init_existential_addr [[ENUM]] : $*ContiguousBytes, $Array<Int>
468+
// CHECK: store %0 to [[INIT_DATA]] : $*Array<Int>
469+
// CHECK: inject_enum_addr [[ALLOC_ENUM]] : $*Optional<ContiguousBytes>, #Optional.some!enumelt.1
470+
// CHECK: [[TAKE_DATA:%.*]] = unchecked_take_enum_data_addr [[ALLOC_ENUM]] : $*Optional<ContiguousBytes>, #Optional.some!enumelt.1
471+
// CHECK: copy_addr [take] [[TAKE_DATA]] to [initialization] [[ALLOC_EXISTENTIAL]] : $*ContiguousBytes
472+
// CHECK: [[OPENED:%.*]] = open_existential_addr immutable_access [[ALLOC_EXISTENTIAL]] : $*ContiguousBytes to $*@opened("8402EC1A-F35D-11E8-950A-D0817AD9F6DD") ContiguousBytes
473+
// CHECK: [[THUNK:%.*]] = function_ref @testDevirtExistentialEnumThunk : $@convention(thin) (UnsafeRawBufferPointer) -> (@out (), @error Error)
474+
// CHECK: [[TTF:%.*]] = thin_to_thick_function [[THUNK:%.*]] : $@convention(thin) (UnsafeRawBufferPointer) -> (@out (), @error Error) to $@noescape @callee_guaranteed (UnsafeRawBufferPointer) -> (@out (), @error Error)
475+
// CHECK: [[WM:%.*]] = witness_method $Array<Int>, #ContiguousBytes.withUnsafeBytes!1 : <Self where Self : ContiguousBytes><R> (Self) -> ((UnsafeRawBufferPointer) throws -> R) throws -> R : $@convention(witness_method: ContiguousBytes) <τ_0_0 where τ_0_0 : ContiguousBytes><τ_1_0> (@noescape @callee_guaranteed (UnsafeRawBufferPointer) -> (@out τ_1_0, @error Error), @in_guaranteed τ_0_0) -> (@out τ_1_0, @error Error)
476+
// CHECK: apply [nothrow] [[WM]]<@opened("8402EC1A-F35D-11E8-950A-D0817AD9F6DD") ContiguousBytes, ()>(%{{.*}}, [[TTF]], [[OPENED]]) : $@convention(witness_method: ContiguousBytes) <τ_0_0 where τ_0_0 : ContiguousBytes><τ_1_0> (@noescape @callee_guaranteed (UnsafeRawBufferPointer) -> (@out τ_1_0, @error Error), @in_guaranteed τ_0_0) -> (@out τ_1_0, @error Error)
477+
// CHECK: destroy_addr [[ALLOC_EXISTENTIAL]] : $*ContiguousBytes
478+
// CHECK-LABEL: } // end sil function 'testDevirtExistentialEnum'
479+
sil shared [noinline] @testDevirtExistentialEnum : $@convention(thin) (@guaranteed Array<Int>) -> () {
480+
bb0(%0 : $Array<Int>):
481+
%3 = alloc_stack $ContiguousBytes
482+
%4 = alloc_stack $Optional<ContiguousBytes>
483+
%5 = init_enum_data_addr %4 : $*Optional<ContiguousBytes>, #Optional.some!enumelt.1
484+
%6 = init_existential_addr %5 : $*ContiguousBytes, $Array<Int>
485+
store %0 to %6 : $*Array<Int>
486+
inject_enum_addr %4 : $*Optional<ContiguousBytes>, #Optional.some!enumelt.1
487+
%9 = unchecked_take_enum_data_addr %4 : $*Optional<ContiguousBytes>, #Optional.some!enumelt.1
488+
copy_addr [take] %9 to [initialization] %3 : $*ContiguousBytes
489+
dealloc_stack %4 : $*Optional<ContiguousBytes>
490+
%12 = open_existential_addr immutable_access %3 : $*ContiguousBytes to $*@opened("8402EC1A-F35D-11E8-950A-D0817AD9F6DD") ContiguousBytes
491+
%13 = alloc_stack $()
492+
%14 = function_ref @testDevirtExistentialEnumThunk : $@convention(thin) (UnsafeRawBufferPointer) -> (@out (), @error Error)
493+
%15 = thin_to_thick_function %14 : $@convention(thin) (UnsafeRawBufferPointer) -> (@out (), @error Error) to $@noescape @callee_guaranteed (UnsafeRawBufferPointer) -> (@out (), @error Error)
494+
%16 = witness_method $@opened("8402EC1A-F35D-11E8-950A-D0817AD9F6DD") ContiguousBytes, #ContiguousBytes.withUnsafeBytes!1 : <Self where Self : ContiguousBytes><R> (Self) -> ((UnsafeRawBufferPointer) throws -> R) throws -> R, %12 : $*@opened("8402EC1A-F35D-11E8-950A-D0817AD9F6DD") ContiguousBytes : $@convention(witness_method: ContiguousBytes) <τ_0_0 where τ_0_0 : ContiguousBytes><τ_1_0> (@noescape @callee_guaranteed (UnsafeRawBufferPointer) -> (@out τ_1_0, @error Error), @in_guaranteed τ_0_0) -> (@out τ_1_0, @error Error)
495+
%21 = apply [nothrow] %16<@opened("8402EC1A-F35D-11E8-950A-D0817AD9F6DD") ContiguousBytes, ()>(%13, %15, %12) : $@convention(witness_method: ContiguousBytes) <τ_0_0 where τ_0_0 : ContiguousBytes><τ_1_0> (@noescape @callee_guaranteed (UnsafeRawBufferPointer) -> (@out τ_1_0, @error Error), @in_guaranteed τ_0_0) -> (@out τ_1_0, @error Error)
496+
dealloc_stack %13 : $*()
497+
destroy_addr %3 : $*ContiguousBytes
498+
dealloc_stack %3 : $*ContiguousBytes
499+
%25 = tuple ()
500+
return %25 : $()
501+
}

0 commit comments

Comments
 (0)