Skip to content

Commit 995c749

Browse files
committed
Fix DestroyHoisting: InteriorLiveness for noescape closures
InteriorLiveness has a new "visitInnerUses" mode used by DestroyHoisting. That mode may visit dependent values, which was not valid for noescape closures. ClosureLifetimeFixup inserts destroys of noescape closures after the destroys of the captures. So following such dependent value could result in an apparent use-after-destroy. This causes DestroyHoisting to insert redundant destroys. Fix: InteriorUses will conservatively only follow dependent values if they are escapable. Non-escapable values, like noescape closures are now considered escapes of the original value that the non-escapable value depends on. This can be improved in the future, but we may want to rewrite ClosureLifetimeFixup first. Fixes the root cause of: rdar://146142041
1 parent 42ad91e commit 995c749

File tree

6 files changed

+111
-18
lines changed

6 files changed

+111
-18
lines changed

SwiftCompilerSources/Sources/Optimizer/Utilities/OwnershipLiveness.swift

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -715,8 +715,17 @@ extension InteriorUseWalker: AddressUseVisitor {
715715

716716
mutating func dependentAddressUse(of operand: Operand, into value: Value)
717717
-> WalkResult {
718-
walkDownUses(of: value)
719-
}
718+
// For Escapable values, simply continue the walk.
719+
if value.mayEscape {
720+
return walkDownUses(of: value)
721+
}
722+
// TODO: Handle non-escapable values by walking through copies as done by LifetimeDependenceDefUseWalker or
723+
// NonEscapingClosureDefUseWalker. But this code path also handles non-escaping closures that have not been promoted
724+
// to [on_stack] (and still have an escapable function type). Such closures may be incorrectly destroyed after their
725+
// captures. To avoid this problem, either rewrite ClosureLifetimeFixup to produce correct OSSA lifetimes, or check
726+
// for that special case and continue to bailout here.
727+
return escapingAddressUse(of: operand)
728+
}
720729

721730
mutating func escapingAddressUse(of operand: Operand) -> WalkResult {
722731
pointerStatus.setEscaping(operand: operand)
@@ -830,12 +839,14 @@ let linearLivenessTest = FunctionTest("linear_liveness_swift") {
830839
let interiorLivenessTest = FunctionTest("interior_liveness_swift") {
831840
function, arguments, context in
832841
let value = arguments.takeValue()
833-
print("Interior liveness: \(value)")
842+
let visitInnerUses = arguments.hasUntaken ? arguments.takeBool() : false
843+
844+
print("Interior liveness\(visitInnerUses ? " with inner uses" : ""): \(value)")
834845

835846
var range = InstructionRange(for: value, context)
836847
defer { range.deinitialize() }
837848

838-
var visitor = InteriorUseWalker(definingValue: value, ignoreEscape: true, visitInnerUses: false, context) {
849+
var visitor = InteriorUseWalker(definingValue: value, ignoreEscape: true, visitInnerUses: visitInnerUses, context) {
839850
range.insert($0.instruction)
840851
return .continueWalk
841852
}

SwiftCompilerSources/Sources/SIL/Instruction.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1182,10 +1182,15 @@ class ClassifyBridgeObjectInst : SingleValueInstruction, UnaryInstruction {}
11821182
final public class PartialApplyInst : SingleValueInstruction, ApplySite {
11831183
public var numArguments: Int { bridged.PartialApplyInst_numArguments() }
11841184

1185-
/// WARNING: isOnStack incorrectly returns false for all closures prior to ClosureLifetimeFixup, even if they need to
1186-
/// be diagnosed as on-stack closures. Use has mayEscape instead.
1185+
/// Warning: isOnStack returns false for all closures prior to ClosureLifetimeFixup, even if they capture on-stack
1186+
/// addresses and need to be diagnosed as non-escaping closures. Use mayEscape to determine whether a closure is
1187+
/// non-escaping prior to ClosureLifetimeFixup.
11871188
public var isOnStack: Bool { bridged.PartialApplyInst_isOnStack() }
11881189

1190+
// Warning: ClosureLifetimeFixup does not promote all non-escaping closures to on-stack. When that promotion fails, it
1191+
// creates a fake destroy of the closure after the captured values that the closure depends on. This is invalid OSSA,
1192+
// so OSSA utilities need to bail-out when isOnStack is false even if hasNoescapeCapture is true to avoid encoutering
1193+
// an invalid nested lifetime.
11891194
public var mayEscape: Bool { !isOnStack && !hasNoescapeCapture }
11901195

11911196
/// True if this closure captures anything nonescaping.

SwiftCompilerSources/Sources/SIL/Type.swift

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,19 @@ public struct Type : CustomStringConvertible, NoReflectionChildren {
7373

7474
public var isMoveOnly: Bool { bridged.isMoveOnly() }
7575

76-
// Note that invalid types are not considered Escapable. This makes it difficult to make any assumptions about
77-
// nonescapable types.
76+
/// Return true if this type conforms to Escapable.
77+
///
78+
/// Note: invalid types are non-Escapable, so take care not to make assumptions about non-Escapable types.
7879
public func isEscapable(in function: Function) -> Bool {
7980
bridged.isEscapable(function.bridged)
8081
}
82+
83+
/// Return true if this type conforms to Escapable and is not a noescape function type.
84+
///
85+
/// Warning: may return true for (source-level) non-escaping closures. After SILGen, all partial_apply instructions
86+
/// have an escaping function type. ClosureLifetimeFixup only changes them to noescape function type if they are
87+
/// promoted to [on_stack]. But regardless of stack promotion, a non-escaping closure value's lifetime is constrained
88+
/// by the lifetime of its captures. Use Value.mayEscape instead to check for this case.
8189
public func mayEscape(in function: Function) -> Bool {
8290
!isNoEscapeFunction && isEscapable(in: function)
8391
}
@@ -204,16 +212,6 @@ public struct Type : CustomStringConvertible, NoReflectionChildren {
204212
}
205213
}
206214

207-
extension Value {
208-
public var isEscapable: Bool {
209-
type.objectType.isEscapable(in: parentFunction)
210-
}
211-
212-
public var mayEscape: Bool {
213-
type.objectType.mayEscape(in: parentFunction)
214-
}
215-
}
216-
217215
extension Type: Equatable {
218216
public static func ==(lhs: Type, rhs: Type) -> Bool {
219217
lhs.bridged.opaqueValue == rhs.bridged.opaqueValue

SwiftCompilerSources/Sources/SIL/Value.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,31 @@ extension Value {
140140
}
141141
}
142142

143+
/// Return true if the object type conforms to Escapable.
144+
///
145+
/// Note: noescape function types conform to Escapable, use mayEscape instead to exclude them.
146+
public var isEscapable: Bool {
147+
type.objectType.isEscapable(in: parentFunction)
148+
}
149+
150+
/// Return true only if this value's lifetime is unconstrained by an outer lifetime. Requires all of the following:
151+
/// - the object type conforms to Escapable
152+
/// - the type is not a noescape function
153+
/// - the value is not the direct result of a partial_apply with a noescape (inout_aliasable) capture.
154+
public var mayEscape: Bool {
155+
if !type.objectType.mayEscape(in: parentFunction) {
156+
return false
157+
}
158+
// A noescape partial_apply has an escaping function type if it has not been promoted to on_stack, but it's value
159+
// still cannot "escape" its captures.
160+
//
161+
// TODO: This would be much more robust if pai.hasNoescapeCapture simply implied !pai.type.isEscapable
162+
if let pai = self as? PartialApplyInst {
163+
return pai.mayEscape
164+
}
165+
return true
166+
}
167+
143168
public var definingInstructionOrTerminator: Instruction? {
144169
if let def = definingInstruction {
145170
return def

lib/SIL/Verifier/SILVerifier.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2202,6 +2202,9 @@ class SILVerifier : public SILVerifierBase<SILVerifier> {
22022202
require(resultInfo->getExtInfo().hasContext(),
22032203
"result of closure cannot have a thin function type");
22042204

2205+
require(PAI->getType().isNoEscapeFunction() == PAI->isOnStack(),
2206+
"closure must be on stack to have a noescape function type");
2207+
22052208
// We rely on all indirect captures to be in the argument list.
22062209
require(PAI->getCallee()->getType().isObject(),
22072210
"Closure callee must not be an address type.");

test/SILOptimizer/ownership_liveness_unit.sil

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ struct NE: ~Escapable {
4343
init(object: borrowing C) { self.object = copy object }
4444
}
4545

46+
sil [ossa] @useCAddress : $@convention(thin) (@inout_aliasable C) -> ()
47+
48+
4649
// =============================================================================
4750
// LinearLiveness
4851
// =============================================================================
@@ -1948,6 +1951,54 @@ bb1(%reborrow2 : @reborrow $D):
19481951
return %99 : $()
19491952
}
19501953

1954+
// CHECK-LABEL: partial_apply_noescape_onheap: interior_liveness_swift with: %box, true
1955+
// CHECK: Interior liveness with inner uses: %{{.*}} = alloc_box ${ var C }
1956+
// CHECK-NEXT: Inner scope: %{{.*}} = begin_borrow [lexical] %{{.*}} : ${ var C }
1957+
// CHECK-NEXT: Pointer escape: %{{.*}} = partial_apply [callee_guaranteed] %{{.*}}(%{{.*}}) : $@convention(thin) (@inout_aliasable C) -> ()
1958+
// CHECK-NEXT: begin: %{{.*}} = alloc_box ${ var C }
1959+
// CHECK-NEXT: ends: destroy_value %{{.*}} : ${ var C }
1960+
// CHECK-NEXT: exits:
1961+
// CHECK-NEXT: interiors: end_borrow %{{.*}} : ${ var C }
1962+
// CHECK-NEXT: %{{.*}} = partial_apply [callee_guaranteed] %{{.*}}(%{{.*}}) : $@convention(thin) (@inout_aliasable C) -> ()
1963+
// CHECK-NEXT: %{{.*}} = project_box %{{.*}} : ${ var C }, 0
1964+
// CHECK-NEXT: %{{.*}} = begin_borrow [lexical] %3 : ${ var C }
1965+
// CHECK-NEXT: last user: destroy_value %3 : ${ var C }
1966+
// CHECK-NEXT: partial_apply_noescape_onheap: interior_liveness_swift with: %box, true
1967+
sil [ossa] @partial_apply_noescape_onheap : $@convention(thin) () -> () {
1968+
bb0:
1969+
cond_br undef, bb2, bb1
1970+
1971+
bb1:
1972+
%1 = enum $Optional<@callee_guaranteed () -> ()>, #Optional.none!enumelt
1973+
br bb4(%1)
1974+
1975+
bb2:
1976+
%box = alloc_box ${ var C }
1977+
%borrow = begin_borrow [lexical] %box
1978+
specify_test "interior_liveness_swift %box true"
1979+
%adr = project_box %borrow : ${ var C }, 0
1980+
br bb3
1981+
1982+
bb3:
1983+
%f = function_ref @useCAddress : $@convention(thin) (@inout_aliasable C) -> ()
1984+
%pa1 = partial_apply [callee_guaranteed] %f(%adr) : $@convention(thin) (@inout_aliasable C) -> ()
1985+
%pa2 = copy_value %pa1
1986+
%pane = convert_escape_to_noescape %pa2 : $@callee_guaranteed () -> () to $@noescape @callee_guaranteed () -> ()
1987+
%enum = enum $Optional<@callee_guaranteed () -> ()>, #Optional.some!enumelt, %pa2
1988+
end_borrow %borrow
1989+
destroy_value %box
1990+
destroy_value %pa1
1991+
destroy_value %pane
1992+
br bb4(%enum)
1993+
1994+
bb4(%phi : @owned $Optional<@callee_guaranteed () -> ()>):
1995+
// ClosureLifetimeFixup destroys closures after destroying its addressable captures:
1996+
// this is bad OSSA but still accepted as valid until ClosureLifetimeFixup is rewritten.
1997+
destroy_value %phi
1998+
%r = tuple ()
1999+
return %r
2000+
}
2001+
19512002
// =============================================================================
19522003
// ExtendedLiveness
19532004
// =============================================================================

0 commit comments

Comments
 (0)