Skip to content

Commit ba92425

Browse files
authored
Merge pull request swiftlang#82286 from atrick/lifedep-endapply-assert
LifetimeDependenceScopeFixup: crash handling dead-end coroutine
2 parents d22a247 + 5b5f370 commit ba92425

File tree

4 files changed

+136
-34
lines changed

4 files changed

+136
-34
lines changed

SwiftCompilerSources/Sources/Optimizer/FunctionPasses/LifetimeDependenceScopeFixup.swift

Lines changed: 27 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -801,7 +801,7 @@ extension ExtendableScope {
801801
func canExtend(beginApply: BeginApplyInst, over range: inout InstructionRange, _ context: some Context) -> Bool {
802802
let canEndAtBoundary = { (boundaryInst: Instruction) in
803803
switch beginApply.endReaches(block: boundaryInst.parentBlock, context) {
804-
case .abortReaches, .endReaches:
804+
case .abortReaches, .endReaches, .deadEndReaches:
805805
return true
806806
case .none:
807807
return false
@@ -929,64 +929,62 @@ private extension BeginApplyInst {
929929
return builder.createEndApply(beginApply: self)
930930
case .abortReaches:
931931
return builder.createAbortApply(beginApply: self)
932+
case .deadEndReaches:
933+
return builder.createEndBorrow(of: self.token)
932934
}
933935
}
934936

935937
enum EndReaches {
936938
case endReaches
937939
case abortReaches
940+
case deadEndReaches
938941
}
939942

940943
/// Return the single kind of coroutine termination that reaches 'reachableBlock' or nil.
941944
func endReaches(block reachableBlock: BasicBlock, _ context: some Context) -> EndReaches? {
942-
var endBlocks = BasicBlockSet(context)
943-
var abortBlocks = BasicBlockSet(context)
945+
// TODO: use InlineArray<3> once bootstrapping is fixed.
946+
var endingBlockMap: [(EndReaches, BasicBlockSet)] = [
947+
(.endReaches, BasicBlockSet(context)),
948+
(.abortReaches, BasicBlockSet(context)),
949+
(.deadEndReaches, BasicBlockSet(context))
950+
]
944951
defer {
945-
endBlocks.deinitialize()
946-
abortBlocks.deinitialize()
952+
for index in endingBlockMap.indices {
953+
endingBlockMap[index].1.deinitialize()
954+
}
947955
}
948956
for endInst in endInstructions {
957+
let endKind: EndReaches
949958
switch endInst {
950959
case let endApply as EndApplyInst:
951960
// Cannot extend the scope of a coroutine when the resume produces a value.
952961
if !endApply.type.isEmpty(in: parentFunction) {
953962
return nil
954963
}
955-
endBlocks.insert(endInst.parentBlock)
964+
endKind = .endReaches
956965
case is AbortApplyInst:
957-
abortBlocks.insert(endInst.parentBlock)
966+
endKind = .abortReaches
967+
case is EndBorrowInst:
968+
endKind = .deadEndReaches
958969
default:
959970
fatalError("invalid begin_apply ending instruction")
960971
}
972+
let endingBlocksIndex = endingBlockMap.firstIndex(where: { $0.0 == endKind })!
973+
endingBlockMap[endingBlocksIndex].1.insert(endInst.parentBlock)
961974
}
962975
var endReaches: EndReaches?
963976
var backwardWalk = BasicBlockWorklist(context)
964977
defer { backwardWalk.deinitialize() }
965978

966979
let backwardVisit = { (block: BasicBlock) -> WalkResult in
967-
if endBlocks.contains(block) {
968-
switch endReaches {
969-
case .none:
970-
endReaches = .endReaches
971-
break
972-
case .endReaches:
973-
break
974-
case .abortReaches:
975-
return .abortWalk
980+
for (endKind, endingBlocks) in endingBlockMap {
981+
if endingBlocks.contains(block) {
982+
if let endReaches = endReaches, endReaches != endKind {
983+
return .abortWalk
984+
}
985+
endReaches = endKind
986+
return .continueWalk
976987
}
977-
return .continueWalk
978-
}
979-
if abortBlocks.contains(block) {
980-
switch endReaches {
981-
case .none:
982-
endReaches = .abortReaches
983-
break
984-
case .abortReaches:
985-
break
986-
case .endReaches:
987-
return .abortWalk
988-
}
989-
return .continueWalk
990988
}
991989
if block == self.parentBlock {
992990
// the insertion point is not dominated by the coroutine

docs/SIL/Instructions.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2615,11 +2615,11 @@ except:
26152615
instead of its normal results.
26162616

26172617
The final (in the case of `@yield_once`) or penultimate (in the case of
2618-
`@yield_once_2`) result of a `begin_apply` is a "token", a special
2619-
value which can only be used as the operand of an `end_apply` or
2620-
`abort_apply` instruction. Before this second instruction is executed,
2621-
the coroutine is said to be "suspended", and the token represents a
2622-
reference to its suspended activation record.
2618+
`@yield_once_2`) result of a `begin_apply` is a "token", a special value which
2619+
can only be used as the operand of an `end_apply`, `abort_apply`, or
2620+
`end_borrow` instruction. Before this second instruction is executed, the
2621+
coroutine is said to be "suspended", and the token represents a reference to its
2622+
suspended activation record.
26232623

26242624
If the coroutine's kind `yield_once_2`, its final result is an address
26252625
of a "token", representing the allocation done by the callee

test/SILOptimizer/lifetime_dependence/scope_fixup.sil

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ struct NCContainer : ~Copyable {
3939
var wrapper: Wrapper { get } // _read
4040
}
4141

42+
struct NCWrapper : ~Copyable, ~Escapable {
43+
@_hasStorage let a: NE { get }
44+
deinit
45+
}
46+
47+
sil @NCWrapper_getNE : $@convention(method) (@guaranteed NCWrapper) -> @lifetime(borrow 0) @owned NE
48+
4249
struct TrivialHolder {
4350
var pointer: UnsafeRawPointer
4451
}
@@ -86,6 +93,12 @@ sil @readAccess : $@yield_once @convention(method) (@guaranteed Holder) -> @life
8693
sil @yieldInoutHolder : $@yield_once @convention(method) (@inout Holder) -> @yields @inout Holder
8794
sil @yieldInoutNE : $@yield_once @convention(method) (@inout Holder) -> @lifetime(borrow 0) @owned NE
8895

96+
class C {
97+
@_hasStorage @_hasInitialValue private var nc: NCWrapper? { get set }
98+
}
99+
100+
sil @C_read : $@yield_once @convention(method) (@guaranteed C) -> @yields @guaranteed Optional<NCWrapper>
101+
89102
// NCContainer.wrapper._read:
90103
// var wrapper: Wrapper {
91104
// _read {
@@ -612,3 +625,56 @@ bb0(%0 : @owned $NCContainer):
612625
%31 = tuple ()
613626
return %31
614627
}
628+
629+
// rdar://153479358 (Compiler crash when force-unwrapping optional ~Copyable type)
630+
//
631+
// Handle dead end coroutines: begin_apply -> end_borrow
632+
// CHECK-LABEL: sil hidden [ossa] @testReadDeadEnd : $@convention(method) (@guaranteed C) -> () {
633+
// CHECK: bb0(%0 : @guaranteed $C):
634+
// CHECK: ({{.*}}, [[TOKEN:%[0-9]+]]) = begin_apply %{{.*}}(%0) : $@yield_once @convention(method) (@guaranteed C) -> @yields @guaranteed Optional<NCWrapper>
635+
// CHECK: switch_enum %{{.*}}, case #Optional.some!enumelt: bb2, case #Optional.none!enumelt: bb1
636+
// CHECK: bb1:
637+
// CHECK: destroy_value [dead_end]
638+
// CHECK: end_borrow [[TOKEN]]
639+
// CHECK: unreachable
640+
// CHECK: bb2(%{{.*}} : @guaranteed $NCWrapper):
641+
// CHECK: mark_dependence [unresolved]
642+
// CHECK: destroy_value
643+
// CHECK: destroy_value
644+
// CHECK: end_apply [[TOKEN]] as $()
645+
// CHECK-LABEL: } // end sil function 'testReadDeadEnd'
646+
sil hidden [ossa] @testReadDeadEnd : $@convention(method) (@guaranteed C) -> () {
647+
bb0(%0 : @guaranteed $C):
648+
// FIXME: I don't know how to print a lifetime-dependent class method in SIL.
649+
// %2 = class_method %0, #C.nc!read : (C) -> () -> (), $@yield_once @convention(method) (@guaranteed C) -> @yields @guaranteed Optional<NCWrapper>
650+
%2 = function_ref @C_read : $@yield_once @convention(method) (@guaranteed C) -> @yields @guaranteed Optional<NCWrapper>
651+
(%3, %4) = begin_apply %2(%0) : $@yield_once @convention(method) (@guaranteed C) -> @yields @guaranteed Optional<NCWrapper>
652+
%5 = copy_value %3
653+
%6 = mark_unresolved_non_copyable_value [no_consume_or_assign] %5
654+
%7 = alloc_stack $Optional<NCWrapper>
655+
%8 = mark_unresolved_non_copyable_value [no_consume_or_assign] %7
656+
%9 = store_borrow %6 to %8
657+
%10 = load_borrow [unchecked] %9
658+
switch_enum %10, case #Optional.some!enumelt: bb2, case #Optional.none!enumelt: bb1
659+
660+
bb1:
661+
end_borrow %10
662+
end_borrow %9
663+
destroy_value [dead_end] %6
664+
end_borrow %4
665+
unreachable
666+
667+
bb2(%25 : @guaranteed $NCWrapper):
668+
%26 = function_ref @NCWrapper_getNE : $@convention(method) (@guaranteed NCWrapper) -> @lifetime(borrow 0) @owned NE
669+
%27 = apply %26(%25) : $@convention(method) (@guaranteed NCWrapper) -> @lifetime(borrow 0) @owned NE
670+
%28 = mark_dependence [unresolved] %27 on %3
671+
end_borrow %10
672+
end_borrow %9
673+
destroy_value %6
674+
%32 = end_apply %4 as $()
675+
dealloc_stack %7
676+
%34 = move_value [var_decl] %28
677+
destroy_value %34
678+
%37 = tuple ()
679+
return %37
680+
}

test/SILOptimizer/lifetime_dependence/verify_diagnostics.swift

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,24 @@ func getImmutableSpan(_ array: inout [Int]) -> Span<Int> {
9090
return array.span
9191
}
9292

93+
struct NCInt: ~Copyable {
94+
var i: Int
95+
96+
@_lifetime(borrow self)
97+
func getNE() -> NEInt {
98+
NEInt(owner: self)
99+
}
100+
}
101+
102+
public struct NEInt: ~Escapable {
103+
var i: Int
104+
105+
@_lifetime(borrow owner)
106+
init(owner: borrowing NCInt) {
107+
self.i = owner.i
108+
}
109+
}
110+
93111
struct TestDeinitCallsAddressor: ~Copyable, ~Escapable {
94112
let a: Borrow<A>
95113

@@ -190,3 +208,23 @@ func testIndirectClosureResult<T>(f: () -> GNE<T>) -> GNE<T> {
190208
// expected-note @-3{{it depends on the lifetime of argument '$return_value'}}
191209
// expected-note @-3{{this use causes the lifetime-dependent value to escape}}
192210
}
211+
212+
// =============================================================================
213+
// Coroutines
214+
// =============================================================================
215+
216+
// Test _read of a noncopyable type with a dead-end (end_borrow)
217+
//
218+
// rdar://153479358 (Compiler crash when force-unwrapping optional ~Copyable type)
219+
class ClassStorage {
220+
private var nc: NCInt?
221+
222+
init(nc: consuming NCInt?) {
223+
self.nc = nc
224+
}
225+
226+
func readNoncopyable() {
227+
let ne = self.nc!.getNE()
228+
_ = ne
229+
}
230+
}

0 commit comments

Comments
 (0)