From b8623000975ec2542cc997f9f57d2c63160f129f Mon Sep 17 00:00:00 2001 From: Andrew Trick Date: Fri, 30 May 2025 17:08:59 -0700 Subject: [PATCH 1/3] LifetimeDependenceDiagnostics: diagnose indirect closure results. Add support for diagnosing calls to closures that return a generic non-Escapable result. Closures do not yet model lifetime dependencies. The diagnostics have a special case for handling nonescaple result with no lifetime dependence, but it previously only handled direct results. This fix handles cases like the following: func callIndirectClosure(f: () -> NE) -> NE { f() } Fixes rdar://134318846 ([nonescapable] diagnose function types with nonescapable results) (cherry picked from commit 1d09c06ab15cbebf25d11c6fa95a660c99a14d3a) --- .../LifetimeDependenceDiagnostics.swift | 14 +++++++++----- .../Utilities/LifetimeDependenceUtils.swift | 17 +++++++---------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/SwiftCompilerSources/Sources/Optimizer/FunctionPasses/LifetimeDependenceDiagnostics.swift b/SwiftCompilerSources/Sources/Optimizer/FunctionPasses/LifetimeDependenceDiagnostics.swift index 145562136fc87..b36745acbd076 100644 --- a/SwiftCompilerSources/Sources/Optimizer/FunctionPasses/LifetimeDependenceDiagnostics.swift +++ b/SwiftCompilerSources/Sources/Optimizer/FunctionPasses/LifetimeDependenceDiagnostics.swift @@ -72,12 +72,16 @@ let lifetimeDependenceDiagnosticsPass = FunctionPass( markDep.settleToEscaping() continue } - if let apply = instruction as? FullApplySite { - // Handle ~Escapable results that do not have a lifetime dependence. This includes implicit initializers and - // @_unsafeNonescapableResult. + if let apply = instruction as? FullApplySite, !apply.hasResultDependence { + // Handle ~Escapable results that do not have a lifetime dependence. This includes implicit initializers, calls to + // closures, and @_unsafeNonescapableResult. apply.resultOrYields.forEach { - if let lifetimeDep = LifetimeDependence(unsafeApplyResult: $0, - context) { + if let lifetimeDep = LifetimeDependence(unsafeApplyResult: $0, apply: apply, context) { + _ = analyze(dependence: lifetimeDep, context) + } + } + apply.indirectResultOperands.forEach { + if let lifetimeDep = LifetimeDependence(unsafeApplyResult: $0.value, apply: apply, context) { _ = analyze(dependence: lifetimeDep, context) } } diff --git a/SwiftCompilerSources/Sources/Optimizer/Utilities/LifetimeDependenceUtils.swift b/SwiftCompilerSources/Sources/Optimizer/Utilities/LifetimeDependenceUtils.swift index 355c1d33f0c53..b745fb5dff86a 100644 --- a/SwiftCompilerSources/Sources/Optimizer/Utilities/LifetimeDependenceUtils.swift +++ b/SwiftCompilerSources/Sources/Optimizer/Utilities/LifetimeDependenceUtils.swift @@ -165,16 +165,12 @@ extension LifetimeDependence { // // This is necessary because inserting a mark_dependence placeholder for such an unsafe dependence would illegally // have the same base and value operand. - // - // TODO: handle indirect results - init?(unsafeApplyResult value: Value, _ context: some Context) { + init?(unsafeApplyResult value: Value, apply: FullApplySite, _ context: some Context) { if value.isEscapable { return nil } - if (value.definingInstructionOrTerminator as! FullApplySite).hasResultDependence { - return nil - } - assert(value.ownership == .owned, "unsafe apply result must be owned") + assert(!apply.hasResultDependence, "mark_dependence should be used instead") + assert(value.ownership == .owned || value.type.isAddress, "unsafe apply result must be owned") self.scope = Scope(base: value, context) self.dependentValue = value self.markDepInst = nil @@ -578,8 +574,8 @@ extension LifetimeDependenceDefUseWalker { } let root = dependence.dependentValue if root.type.isAddress { - // The root address may be an escapable mark_dependence that guards its address uses (unsafeAddress), or an - // allocation or incoming argument. In all these cases, it is sufficient to walk down the address uses. + // The root address may be an escapable mark_dependence that guards its address uses (unsafeAddress), an + // allocation, an incoming argument, or an outgoing argument. In all these cases, walk down the address uses. return walkDownAddressUses(of: root) } return walkDownUses(of: root, using: nil) @@ -974,7 +970,8 @@ extension LifetimeDependenceDefUseWalker { private mutating func visitAppliedUse(of operand: Operand, by apply: FullApplySite) -> WalkResult { if let conv = apply.convention(of: operand), conv.isIndirectOut { - return leafUse(of: operand) + // This apply initializes an allocation. + return dependentUse(of: operand, dependentAddress: operand.value) } if apply.isCallee(operand: operand) { return leafUse(of: operand) From fb2a6ee9a8654b15b6fb1b0f1edce61d4dc1b464 Mon Sep 17 00:00:00 2001 From: Andrew Trick Date: Tue, 3 Jun 2025 23:34:30 -0700 Subject: [PATCH 2/3] LifetimeDependenceDiagnostics: fix source loc for implicit variables Diagnostics on an indirect result ($return_value) did not report a source location. (cherry picked from commit c030b78c7d1a95348da43c612e17b3b60a59459b) --- .../LifetimeDependenceDiagnostics.swift | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/SwiftCompilerSources/Sources/Optimizer/FunctionPasses/LifetimeDependenceDiagnostics.swift b/SwiftCompilerSources/Sources/Optimizer/FunctionPasses/LifetimeDependenceDiagnostics.swift index b36745acbd076..9c529995270a9 100644 --- a/SwiftCompilerSources/Sources/Optimizer/FunctionPasses/LifetimeDependenceDiagnostics.swift +++ b/SwiftCompilerSources/Sources/Optimizer/FunctionPasses/LifetimeDependenceDiagnostics.swift @@ -241,10 +241,12 @@ private struct DiagnoseDependence { onError() // Identify the escaping variable. - let escapingVar = LifetimeVariable(dependent: operand.value, context) + let escapingVar = LifetimeVariable(usedBy: operand, context) if let varDecl = escapingVar.varDecl { // Use the variable location, not the access location. - diagnose(varDecl.nameLoc, .lifetime_variable_outside_scope, escapingVar.name ?? "") + // Variable names like $return_value and $implicit_value don't have source locations. + let sourceLoc = varDecl.nameLoc ?? escapingVar.sourceLoc + diagnose(sourceLoc, .lifetime_variable_outside_scope, escapingVar.name ?? "") } else if let sourceLoc = escapingVar.sourceLoc { diagnose(sourceLoc, .lifetime_value_outside_scope) } else { @@ -267,7 +269,7 @@ private struct DiagnoseDependence { // Identify the dependence scope. If no source location is found, bypass this diagnostic. func reportScope() { - let parentVar = LifetimeVariable(dependent: dependence.parentValue, context) + let parentVar = LifetimeVariable(definedBy: dependence.parentValue, context) // First check if the dependency is limited to an access scope. If the access has no source location then // fall-through to report possible dependence on an argument. if parentVar.isAccessScope, let accessLoc = parentVar.sourceLoc { @@ -314,7 +316,22 @@ private struct LifetimeVariable { return varDecl?.userFacingName } - init(dependent value: Value, _ context: some Context) { + init(usedBy operand: Operand, _ context: some Context) { + self = .init(dependent: operand.value, context) + // variable names like $return_value and $implicit_value don't have source locations. + // For @out arguments, the operand's location is the best answer. + // Otherwise, fall back to the function's location. + self.sourceLoc = self.sourceLoc ?? operand.instruction.location.sourceLoc + ?? operand.instruction.parentFunction.location.sourceLoc + } + + init(definedBy value: Value, _ context: some Context) { + self = .init(dependent: value, context) + // Fall back to the function's location. + self.sourceLoc = self.sourceLoc ?? value.parentFunction.location.sourceLoc + } + + private init(dependent value: Value, _ context: some Context) { guard let introducer = getFirstVariableIntroducer(of: value, context) else { return } From 4caca644ed960ee6d242f7a701fc6a9b756f7c7e Mon Sep 17 00:00:00 2001 From: Andrew Trick Date: Wed, 4 Jun 2025 11:56:48 -0700 Subject: [PATCH 3/3] Add unit tests for indirect lifetime dependence. (cherry picked from commit b1044c62e571813344370a32938c8b1a20e972fb) --- .../verify_diagnostics.swift | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/test/SILOptimizer/lifetime_dependence/verify_diagnostics.swift b/test/SILOptimizer/lifetime_dependence/verify_diagnostics.swift index dffe8c34dc9fb..5cc4abab5e066 100644 --- a/test/SILOptimizer/lifetime_dependence/verify_diagnostics.swift +++ b/test/SILOptimizer/lifetime_dependence/verify_diagnostics.swift @@ -49,6 +49,17 @@ struct Holder { var c: C? = nil } +// Generic non-Escapable for indirect values. +struct GNE : ~Escapable { + let t: T + @lifetime(borrow t) + init(t: borrowing T) { self.t = copy t } +} + +@_silgen_name("forward") +@lifetime(copy arg) +func forward(_ arg: GNE) -> GNE + @_silgen_name("getGeneric") @lifetime(borrow holder) func getGeneric(_ holder: borrowing Holder, _: T.Type) -> T @@ -156,3 +167,26 @@ func testClosureCapture1(_ a: HasMethods) { } */ } + +// ============================================================================= +// Indirect ~Escapable results +// ============================================================================= + +@lifetime(copy arg1) +func testIndirectForwardedResult(arg1: GNE) -> GNE { + forward(arg1) +} + +@lifetime(copy arg1) +func testIndirectNonForwardedResult(arg1: GNE, arg2: GNE) -> GNE { + // expected-error @-1{{lifetime-dependent variable 'arg2' escapes its scope}} + // expected-note @-2{{it depends on the lifetime of argument 'arg2'}} + forward(arg2) // expected-note {{this use causes the lifetime-dependent value to escape}} +} + +func testIndirectClosureResult(f: () -> GNE) -> GNE { + f() + // expected-error @-1{{lifetime-dependent variable '$return_value' escapes its scope}} + // expected-note @-3{{it depends on the lifetime of argument '$return_value'}} + // expected-note @-3{{this use causes the lifetime-dependent value to escape}} +}