Skip to content

Commit ce18a4b

Browse files
authored
Merge pull request #79985 from atrick/fix-destroyhoist
Fix DestroyHoisting: InteriorLiveness for noescape closures
2 parents 39687f1 + 995c749 commit ce18a4b

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
@@ -61,11 +61,19 @@ public struct Type : TypeProperties, CustomStringConvertible, NoReflectionChildr
6161

6262
public var isMoveOnly: Bool { bridged.isMoveOnly() }
6363

64-
// Note that invalid types are not considered Escapable. This makes it difficult to make any assumptions about
65-
// nonescapable types.
64+
/// Return true if this type conforms to Escapable.
65+
///
66+
/// Note: invalid types are non-Escapable, so take care not to make assumptions about non-Escapable types.
6667
public func isEscapable(in function: Function) -> Bool {
6768
bridged.isEscapable(function.bridged)
6869
}
70+
71+
/// Return true if this type conforms to Escapable and is not a noescape function type.
72+
///
73+
/// Warning: may return true for (source-level) non-escaping closures. After SILGen, all partial_apply instructions
74+
/// have an escaping function type. ClosureLifetimeFixup only changes them to noescape function type if they are
75+
/// promoted to [on_stack]. But regardless of stack promotion, a non-escaping closure value's lifetime is constrained
76+
/// by the lifetime of its captures. Use Value.mayEscape instead to check for this case.
6977
public func mayEscape(in function: Function) -> Bool {
7078
!isNoEscapeFunction && isEscapable(in: function)
7179
}
@@ -173,16 +181,6 @@ public struct Type : TypeProperties, CustomStringConvertible, NoReflectionChildr
173181
}
174182
}
175183

176-
extension Value {
177-
public var isEscapable: Bool {
178-
type.objectType.isEscapable(in: parentFunction)
179-
}
180-
181-
public var mayEscape: Bool {
182-
type.objectType.mayEscape(in: parentFunction)
183-
}
184-
}
185-
186184
extension Type: Equatable {
187185
public static func ==(lhs: Type, rhs: Type) -> Bool {
188186
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)