From c20142afe094794f0db5da536ecdd34cf164e918 Mon Sep 17 00:00:00 2001 From: Andrew Trick Date: Thu, 23 Oct 2025 23:03:58 -0700 Subject: [PATCH 01/19] [NFC] Add ReturnInstruction protocol --- SwiftCompilerSources/Sources/SIL/Instruction.swift | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/SwiftCompilerSources/Sources/SIL/Instruction.swift b/SwiftCompilerSources/Sources/SIL/Instruction.swift index dddd61e6e3905..cdd7676fdd192 100644 --- a/SwiftCompilerSources/Sources/SIL/Instruction.swift +++ b/SwiftCompilerSources/Sources/SIL/Instruction.swift @@ -1854,13 +1854,17 @@ public class TermInst : Instruction { final public class UnreachableInst : TermInst { } -final public class ReturnInst : TermInst, UnaryInstruction { +public protocol ReturnInstruction: TermInst { + var returnedValue: Value { get } +} + +final public class ReturnInst : TermInst, UnaryInstruction, ReturnInstruction { public var returnedValue: Value { operand.value } public override var isFunctionExiting: Bool { true } } -final public class ReturnBorrowInst : TermInst { - public var returnValue: Value { operands[0].value } +final public class ReturnBorrowInst : TermInst, ReturnInstruction { + public var returnedValue: Value { operands[0].value } public var enclosingOperands: OperandArray { let ops = operands return ops[1.. Date: Sat, 11 Oct 2025 20:30:46 -0700 Subject: [PATCH 02/19] [NFC] Rename ArgumentConventions.parameterIndex(ofArgumentIndex:) --- SwiftCompilerSources/Sources/SIL/Argument.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/SwiftCompilerSources/Sources/SIL/Argument.swift b/SwiftCompilerSources/Sources/SIL/Argument.swift index 020918dafa80a..7255c9c9f77ef 100644 --- a/SwiftCompilerSources/Sources/SIL/Argument.swift +++ b/SwiftCompilerSources/Sources/SIL/Argument.swift @@ -290,7 +290,7 @@ public struct ArgumentConventions : Collection, CustomStringConvertible { } public subscript(_ argumentIndex: Int) -> ArgumentConvention { - if let paramIdx = parameterIndex(for: argumentIndex) { + if let paramIdx = parameterIndex(ofArgumentIndex: argumentIndex) { return convention.parameters[paramIdx].convention } let resultInfo = convention.indirectSILResult(at: argumentIndex) @@ -298,21 +298,21 @@ public struct ArgumentConventions : Collection, CustomStringConvertible { } public subscript(result argumentIndex: Int) -> ResultInfo? { - if parameterIndex(for: argumentIndex) != nil { + if parameterIndex(ofArgumentIndex: argumentIndex) != nil { return nil } return convention.indirectSILResult(at: argumentIndex) } public subscript(parameter argumentIndex: Int) -> ParameterInfo? { - guard let paramIdx = parameterIndex(for: argumentIndex) else { + guard let paramIdx = parameterIndex(ofArgumentIndex: argumentIndex) else { return nil } return convention.parameters[paramIdx] } public subscript(parameterDependencies targetArgumentIndex: Int) -> FunctionConvention.LifetimeDependencies? { - guard let targetParamIdx = parameterIndex(for: targetArgumentIndex) else { + guard let targetParamIdx = parameterIndex(ofArgumentIndex: targetArgumentIndex) else { return nil } return convention.parameterDependencies(for: targetParamIdx) @@ -365,14 +365,14 @@ public struct ArgumentConventions : Collection, CustomStringConvertible { } extension ArgumentConventions { - private func parameterIndex(for argIdx: Int) -> Int? { + private func parameterIndex(ofArgumentIndex argIdx: Int) -> Int? { let firstParamIdx = firstParameterIndex // bridging call return argIdx < firstParamIdx ? nil : argIdx - firstParamIdx } private func findDependence(source argumentIndex: Int, in dependencies: FunctionConvention.LifetimeDependencies?) -> LifetimeDependenceConvention? { - guard let paramIdx = parameterIndex(for: argumentIndex) else { + guard let paramIdx = parameterIndex(ofArgumentIndex: argumentIndex) else { return nil } return dependencies?[paramIdx] From 3bb3e4d077eddac014bfec344d64761437a3bcf1 Mon Sep 17 00:00:00 2001 From: Andrew Trick Date: Sat, 11 Oct 2025 20:49:10 -0700 Subject: [PATCH 03/19] [NFC] Add ApplySite.parameterDependence(target:source:) --- .../LifetimeDependenceDiagnostics.swift | 3 ++- .../Utilities/FunctionSignatureTransforms.swift | 2 +- SwiftCompilerSources/Sources/SIL/ApplySite.swift | 13 +++++++++++++ SwiftCompilerSources/Sources/SIL/Argument.swift | 11 +++++------ 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/SwiftCompilerSources/Sources/Optimizer/FunctionPasses/LifetimeDependenceDiagnostics.swift b/SwiftCompilerSources/Sources/Optimizer/FunctionPasses/LifetimeDependenceDiagnostics.swift index b7bd49bb7923a..235fb86caf13a 100644 --- a/SwiftCompilerSources/Sources/Optimizer/FunctionPasses/LifetimeDependenceDiagnostics.swift +++ b/SwiftCompilerSources/Sources/Optimizer/FunctionPasses/LifetimeDependenceDiagnostics.swift @@ -184,7 +184,8 @@ private struct DiagnoseDependence { if inoutArg == sourceArg { return .continueWalk } - if function.argumentConventions.getDependence(target: inoutArg.index, source: sourceArg.index) != nil { + if function.argumentConventions.parameterDependence(targetArgumentIndex: inoutArg.index, + sourceArgumentIndex: sourceArg.index) != nil { // The inout result depends on a lifetime that is inherited or borrowed in the caller. log(" has dependent inout argument: \(inoutArg)") return .continueWalk diff --git a/SwiftCompilerSources/Sources/Optimizer/Utilities/FunctionSignatureTransforms.swift b/SwiftCompilerSources/Sources/Optimizer/Utilities/FunctionSignatureTransforms.swift index 310dec2c9f9c9..08050e4c58d97 100644 --- a/SwiftCompilerSources/Sources/Optimizer/Utilities/FunctionSignatureTransforms.swift +++ b/SwiftCompilerSources/Sources/Optimizer/Utilities/FunctionSignatureTransforms.swift @@ -26,7 +26,7 @@ private extension ArgumentConventions { // Check if `argIndex` is a lifetime source in parameterDependencies for targetIndex in firstParameterIndex.. LifetimeDependenceConvention? { + guard let targetArgIdx = calleeArgumentIndex(ofOperandIndex: targetOperandIndex), + let sourceArgIdx = calleeArgumentIndex(ofOperandIndex: sourceOperandIndex) else { + return nil + } + return calleeArgumentConventions.parameterDependence(targetArgumentIndex: targetArgIdx, + sourceArgumentIndex: sourceArgIdx) + } + public var firstParameterOperandIndex: Int { return ApplyOperandConventions.firstArgumentIndex + calleeArgumentConventions.firstParameterIndex @@ -247,6 +256,10 @@ extension ApplySite { : operandConventions[parameterDependencies: idx] } + public func parameterDependence(target: Operand, source: Operand) -> LifetimeDependenceConvention? { + return operandConventions.parameterDependence(targetOperandIndex: target.index, sourceOperandIndex: source.index) + } + public var yieldConventions: YieldConventions { YieldConventions(convention: functionConvention) } diff --git a/SwiftCompilerSources/Sources/SIL/Argument.swift b/SwiftCompilerSources/Sources/SIL/Argument.swift index 7255c9c9f77ef..837489c4b3a5e 100644 --- a/SwiftCompilerSources/Sources/SIL/Argument.swift +++ b/SwiftCompilerSources/Sources/SIL/Argument.swift @@ -318,17 +318,16 @@ public struct ArgumentConventions : Collection, CustomStringConvertible { return convention.parameterDependencies(for: targetParamIdx) } + /// Return a parameter dependence of the target argument on the source argument. + public func parameterDependence(targetArgumentIndex: Int, sourceArgumentIndex: Int) -> LifetimeDependenceConvention? { + findDependence(source: sourceArgumentIndex, in: self[parameterDependencies: targetArgumentIndex]) + } + /// Return a dependence of the function results on the indexed parameter. public subscript(resultDependsOn argumentIndex: Int) -> LifetimeDependenceConvention? { findDependence(source: argumentIndex, in: convention.resultDependencies) } - /// Return a dependence of the target argument on the source argument. - public func getDependence(target targetArgumentIndex: Int, source sourceArgumentIndex: Int) - -> LifetimeDependenceConvention? { - findDependence(source: sourceArgumentIndex, in: self[parameterDependencies: targetArgumentIndex]) - } - /// Number of SIL arguments for the function type's results /// including the error result. Use this to avoid lazy iteration /// over indirectSILResults to find the count. From 1bf600db9b3a29075ee82deec88a040cb70dce0a Mon Sep 17 00:00:00 2001 From: Andrew Trick Date: Fri, 10 Oct 2025 16:29:56 -0700 Subject: [PATCH 04/19] [NFC] Add ApplySite.fullyAssigns(Operand) query --- .../Sources/SIL/ApplySite.swift | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/SwiftCompilerSources/Sources/SIL/ApplySite.swift b/SwiftCompilerSources/Sources/SIL/ApplySite.swift index 85f8ae247d235..ce4d502ba2221 100644 --- a/SwiftCompilerSources/Sources/SIL/ApplySite.swift +++ b/SwiftCompilerSources/Sources/SIL/ApplySite.swift @@ -122,6 +122,22 @@ public protocol ApplySite : Instruction { var unappliedArgumentCount: Int { get } } +// lattice: no -> lifetime -> value +public enum IsFullyAssigned { + case no + case lifetime + case value + + var reassignsLifetime: Bool { + switch self { + case .no: + false + case .lifetime, .value: + true + } + } +} + extension ApplySite { public var callee: Value { operands[ApplyOperandConventions.calleeIndex].value } @@ -260,6 +276,26 @@ extension ApplySite { return operandConventions.parameterDependence(targetOperandIndex: target.index, sourceOperandIndex: source.index) } + /// Returns .value if this apply fully assigns 'operand' (via @out). + /// + /// Returns .lifetime if this 'operand' is a non-Escapable inout argument and its lifetime is not propagated by the + /// call ('@lifetime(param: copy param)' is not present). + public func fullyAssigns(operand: Operand) -> IsFullyAssigned { + switch convention(of: operand) { + case .indirectOut: + return .value + case .indirectInout: + if let argIdx = calleeArgumentIndex(of: operand), + calleeArgumentConventions.parameterDependence(targetArgumentIndex: argIdx, sourceArgumentIndex: argIdx) == nil + { + return .lifetime + } + return .no + default: + return .no + } + } + public var yieldConventions: YieldConventions { YieldConventions(convention: functionConvention) } From 432e31612a6783aa3cafc3442cbb32932438f9eb Mon Sep 17 00:00:00 2001 From: Andrew Trick Date: Fri, 10 Oct 2025 16:26:49 -0700 Subject: [PATCH 05/19] [NFC] Extend AddressInitializationWalker.findSingleInitializer Handle applies that reassign the lifetime of their operand vs. the value of their operand. --- .../Optimizer/Utilities/AddressUtils.swift | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/SwiftCompilerSources/Sources/Optimizer/Utilities/AddressUtils.swift b/SwiftCompilerSources/Sources/Optimizer/Utilities/AddressUtils.swift index 3f1b5b50cea8b..5887682b1ca62 100644 --- a/SwiftCompilerSources/Sources/Optimizer/Utilities/AddressUtils.swift +++ b/SwiftCompilerSources/Sources/Optimizer/Utilities/AddressUtils.swift @@ -279,7 +279,7 @@ extension AccessBase { default: return nil } - return AddressInitializationWalker.findSingleInitializer(ofAddress: baseAddr, context: context) + return AddressInitializationWalker.findSingleInitializer(ofAddress: baseAddr, requireFullyAssigned: .value, context) } } @@ -311,6 +311,7 @@ extension AccessBase { // modification of memory. struct AddressInitializationWalker: AddressDefUseWalker, AddressUseVisitor { let baseAddress: Value + let requireFullyAssigned: IsFullyAssigned let context: any Context var walkDownCache = WalkerCache() @@ -318,18 +319,21 @@ struct AddressInitializationWalker: AddressDefUseWalker, AddressUseVisitor { var isProjected = false var initializer: AccessBase.Initializer? - static func findSingleInitializer(ofAddress baseAddr: Value, context: some Context) + static func findSingleInitializer(ofAddress baseAddr: Value, requireFullyAssigned: IsFullyAssigned, + _ context: some Context) -> AccessBase.Initializer? { - var walker = AddressInitializationWalker(baseAddress: baseAddr, context) + var walker = AddressInitializationWalker(baseAddress: baseAddr, requireFullyAssigned, context) if walker.walkDownUses(ofAddress: baseAddr, path: SmallProjectionPath()) == .abortWalk { return nil } return walker.initializer } - private init(baseAddress: Value, _ context: some Context) { + private init(baseAddress: Value, _ requireFullyAssigned: IsFullyAssigned, _ context: some Context) { + assert(requireFullyAssigned != .no) self.baseAddress = baseAddress + self.requireFullyAssigned = requireFullyAssigned self.context = context if let arg = baseAddress as? FunctionArgument { assert(!arg.convention.isIndirectIn, "@in arguments cannot be initialized") @@ -392,7 +396,15 @@ extension AddressInitializationWalker { mutating func appliedAddressUse(of operand: Operand, by apply: FullApplySite) -> WalkResult { - if operand.isAddressInitialization { + switch apply.fullyAssigns(operand: operand) { + case .no: + break + case .lifetime: + if requireFullyAssigned == .value { + break + } + fallthrough + case .value: return setInitializer(instruction: operand.instruction) } guard let convention = apply.convention(of: operand) else { From ce153a85ea3cce537995e1239aff7bf22b10c868 Mon Sep 17 00:00:00 2001 From: Andrew Trick Date: Mon, 20 Oct 2025 21:03:55 -0700 Subject: [PATCH 06/19] [NFC] extend AddressInitializationWalker to report address reads --- .../Optimizer/Utilities/AddressUtils.swift | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/SwiftCompilerSources/Sources/Optimizer/Utilities/AddressUtils.swift b/SwiftCompilerSources/Sources/Optimizer/Utilities/AddressUtils.swift index 5887682b1ca62..10772189f992c 100644 --- a/SwiftCompilerSources/Sources/Optimizer/Utilities/AddressUtils.swift +++ b/SwiftCompilerSources/Sources/Optimizer/Utilities/AddressUtils.swift @@ -312,6 +312,7 @@ extension AccessBase { struct AddressInitializationWalker: AddressDefUseWalker, AddressUseVisitor { let baseAddress: Value let requireFullyAssigned: IsFullyAssigned + let onRead: WalkResult let context: any Context var walkDownCache = WalkerCache() @@ -320,20 +321,21 @@ struct AddressInitializationWalker: AddressDefUseWalker, AddressUseVisitor { var initializer: AccessBase.Initializer? static func findSingleInitializer(ofAddress baseAddr: Value, requireFullyAssigned: IsFullyAssigned, - _ context: some Context) + allowRead: Bool = true, _ context: some Context) -> AccessBase.Initializer? { - var walker = AddressInitializationWalker(baseAddress: baseAddr, requireFullyAssigned, context) + var walker = AddressInitializationWalker(baseAddress: baseAddr, requireFullyAssigned, allowRead: allowRead, context) if walker.walkDownUses(ofAddress: baseAddr, path: SmallProjectionPath()) == .abortWalk { return nil } return walker.initializer } - private init(baseAddress: Value, _ requireFullyAssigned: IsFullyAssigned, _ context: some Context) { + private init(baseAddress: Value, _ requireFullyAssigned: IsFullyAssigned, allowRead: Bool, _ context: some Context) { assert(requireFullyAssigned != .no) self.baseAddress = baseAddress self.requireFullyAssigned = requireFullyAssigned + self.onRead = allowRead ? .continueWalk : .abortWalk self.context = context if let arg = baseAddress as? FunctionArgument { assert(!arg.convention.isIndirectIn, "@in arguments cannot be initialized") @@ -391,15 +393,21 @@ extension AddressInitializationWalker { // FIXME: check mayWriteToMemory but ignore non-stores. Currently, // stores should all be checked my isAddressInitialization, but // this is not robust. - return .continueWalk + return onRead } mutating func appliedAddressUse(of operand: Operand, by apply: FullApplySite) -> WalkResult { switch apply.fullyAssigns(operand: operand) { case .no: + if onRead == .abortWalk { + return .abortWalk + } break case .lifetime: + if onRead == .abortWalk { + return .abortWalk + } if requireFullyAssigned == .value { break } @@ -415,26 +423,26 @@ extension AddressInitializationWalker { mutating func loadedAddressUse(of operand: Operand, intoValue value: Value) -> WalkResult { - return .continueWalk + return onRead } mutating func loadedAddressUse(of operand: Operand, intoAddress address: Operand) -> WalkResult { - return .continueWalk + return onRead } mutating func yieldedAddressUse(of operand: Operand) -> WalkResult { // An inout yield is a partial write. Initialization via coroutine is not supported, so we assume a prior // initialization must dominate the yield. - return .continueWalk + return onRead } mutating func dependentAddressUse(of operand: Operand, dependentValue value: Value) -> WalkResult { - return .continueWalk + return onRead } mutating func dependentAddressUse(of operand: Operand, dependentAddress address: Value) -> WalkResult { - return .continueWalk + return onRead } mutating func escapingAddressUse(of operand: Operand) -> WalkResult { From a0f0c108b03d696512d4f229bcb0f0560f456161 Mon Sep 17 00:00:00 2001 From: Andrew Trick Date: Thu, 9 Oct 2025 04:54:36 -0700 Subject: [PATCH 07/19] [NFC] Add a new entry point for DiagnoseDependence.reportError Allow diagnosing values that have no relevant users. Such as an @inout argument. --- .../LifetimeDependenceDiagnostics.swift | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/SwiftCompilerSources/Sources/Optimizer/FunctionPasses/LifetimeDependenceDiagnostics.swift b/SwiftCompilerSources/Sources/Optimizer/FunctionPasses/LifetimeDependenceDiagnostics.swift index 235fb86caf13a..61c672c838185 100644 --- a/SwiftCompilerSources/Sources/Optimizer/FunctionPasses/LifetimeDependenceDiagnostics.swift +++ b/SwiftCompilerSources/Sources/Optimizer/FunctionPasses/LifetimeDependenceDiagnostics.swift @@ -160,7 +160,7 @@ private struct DiagnoseDependence { func checkInScope(operand: Operand) -> WalkResult { if let range, !range.inclusiveRangeContains(operand.instruction) { log(" out-of-range error: \(operand.instruction)") - reportError(operand: operand, diagID: .lifetime_outside_scope_use) + reportError(escapingValue: operand.value, user: operand.instruction, diagID: .lifetime_outside_scope_use) return .abortWalk } log(" contains: \(operand.instruction)") @@ -169,7 +169,12 @@ private struct DiagnoseDependence { func reportEscaping(operand: Operand) { log(" escaping error: \(operand.instruction)") - reportError(operand: operand, diagID: .lifetime_outside_scope_escape) + reportError(escapingValue: operand.value, user: operand.instruction, diagID: .lifetime_outside_scope_escape) + } + + func reportEscaping(value: Value, user: Instruction) { + log(" escaping error: \(value) at \(user)") + reportError(escapingValue: value, user: user, diagID: .lifetime_outside_scope_escape) } func reportUnknown(operand: Operand) { @@ -258,7 +263,7 @@ private struct DiagnoseDependence { return .abortWalk } - func reportError(operand: Operand, diagID: DiagID) { + func reportError(escapingValue: Value, user: Instruction, diagID: DiagID) { // If the dependent value is Escapable, then mark_dependence resolution fails, but this is not a diagnostic error. if dependence.dependentValue.isEscapable { return @@ -266,7 +271,7 @@ private struct DiagnoseDependence { onError() // Identify the escaping variable. - let escapingVar = LifetimeVariable(usedBy: operand, context) + let escapingVar = LifetimeVariable(definedBy: escapingValue, user: user, context) if let varDecl = escapingVar.varDecl { // Use the variable location, not the access location. // Variable names like $return_value and $implicit_value don't have source locations. @@ -288,7 +293,7 @@ private struct DiagnoseDependence { diagnoseImplicitFunction() reportScope() // Identify the use point. - if let userSourceLoc = operand.instruction.location.sourceLoc { + if let userSourceLoc = user.location.sourceLoc { diagnose(userSourceLoc, diagID) } } @@ -359,13 +364,12 @@ private struct LifetimeVariable { return varDecl?.userFacingName } - init(usedBy operand: Operand, _ context: some Context) { - self = .init(dependent: operand.value, context) + init(definedBy value: Value, user: Instruction, _ context: some Context) { + self = .init(dependent: 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. + // For @out arguments, the user'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 + self.sourceLoc = self.sourceLoc ?? user.location.sourceLoc ?? user.parentFunction.location.sourceLoc } init(definedBy value: Value, _ context: some Context) { From ff4d053a44a79767c2b53c841ba69b1c2b9c2ba2 Mon Sep 17 00:00:00 2001 From: Andrew Trick Date: Thu, 9 Oct 2025 05:29:08 -0700 Subject: [PATCH 08/19] [NFC] LifetimeDependenceDefUseWalker.inoutDependence entry point Handle cases where there's no relevant operand. --- .../LifetimeDependenceDiagnostics.swift | 4 ++-- .../LifetimeDependenceScopeFixup.swift | 22 +++++++++---------- .../Utilities/LifetimeDependenceUtils.swift | 21 ++++++++---------- .../Utilities/LocalVariableUtils.swift | 1 + 4 files changed, 23 insertions(+), 25 deletions(-) diff --git a/SwiftCompilerSources/Sources/Optimizer/FunctionPasses/LifetimeDependenceDiagnostics.swift b/SwiftCompilerSources/Sources/Optimizer/FunctionPasses/LifetimeDependenceDiagnostics.swift index 61c672c838185..d472f714f1f54 100644 --- a/SwiftCompilerSources/Sources/Optimizer/FunctionPasses/LifetimeDependenceDiagnostics.swift +++ b/SwiftCompilerSources/Sources/Optimizer/FunctionPasses/LifetimeDependenceDiagnostics.swift @@ -557,9 +557,9 @@ extension DiagnoseDependenceWalker : LifetimeDependenceDefUseWalker { return .abortWalk } - mutating func inoutDependence(argument: FunctionArgument, on operand: Operand) -> WalkResult { + mutating func inoutDependence(argument: FunctionArgument, functionExit: Instruction) -> WalkResult { if diagnostics.checkInoutResult(argument: argument) == .abortWalk { - diagnostics.reportEscaping(operand: operand) + diagnostics.reportEscaping(value: argument, user: functionExit) return .abortWalk } return .continueWalk diff --git a/SwiftCompilerSources/Sources/Optimizer/FunctionPasses/LifetimeDependenceScopeFixup.swift b/SwiftCompilerSources/Sources/Optimizer/FunctionPasses/LifetimeDependenceScopeFixup.swift index 94dfc78e820ba..f90815bbee2d9 100644 --- a/SwiftCompilerSources/Sources/Optimizer/FunctionPasses/LifetimeDependenceScopeFixup.swift +++ b/SwiftCompilerSources/Sources/Optimizer/FunctionPasses/LifetimeDependenceScopeFixup.swift @@ -685,7 +685,7 @@ extension ScopeExtension { do { // The innermost scope that must be extended must dominate all uses. var walker = LifetimeDependentUseWalker(function, localReachabilityCache, context) { - inRangeUses.append($0.instruction) + inRangeUses.append($0) return .continueWalk } defer {walker.deinitialize()} @@ -1064,7 +1064,7 @@ private extension BeginApplyInst { private struct LifetimeDependentUseWalker : LifetimeDependenceDefUseWalker { let function: Function let context: Context - let visitor: (Operand) -> WalkResult + let visitor: (Instruction) -> WalkResult let localReachabilityCache: LocalVariableReachabilityCache var visitedValues: ValueSet @@ -1072,7 +1072,7 @@ private struct LifetimeDependentUseWalker : LifetimeDependenceDefUseWalker { var dependsOnCaller = false init(_ function: Function, _ localReachabilityCache: LocalVariableReachabilityCache, _ context: Context, - visitor: @escaping (Operand) -> WalkResult) { + visitor: @escaping (Instruction) -> WalkResult) { self.function = function self.context = context self.visitor = visitor @@ -1091,42 +1091,42 @@ private struct LifetimeDependentUseWalker : LifetimeDependenceDefUseWalker { mutating func deadValue(_ value: Value, using operand: Operand?) -> WalkResult { if let operand { - return visitor(operand) + return visitor(operand.instruction) } return .continueWalk } mutating func leafUse(of operand: Operand) -> WalkResult { - return visitor(operand) + return visitor(operand.instruction) } mutating func escapingDependence(on operand: Operand) -> WalkResult { log(">>> Escaping dependence: \(operand)") - _ = visitor(operand) + _ = visitor(operand.instruction) // Make a best-effort attempt to extend the access scope regardless of escapes. It is possible that some mandatory // pass between scope fixup and diagnostics will make it possible for the LifetimeDependenceDefUseWalker to analyze // this use. return .continueWalk } - mutating func inoutDependence(argument: FunctionArgument, on operand: Operand) -> WalkResult { + mutating func inoutDependence(argument: FunctionArgument, functionExit: Instruction) -> WalkResult { dependsOnCaller = true - return visitor(operand) + return visitor(functionExit) } mutating func returnedDependence(result operand: Operand) -> WalkResult { dependsOnCaller = true - return visitor(operand) + return visitor(operand.instruction) } mutating func returnedDependence(address: FunctionArgument, on operand: Operand) -> WalkResult { dependsOnCaller = true - return visitor(operand) + return visitor(operand.instruction) } mutating func yieldedDependence(result: Operand) -> WalkResult { - return visitor(result) + return visitor(result.instruction) } mutating func storeToYieldDependence(address: Value, of operand: Operand) -> WalkResult { diff --git a/SwiftCompilerSources/Sources/Optimizer/Utilities/LifetimeDependenceUtils.swift b/SwiftCompilerSources/Sources/Optimizer/Utilities/LifetimeDependenceUtils.swift index eb21be91aef7a..971db275c54c4 100644 --- a/SwiftCompilerSources/Sources/Optimizer/Utilities/LifetimeDependenceUtils.swift +++ b/SwiftCompilerSources/Sources/Optimizer/Utilities/LifetimeDependenceUtils.swift @@ -614,7 +614,7 @@ extension LifetimeDependence.Scope { /// leafUse(of: Operand) -> WalkResult /// deadValue(_ value: Value, using operand: Operand?) -> WalkResult /// escapingDependence(on operand: Operand) -> WalkResult -/// inoutDependence(argument: FunctionArgument, on: Operand) -> WalkResult +/// inoutDependence(argument: FunctionArgument, functionExit: Instruction) -> WalkResult /// returnedDependence(result: Operand) -> WalkResult /// returnedDependence(address: FunctionArgument, on: Operand) -> WalkResult /// yieldedDependence(result: Operand) -> WalkResult @@ -634,9 +634,9 @@ protocol LifetimeDependenceDefUseWalker : ForwardingDefUseWalker, mutating func escapingDependence(on operand: Operand) -> WalkResult - // Assignment to an inout argument. This does not include the indirect out result, which is considered a return + // Assignment to an inout argument. This does not include the @out result, which is considered a return // value. - mutating func inoutDependence(argument: FunctionArgument, on: Operand) -> WalkResult + mutating func inoutDependence(argument: FunctionArgument, functionExit: Instruction) -> WalkResult mutating func returnedDependence(result: Operand) -> WalkResult @@ -1025,15 +1025,12 @@ extension LifetimeDependenceDefUseWalker { if !localReachability.gatherAllReachableUses(of: storeAccess, in: &accessStack) { return escapingDependence(on: storedOperand) } - for localAccess in accessStack { - if visitLocalAccess(allocation: allocation, localAccess: localAccess, initialValue: storedOperand) == .abortWalk { - return .abortWalk - } + return accessStack.walk { localAccess in + visitLocalAccess(allocation: allocation, localAccess: localAccess) } - return .continueWalk } - private mutating func visitLocalAccess(allocation: Value, localAccess: LocalVariableAccess, initialValue: Operand) + private mutating func visitLocalAccess(allocation: Value, localAccess: LocalVariableAccess) -> WalkResult { switch localAccess.kind { case .beginAccess: @@ -1080,7 +1077,7 @@ extension LifetimeDependenceDefUseWalker { case .outgoingArgument: let arg = allocation as! FunctionArgument assert(arg.type.isAddress, "returned local must be allocated with an indirect argument") - return inoutDependence(argument: arg, on: initialValue) + return inoutDependence(argument: arg, functionExit: localAccess.instruction!) case .inoutYield: return yieldedDependence(result: localAccess.operand!) case .incomingArgument: @@ -1180,8 +1177,8 @@ private struct LifetimeDependenceUsePrinter : LifetimeDependenceDefUseWalker { return .continueWalk } - mutating func inoutDependence(argument: FunctionArgument, on operand: Operand) -> WalkResult { - print("Out use: \(operand) in: \(argument)") + mutating func inoutDependence(argument: FunctionArgument, functionExit: Instruction) -> WalkResult { + print("Returned inout: \(argument) exit: \(functionExit)") return .continueWalk } diff --git a/SwiftCompilerSources/Sources/Optimizer/Utilities/LocalVariableUtils.swift b/SwiftCompilerSources/Sources/Optimizer/Utilities/LocalVariableUtils.swift index e6fc197c40c43..dffda0750c1af 100644 --- a/SwiftCompilerSources/Sources/Optimizer/Utilities/LocalVariableUtils.swift +++ b/SwiftCompilerSources/Sources/Optimizer/Utilities/LocalVariableUtils.swift @@ -857,6 +857,7 @@ extension LocalVariableReachableAccess { break } if block.terminator.isFunctionExiting { + // Record any reachable function exit as .outgoingArgument. accessStack.push(LocalVariableAccess(.outgoingArgument, block.terminator)) } else if block.successors.isEmpty { accessStack.push(LocalVariableAccess(.deadEnd, block.terminator)) From 6e0eeb00e341e00e8ef0a925a04b665aec884d5a Mon Sep 17 00:00:00 2001 From: Andrew Trick Date: Thu, 9 Oct 2025 05:30:20 -0700 Subject: [PATCH 09/19] Fix LifetimeDependenceDefUseWalker for @inout reassignment --- .../Optimizer/Utilities/AddressUtils.swift | 2 +- .../Utilities/LifetimeDependenceUtils.swift | 32 ++- .../Utilities/LocalVariableUtils.swift | 206 ++++++++++++------ 3 files changed, 170 insertions(+), 70 deletions(-) diff --git a/SwiftCompilerSources/Sources/Optimizer/Utilities/AddressUtils.swift b/SwiftCompilerSources/Sources/Optimizer/Utilities/AddressUtils.swift index 10772189f992c..b913801cc23ca 100644 --- a/SwiftCompilerSources/Sources/Optimizer/Utilities/AddressUtils.swift +++ b/SwiftCompilerSources/Sources/Optimizer/Utilities/AddressUtils.swift @@ -649,7 +649,7 @@ extension AddressOwnershipLiveRange { var reachableUses = Stack(context) defer { reachableUses.deinitialize() } - localReachability.gatherKnownLifetimeUses(from: assignment, in: &reachableUses) + localReachability.gatherKnownLivenessUses(from: assignment, in: &reachableUses) let assignmentInst = assignment.instruction ?? allocation.parentFunction.entryBlock.instructions.first! var range = InstructionRange(begin: assignmentInst, context) diff --git a/SwiftCompilerSources/Sources/Optimizer/Utilities/LifetimeDependenceUtils.swift b/SwiftCompilerSources/Sources/Optimizer/Utilities/LifetimeDependenceUtils.swift index 971db275c54c4..b73b35998bd1a 100644 --- a/SwiftCompilerSources/Sources/Optimizer/Utilities/LifetimeDependenceUtils.swift +++ b/SwiftCompilerSources/Sources/Optimizer/Utilities/LifetimeDependenceUtils.swift @@ -662,12 +662,38 @@ 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), an - // allocation, an incoming argument, or an outgoing argument. In all these cases, walk down the address uses. + // 'root' may be an incoming ~Escapable argument (where the argument is both the scope and the dependent value). + // If it is @inout, treat it like a local variable initialized on entry and possibly reassigned. + if let arg = root as? FunctionArgument, arg.convention.isInout { + return visitInoutAccess(argument: arg) + } + + // Conservatively walk down any other address. This includes: + // An @in argument: assume it is initialized on entry and never reassigned. + // An @out argument: assume the first address use is the one and only assignment on each return path. + // An escapable mark_dependence that guards its address uses (unsafeAddress). + // Any other unknown address producer. return walkDownAddressUses(of: root) } return walkDownUses(of: root, using: nil) } + + // Find all @inout local variable uses reachabile from function entry. If local analysis fails to gather reachable + // uses, fall back to walkDownAddressUse to produce a better diagnostic. + mutating func visitInoutAccess(argument: FunctionArgument) -> WalkResult { + guard let localReachability = localReachabilityCache.reachability(for: argument, walkerContext) else { + return walkDownAddressUses(of: argument) + } + var reachableUses = Stack(walkerContext) + defer { reachableUses.deinitialize() } + + if !localReachability.gatherAllReachableDependentUsesFromEntry(in: &reachableUses) { + return walkDownAddressUses(of: argument) + } + return reachableUses.walk { localAccess in + visitLocalAccess(allocation: argument, localAccess: localAccess) + } + } } // Implement ForwardingDefUseWalker @@ -1022,7 +1048,7 @@ extension LifetimeDependenceDefUseWalker { if case let .access(beginAccess) = storeAddress.enclosingAccessScope { storeAccess = beginAccess } - if !localReachability.gatherAllReachableUses(of: storeAccess, in: &accessStack) { + if !localReachability.gatherAllReachableDependentUses(of: storeAccess, in: &accessStack) { return escapingDependence(on: storedOperand) } return accessStack.walk { localAccess in diff --git a/SwiftCompilerSources/Sources/Optimizer/Utilities/LocalVariableUtils.swift b/SwiftCompilerSources/Sources/Optimizer/Utilities/LocalVariableUtils.swift index dffda0750c1af..db7e61d5fc8e3 100644 --- a/SwiftCompilerSources/Sources/Optimizer/Utilities/LocalVariableUtils.swift +++ b/SwiftCompilerSources/Sources/Optimizer/Utilities/LocalVariableUtils.swift @@ -168,7 +168,7 @@ struct LocalVariableAccess: CustomStringConvertible { class LocalVariableAccessInfo: CustomStringConvertible { let access: LocalVariableAccess - private var _isFullyAssigned: Bool? + private var _isFullyAssigned: IsFullyAssigned? /// Cache whether the allocation has escaped prior to this access. /// This returns `nil` until reachability is computed. @@ -180,30 +180,36 @@ class LocalVariableAccessInfo: CustomStringConvertible { case .beginAccess: switch (localAccess.instruction as! BeginAccessInst).accessKind { case .read, .deinit: - self._isFullyAssigned = false + _isFullyAssigned = .no case .`init`, .modify: break // lazily compute full assignment } case .load, .dependenceSource, .dependenceDest: - self._isFullyAssigned = false + _isFullyAssigned = .no case .store, .storeBorrow: if let store = localAccess.instruction as? StoringInstruction { - self._isFullyAssigned = LocalVariableAccessInfo.isBase(address: store.destination) + self._isFullyAssigned = LocalVariableAccessInfo.isBase(address: store.destination) ? .value : .no + } else { - self._isFullyAssigned = true + self._isFullyAssigned = .value } case .apply: + // This logic is consistent with AddressInitializationWalker.appliedAddressUse() let apply = localAccess.instruction as! FullApplySite if let convention = apply.convention(of: localAccess.operand!) { - self._isFullyAssigned = convention.isIndirectOut - } else { - self._isFullyAssigned = false + if convention.isIndirectOut { + self._isFullyAssigned = .value + } + if convention.isInout { + self._isFullyAssigned = apply.fullyAssigns(operand: localAccess.operand!) + } } + _isFullyAssigned = .no case .escape: - self._isFullyAssigned = false + _isFullyAssigned = .no self.hasEscaped = true case .inoutYield: - self._isFullyAssigned = false + _isFullyAssigned = .no case .incomingArgument, .outgoingArgument, .deadEnd: fatalError("Function arguments are never mapped to LocalVariableAccessInfo") } @@ -217,7 +223,7 @@ class LocalVariableAccessInfo: CustomStringConvertible { /// Is this access a full assignment such that none of the variable's components are reachable from a previous /// access. - func isFullyAssigned(_ context: Context) -> Bool { + func isFullyAssigned(_ context: Context) -> IsFullyAssigned { if let cached = _isFullyAssigned { return cached } @@ -226,8 +232,15 @@ class LocalVariableAccessInfo: CustomStringConvertible { } assert(isModify) let beginAccess = access.instruction as! BeginAccessInst - let initializer = AddressInitializationWalker.findSingleInitializer(ofAddress: beginAccess, context: context) - _isFullyAssigned = (initializer != nil) ? true : false + if AddressInitializationWalker.findSingleInitializer(ofAddress: beginAccess, requireFullyAssigned: .value, context) + != nil { + _isFullyAssigned = .value + } else if AddressInitializationWalker.findSingleInitializer(ofAddress: beginAccess, + requireFullyAssigned: .lifetime, context) != nil { + _isFullyAssigned = .lifetime + } else { + _isFullyAssigned = .no + } return _isFullyAssigned! } @@ -553,12 +566,13 @@ extension LocalVariableAccessWalker: AddressUseVisitor { /// it as an analysis. We expect a very small number of accesses per local variable. struct LocalVariableAccessBlockMap { // Lattice, from most information to least information: - // none -> read -> modify -> escape -> assign + // none -> read -> modify -> escape -> assignLifetime -> assignValue enum BlockEffect: Int { case read // no modification or escape case modify // no full assignment or escape case escape // no full assignment - case assign // full assignment, other accesses may be before or after it. + case assignLifetime // lifetime assignment, other accesses may be before or after it. + case assignValue // full value assignment, other accesses may be before or after it. /// Return a merged lattice state such that the result has strictly less information. func meet(_ other: BlockEffect?) -> BlockEffect { @@ -607,8 +621,13 @@ extension LocalVariableAccessBlockMap.BlockEffect { if accessInfo.isEscape { self = .escape } - if accessInfo.isFullyAssigned(context) { - self = .assign + switch accessInfo.isFullyAssigned(context) { + case .no: + break + case .lifetime: + self = .assignLifetime + case .value: + self = .assignValue } } } @@ -647,6 +666,52 @@ struct LocalVariableReachableAccess { } } +extension LocalVariableReachableAccess { + enum ForwardDataFlowEffect: Int { + case read // no modification or escape + case modify // no full assignment or escape + case escape // no full assignment + case assign // lifetime or value assignment, other accesses may be before or after it. + + /// Return a merged lattice state such that the result has strictly less information. + func meet(_ other: ForwardDataFlowEffect?) -> ForwardDataFlowEffect { + guard let other else { + return self + } + return other.rawValue > self.rawValue ? other : self + } + } + + enum DataFlowMode { + /// Find the known live range, which may safely enclose dependent uses. Records escapes and continues walking. + /// Record the destroy or reassignment access of the local before the walk stops. + case livenessUses + + // Find all dependent uses, stop at escapes, stop before recording the destroy or reassignment. + case dependentUses + + func getForwardEffect(_ effect: BlockEffect) -> ForwardDataFlowEffect { + switch effect { + case .read: + .read + case .modify: + .modify + case .escape: + .escape + case .assignLifetime: + switch self { + case .livenessUses: + .modify + case .dependentUses: + .assign + } + case .assignValue: + .assign + } + } + } +} + // Find reaching assignments... extension LocalVariableReachableAccess { // Gather all fully assigned accesses that reach 'instruction'. If 'instruction' is itself a modify access, it is @@ -672,9 +737,9 @@ extension LocalVariableReachableAccess { // `blockInfo.effect` is the same as `currentEffect` returned by backwardScanAccesses, except when an early escape // happens below an assign, in which case we report the escape here. switch currentEffect { - case .none, .read, .modify, .escape: + case .none, .read, .modify, .escape, .assignLifetime: break - case .assign: + case .assignValue: currentEffect = backwardScanAccesses(before: block.instructions.reversed().first!, accessStack: &accessStack) } if !backwardPropagateEffect(in: block, effect: currentEffect, blockList: &blockList, accessStack: &accessStack) { @@ -689,13 +754,13 @@ extension LocalVariableReachableAccess { accessStack: inout Stack) -> Bool { switch effect { - case .none, .read, .modify: + case .none, .read, .modify, .assignLifetime: if block != accessMap.allocation.parentBlock { for predecessor in block.predecessors { blockList.pushIfNotVisited(predecessor) } } else if block == accessMap.function.entryBlock { accessStack.push(accessMap.liveInAccess!) } - case .assign: + case .assignValue: break case .escape: return false @@ -704,7 +769,7 @@ extension LocalVariableReachableAccess { } // Check all instructions in this block before and including `first`. Return a BlockEffect indicating the combined - // effects seen before stopping the scan. A .escape or .assign stops the scan. + // effects seen before stopping the scan. A .escape or .assignValue stops the scan. private func backwardScanAccesses(before first: Instruction, accessStack: inout Stack) -> BlockEffect? { var currentEffect: BlockEffect? @@ -714,9 +779,9 @@ extension LocalVariableReachableAccess { } currentEffect = BlockEffect(for: accessInfo, accessMap.context).meet(currentEffect) switch currentEffect! { - case .read, .modify: + case .read, .modify, .assignLifetime: continue - case .assign: + case .assignValue: accessStack.push(accessInfo.access) case .escape: break @@ -729,30 +794,27 @@ extension LocalVariableReachableAccess { // Find reachable accesses... extension LocalVariableReachableAccess { - /// This performs a forward CFG walk to find known reachable uses from `assignment`. This ignores aliasing and - /// escapes. - /// - /// The known live range is the range in which the assigned value is valid and may be used by dependent values. It - /// includes the destroy or reassignment of the local. - func gatherKnownLifetimeUses(from assignment: LocalVariableAccess, + /// This performs a forward CFG walk to find known reachable uses from `assignment` that guarantee liveness and may + /// safely enclose dependent uses. + func gatherKnownLivenessUses(from assignment: LocalVariableAccess, in accessStack: inout Stack) { if let modifyInst = assignment.instruction { - _ = gatherReachableUses(after: modifyInst, in: &accessStack, lifetime: true) + _ = gatherReachableUses(after: modifyInst, in: &accessStack, mode: .livenessUses) return } - gatherKnownLifetimeUsesFromEntry(in: &accessStack) + gatherKnownLivenessUsesFromEntry(in: &accessStack) } - /// This performs a forward CFG walk to find known reachable uses from the function entry. This ignores aliasing and - /// escapes. - private func gatherKnownLifetimeUsesFromEntry(in accessStack: inout Stack) { + /// This performs a forward CFG walk to find known reachable uses from the function entry that guarantee liveness and + /// may safely enclose dependent uses. + private func gatherKnownLivenessUsesFromEntry(in accessStack: inout Stack) { assert(accessMap.liveInAccess!.kind == .incomingArgument, "only an argument access is live in to the function") let firstInst = accessMap.function.entryBlock.instructions.first! - _ = gatherReachableUses(onOrAfter: firstInst, in: &accessStack, lifetime: true) + _ = gatherReachableUses(onOrAfter: firstInst, in: &accessStack, mode: .livenessUses) } - /// This performs a forward CFG walk to find all reachable uses of `modifyInst`. `modifyInst` may be a `begin_access - /// [modify]` or instruction that initializes the local variable. + /// This performs a forward CFG walk to find all reachable lifetime dependent uses of `modifyInst`. `modifyInst` may + /// be a `begin_access [modify]` or instruction that initializes the local variable. /// /// This does not include the destroy or reassignment of the value set by `modifyInst`. /// @@ -762,12 +824,16 @@ extension LocalVariableReachableAccess { /// This does not gather the escaping accesses themselves. When escapes are reachable, it also does not guarantee that /// previously reachable accesses are gathered. /// + /// The walk stops at any variable assignment that does not propagate the lifetime dependency; for example, at an + /// @inout argument that does not depend on itself (apply.fullyAssigns(arg) == .lifetime). + /// /// This computes reachability separately for each store. If this store is a fully assigned access, then /// this never repeats work (it is a linear-time analysis over all assignments), because the walk always stops at the /// next fully-assigned access. Field assignment can result in an analysis that is quadratic in the number /// stores. Nonetheless, the analysis is highly efficient because it maintains no block state other than the /// block's intrusive bit set. - func gatherAllReachableUses(of modifyInst: Instruction, in accessStack: inout Stack) -> Bool { + func gatherAllReachableDependentUses(of modifyInst: Instruction, + in accessStack: inout Stack) -> Bool { guard let accessInfo = accessMap[modifyInst] else { return false } @@ -777,64 +843,72 @@ extension LocalVariableReachableAccess { if accessInfo.hasEscaped! { return false } - return gatherReachableUses(after: modifyInst, in: &accessStack, lifetime: false) + return gatherReachableUses(after: modifyInst, in: &accessStack, mode: .dependentUses) + } + + func gatherAllReachableDependentUsesFromEntry(in accessStack: inout Stack) -> Bool { + return gatherReachableUses(onOrAfter: accessMap.function.entryBlock.instructions.first!, in: &accessStack, + mode: .dependentUses) } /// This performs a forward CFG walk to find all uses of this local variable reachable after `begin`. /// - /// If `lifetime` is true, then this gathers the full known lifetime, including destroys and reassignments ignoring - /// escapes. + /// For DataFlowMode.livenessUses, this gathers the full known live range, including destroys and reassignments + /// continuing past escapes. /// - /// If `lifetime` is false, then this returns `false` if the walk ended early because of a reachable escape. + /// For DataFlowMode.dependentUses, this returns `false` if the walk ended early because of a reachable escape. private func gatherReachableUses(after begin: Instruction, in accessStack: inout Stack, - lifetime: Bool) -> Bool { + mode: DataFlowMode) -> Bool { if let term = begin as? TermInst { for succ in term.successors { - if !gatherReachableUses(onOrAfter: succ.instructions.first!, in: &accessStack, lifetime: lifetime) { + if !gatherReachableUses(onOrAfter: succ.instructions.first!, in: &accessStack, mode: mode) { return false } } return true } else { - return gatherReachableUses(onOrAfter: begin.next!, in: &accessStack, lifetime: lifetime) + return gatherReachableUses(onOrAfter: begin.next!, in: &accessStack, mode: mode) } } /// This performs a forward CFG walk to find all uses of this local variable reachable after and including `begin`. /// - /// If `lifetime` is true, then this returns false if the walk ended early because of a reachable escape. + /// For DataFlowMode.dependentUses, then this returns false if the walk ended early because of a reachable escape. private func gatherReachableUses(onOrAfter begin: Instruction, in accessStack: inout Stack, - lifetime: Bool) -> Bool { + mode: DataFlowMode) -> Bool { var blockList = BasicBlockWorklist(context) defer { blockList.deinitialize() } let initialBlock = begin.parentBlock - let initialEffect = forwardScanAccesses(after: begin, accessStack: &accessStack, lifetime: lifetime) - if !lifetime, initialEffect == .escape { + let initialEffect = forwardScanAccesses(after: begin, accessStack: &accessStack, mode: mode) + if mode == .dependentUses, initialEffect == .escape { return false } forwardPropagateEffect(in: initialBlock, blockInfo: blockMap[initialBlock], effect: initialEffect, blockList: &blockList, accessStack: &accessStack) while let block = blockList.pop() { let blockInfo = blockMap[block] - var currentEffect = blockInfo?.effect - // lattice: none -> read -> modify -> escape -> assign + var currentEffect: ForwardDataFlowEffect? + if let blockEffect = blockInfo?.effect { + currentEffect = mode.getForwardEffect(blockEffect) + } + // lattice: none -> read -> modify -> escape -> assignLifetime -> assignValue // - // `blockInfo.effect` is the same as `currentEffect` returned by forwardScanAccesses, except when an early - // disallowed escape happens before an assign. + // `blockInfo.effect` is the same as `currentEffect` returned by forwardScanAccesses below, except when + // forwardScanAccesses finds an early disallowed escape before the assign. switch currentEffect { case .none: break case .escape: - if !lifetime { + if mode == .dependentUses { break } fallthrough case .read, .modify, .assign: let firstInst = block.instructions.first! - currentEffect = forwardScanAccesses(after: firstInst, accessStack: &accessStack, lifetime: lifetime) + currentEffect = forwardScanAccesses(after: firstInst, accessStack: &accessStack, mode: mode) } - if !lifetime, currentEffect == .escape { + if mode == .dependentUses, currentEffect == .escape { return false } forwardPropagateEffect(in: block, blockInfo: blockInfo, effect: currentEffect, blockList: &blockList, @@ -848,7 +922,7 @@ extension LocalVariableReachableAccess { typealias BlockEffect = LocalVariableAccessBlockMap.BlockEffect typealias BlockInfo = LocalVariableAccessBlockMap.BlockInfo - private func forwardPropagateEffect(in block: BasicBlock, blockInfo: BlockInfo?, effect: BlockEffect?, + private func forwardPropagateEffect(in block: BasicBlock, blockInfo: BlockInfo?, effect: ForwardDataFlowEffect?, blockList: inout BasicBlockWorklist, accessStack: inout Stack) { switch effect { @@ -870,24 +944,24 @@ extension LocalVariableReachableAccess { } // Check all instructions in this block after and including `begin`. Return a BlockEffect indicating the combined - // effects seen before stopping the scan. An .assign stops the scan. A .escape stops the scan if lifetime is false. + // effects seen before stopping the scan. An .assign stops the scan. A .escape stops the scan for .dependentUses. private func forwardScanAccesses(after first: Instruction, accessStack: inout Stack, - lifetime: Bool) - -> BlockEffect? { - var currentEffect: BlockEffect? + mode: DataFlowMode) + -> ForwardDataFlowEffect? { + var currentEffect: ForwardDataFlowEffect? for inst in InstructionList(first: first) { guard let accessInfo = accessMap[inst] else { continue } - currentEffect = BlockEffect(for: accessInfo, accessMap.context).meet(currentEffect) + currentEffect = mode.getForwardEffect(BlockEffect(for: accessInfo, accessMap.context)).meet(currentEffect) switch currentEffect! { case .assign: - if lifetime { + if mode == .livenessUses { accessStack.push(accessInfo.access) } return currentEffect case .escape: - if !lifetime { + if mode == .dependentUses { log("Local variable: \(accessMap.allocation)\n escapes at \(inst)") return currentEffect } @@ -994,7 +1068,7 @@ let localVariableReachableUsesTest = FunctionTest("local_variable_reachable_uses print("### Modify: \(modify)") var reachableUses = Stack(context) defer { reachableUses.deinitialize() } - guard localReachability.gatherAllReachableUses(of: modify, in: &reachableUses) else { + guard localReachability.gatherAllReachableDependentUses(of: modify, in: &reachableUses) else { print("!!! Reachable escape") return } From de34abe3e5f4ff5c9895afb3a633f90724f1cc61 Mon Sep 17 00:00:00 2001 From: Andrew Trick Date: Mon, 20 Oct 2025 21:06:44 -0700 Subject: [PATCH 10/19] Fix LocalVariableReachableAccess to handle potential reassignment. Define LocalAccessInfo._isFullyAssigned to mean that the access does not read the incoming value. Then treat any assignment that isn't full as a potential read. --- .../Utilities/LocalVariableUtils.swift | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/SwiftCompilerSources/Sources/Optimizer/Utilities/LocalVariableUtils.swift b/SwiftCompilerSources/Sources/Optimizer/Utilities/LocalVariableUtils.swift index db7e61d5fc8e3..bed71e0dbb147 100644 --- a/SwiftCompilerSources/Sources/Optimizer/Utilities/LocalVariableUtils.swift +++ b/SwiftCompilerSources/Sources/Optimizer/Utilities/LocalVariableUtils.swift @@ -180,12 +180,12 @@ class LocalVariableAccessInfo: CustomStringConvertible { case .beginAccess: switch (localAccess.instruction as! BeginAccessInst).accessKind { case .read, .deinit: - _isFullyAssigned = .no + self._isFullyAssigned = .no case .`init`, .modify: break // lazily compute full assignment } case .load, .dependenceSource, .dependenceDest: - _isFullyAssigned = .no + self._isFullyAssigned = .no case .store, .storeBorrow: if let store = localAccess.instruction as? StoringInstruction { self._isFullyAssigned = LocalVariableAccessInfo.isBase(address: store.destination) ? .value : .no @@ -196,20 +196,22 @@ class LocalVariableAccessInfo: CustomStringConvertible { case .apply: // This logic is consistent with AddressInitializationWalker.appliedAddressUse() let apply = localAccess.instruction as! FullApplySite - if let convention = apply.convention(of: localAccess.operand!) { - if convention.isIndirectOut { - self._isFullyAssigned = .value - } - if convention.isInout { - self._isFullyAssigned = apply.fullyAssigns(operand: localAccess.operand!) - } + guard let convention = apply.convention(of: localAccess.operand!) else { + self._isFullyAssigned = .no + break + } + if convention.isIndirectOut { + self._isFullyAssigned = .value + } else if convention.isInout { + self._isFullyAssigned = apply.fullyAssigns(operand: localAccess.operand!) + } else { + self._isFullyAssigned = .no } - _isFullyAssigned = .no case .escape: - _isFullyAssigned = .no + self._isFullyAssigned = .no self.hasEscaped = true case .inoutYield: - _isFullyAssigned = .no + self._isFullyAssigned = .no case .incomingArgument, .outgoingArgument, .deadEnd: fatalError("Function arguments are never mapped to LocalVariableAccessInfo") } @@ -222,7 +224,7 @@ class LocalVariableAccessInfo: CustomStringConvertible { var isEscape: Bool { access.isEscape } /// Is this access a full assignment such that none of the variable's components are reachable from a previous - /// access. + /// access? Only returns '.value' if this access does not read the incoming value. func isFullyAssigned(_ context: Context) -> IsFullyAssigned { if let cached = _isFullyAssigned { return cached @@ -232,7 +234,8 @@ class LocalVariableAccessInfo: CustomStringConvertible { } assert(isModify) let beginAccess = access.instruction as! BeginAccessInst - if AddressInitializationWalker.findSingleInitializer(ofAddress: beginAccess, requireFullyAssigned: .value, context) + if AddressInitializationWalker.findSingleInitializer(ofAddress: beginAccess, requireFullyAssigned: .value, + allowRead: false, context) != nil { _isFullyAssigned = .value } else if AddressInitializationWalker.findSingleInitializer(ofAddress: beginAccess, @@ -956,7 +959,7 @@ extension LocalVariableReachableAccess { currentEffect = mode.getForwardEffect(BlockEffect(for: accessInfo, accessMap.context)).meet(currentEffect) switch currentEffect! { case .assign: - if mode == .livenessUses { + if mode == .livenessUses || accessInfo.isFullyAssigned(context) != .value { accessStack.push(accessInfo.access) } return currentEffect From 77ee27a0e79696d022daa775dafb022ce29508ae Mon Sep 17 00:00:00 2001 From: Andrew Trick Date: Wed, 15 Oct 2025 22:12:56 -0700 Subject: [PATCH 11/19] [NFC] LocalVariableAccessInfo.description --- .../Sources/Optimizer/Utilities/LocalVariableUtils.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SwiftCompilerSources/Sources/Optimizer/Utilities/LocalVariableUtils.swift b/SwiftCompilerSources/Sources/Optimizer/Utilities/LocalVariableUtils.swift index bed71e0dbb147..1d93203ac790d 100644 --- a/SwiftCompilerSources/Sources/Optimizer/Utilities/LocalVariableUtils.swift +++ b/SwiftCompilerSources/Sources/Optimizer/Utilities/LocalVariableUtils.swift @@ -158,7 +158,7 @@ struct LocalVariableAccess: CustomStringConvertible { str += "deadEnd" } if let inst = instruction { - str += "\(inst)" + str += ", \(inst)" } return str } @@ -249,6 +249,7 @@ class LocalVariableAccessInfo: CustomStringConvertible { var description: String { return "assign: \(_isFullyAssigned == nil ? "unknown" : String(describing: _isFullyAssigned!)), " + + "hasEscaped: \(hasEscaped == nil ? "unknown" : String(describing: hasEscaped!)), " + "\(access)" } From 8200a871c965c266f01aceebb4ae3a79ceb763e7 Mon Sep 17 00:00:00 2001 From: Andrew Trick Date: Sat, 11 Oct 2025 20:49:49 -0700 Subject: [PATCH 12/19] Fix LifetimeDependenceDefUseWalker to follow inout dependence Fixes rdar://157796728 ([nonescapable] [miscompile] No diagnostic error when an inout MutableSpan is reassigned to a different lifetime source) --- .../Utilities/LifetimeDependenceUtils.swift | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/SwiftCompilerSources/Sources/Optimizer/Utilities/LifetimeDependenceUtils.swift b/SwiftCompilerSources/Sources/Optimizer/Utilities/LifetimeDependenceUtils.swift index b73b35998bd1a..cfad02775e84c 100644 --- a/SwiftCompilerSources/Sources/Optimizer/Utilities/LifetimeDependenceUtils.swift +++ b/SwiftCompilerSources/Sources/Optimizer/Utilities/LifetimeDependenceUtils.swift @@ -1121,15 +1121,28 @@ extension LifetimeDependenceDefUseWalker { if apply.isCallee(operand: operand) { return leafUse(of: operand) } + // Find any copied dependence on this source operand, either targeting the result or an inout parameter. If the + // lifetime dependence is scoped, then we can ignore it because a mark_dependence [nonescaping] represents the + // dependence. + for targetOperand in apply.argumentOperands { + guard !targetOperand.value.isEscapable else { + continue + } + if let dep = apply.parameterDependence(target: targetOperand, source: operand), + !dep.isScoped { + let targetAddress = targetOperand.value + assert(targetAddress.type.isAddress, "a parameter dependence target must be 'inout'") + if dependentUse(of: operand, dependentAddress: targetAddress) == .abortWalk { + return .abortWalk + } + } + } if let dep = apply.resultDependence(on: operand), !dep.isScoped { // Operand is nonescapable and passed as a call argument. If the // result inherits its lifetime, then consider any nonescapable // result value to be a dependent use. // - // If the lifetime dependence is scoped, then we can ignore it - // because a mark_dependence [nonescaping] represents the - // dependence. if let result = apply.singleDirectResult, !result.isEscapable { if dependentUse(of: operand, dependentValue: result) == .abortWalk { return .abortWalk From 62b04caaa76f9f3eab99b82e29d183328ee52bd2 Mon Sep 17 00:00:00 2001 From: Andrew Trick Date: Fri, 17 Oct 2025 19:33:19 -0700 Subject: [PATCH 13/19] LocalVariableUtils add non-escaping closure capture support Don't always consider an inout_aliasable argument to have escaped. AccessEnforcementSelection has already done that analysis and left begin_access [dynamic] artifacts if the argument has escaped in any meaningful way. Use that information to Essential for supporting autoclosures that call mutating methods on span-like things. Such as UTF8Span.UnicodeScalarIterator.skipForward(): e.g. while numSkipped < n && skipForward() != 0 { ... } --- .../Utilities/LocalVariableUtils.swift | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/SwiftCompilerSources/Sources/Optimizer/Utilities/LocalVariableUtils.swift b/SwiftCompilerSources/Sources/Optimizer/Utilities/LocalVariableUtils.swift index 1d93203ac790d..ddb0b9f7e6339 100644 --- a/SwiftCompilerSources/Sources/Optimizer/Utilities/LocalVariableUtils.swift +++ b/SwiftCompilerSources/Sources/Optimizer/Utilities/LocalVariableUtils.swift @@ -21,6 +21,12 @@ /// /// 3. Stored properties in heap objects or global variables. These are always formally accessed. /// +/// Pass dependencies: +/// +/// AccessEnforcementSelection must run first to correctly determine which non-esacping closure captures may have +/// escaped prior to the closure invocation. Any access to an inout_aliasable argument that may have escaped in the +/// caller will be marked [dynamic]. +/// //===----------------------------------------------------------------------===// import SIL @@ -287,6 +293,9 @@ struct LocalVariableAccessMap: Collection, CustomStringConvertible { var isBoxed: Bool { allocation is AllocBoxInst } + // If 'mayAlias' is true (@inout_aliasable), then this variable may have escaped before entering the current + // function. 'walkAccesses' determines whether this allocation is considered to have escaped on entry by checked for + // the existienced of begin_access [dynamic]. var mayAlias: Bool { if let arg = allocation as? FunctionArgument, arg.convention == .indirectInoutAliasable { return true @@ -324,18 +333,26 @@ struct LocalVariableAccessMap: Collection, CustomStringConvertible { if walker.walkDown(allocation: allocation) == .abortWalk { return .abortWalk } + var escapedOnEntry = false for localAccess in walker.accessStack { let info = LocalVariableAccessInfo(localAccess: localAccess) - if mayAlias { - // Local allocations can only escape prior to assignment if they are boxed or inout_aliasable. - info.hasEscaped = true - } else if !isBoxed { + // inout_aliasable "allocation" has escaped on entry if any begin_access [dynamic] is present. + if mayAlias, info.access.kind == .beginAccess, + (localAccess.instruction as! BeginAccessInst).enforcement == .dynamic { + escapedOnEntry = true + } + if !isBoxed { // Boxed allocation requires reachability to determine whether the box escaped prior to assignment. info.hasEscaped = info.isEscape } accessMap[localAccess.instruction!] = info accessList.append(info) } + if escapedOnEntry { + for accessInfo in accessList { + accessInfo.hasEscaped = true + } + } return .continueWalk } From 305d75187a89f352dae9b4aed160e47d61c0c617 Mon Sep 17 00:00:00 2001 From: Andrew Trick Date: Tue, 21 Oct 2025 21:50:16 -0700 Subject: [PATCH 14/19] LocalVariableUtils: precisely handle function live-out Only record an outgoingArgument access when the current allocation is an outgoing argument and the function exiting instruction is ReturnInst. This avoids invalid SIL where we attempt to extend end_access up to an `unwind` but fail to actually materialize that end_access. --- .../Utilities/LocalVariableUtils.swift | 37 ++++++++++++------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/SwiftCompilerSources/Sources/Optimizer/Utilities/LocalVariableUtils.swift b/SwiftCompilerSources/Sources/Optimizer/Utilities/LocalVariableUtils.swift index ddb0b9f7e6339..d80d711d830b6 100644 --- a/SwiftCompilerSources/Sources/Optimizer/Utilities/LocalVariableUtils.swift +++ b/SwiftCompilerSources/Sources/Optimizer/Utilities/LocalVariableUtils.swift @@ -281,7 +281,8 @@ struct LocalVariableAccessMap: Collection, CustomStringConvertible { let context: Context let allocation: Value - let liveInAccess: LocalVariableAccess? + let isLiveIn: Bool + let isLiveOut: Bool // All mapped accesses have a valid instruction. // @@ -306,12 +307,19 @@ struct LocalVariableAccessMap: Collection, CustomStringConvertible { init?(allocation: Value, _ context: Context) { switch allocation { case is AllocBoxInst, is AllocStackInst: - self.liveInAccess = nil - break + self.isLiveIn = false + self.isLiveOut = false case let arg as FunctionArgument: switch arg.convention { - case .indirectIn, .indirectInout, .indirectInoutAliasable: - self.liveInAccess = LocalVariableAccess(.incomingArgument, nil) + case .indirectIn: + self.isLiveIn = true + self.isLiveOut = false + case .indirectInout, .indirectInoutAliasable: + self.isLiveIn = true + self.isLiveOut = true + case .indirectOut: + self.isLiveIn = false + self.isLiveOut = true default: return nil } @@ -779,7 +787,8 @@ extension LocalVariableReachableAccess { if block != accessMap.allocation.parentBlock { for predecessor in block.predecessors { blockList.pushIfNotVisited(predecessor) } } else if block == accessMap.function.entryBlock { - accessStack.push(accessMap.liveInAccess!) + assert(accessMap.isLiveIn) + accessStack.push(LocalVariableAccess(.incomingArgument, nil)) } case .assignValue: break @@ -829,7 +838,7 @@ extension LocalVariableReachableAccess { /// This performs a forward CFG walk to find known reachable uses from the function entry that guarantee liveness and /// may safely enclose dependent uses. private func gatherKnownLivenessUsesFromEntry(in accessStack: inout Stack) { - assert(accessMap.liveInAccess!.kind == .incomingArgument, "only an argument access is live in to the function") + assert(accessMap.isLiveIn, "only an argument access is live in to the function") let firstInst = accessMap.function.entryBlock.instructions.first! _ = gatherReachableUses(onOrAfter: firstInst, in: &accessStack, mode: .livenessUses) } @@ -906,7 +915,7 @@ extension LocalVariableReachableAccess { return false } forwardPropagateEffect(in: initialBlock, blockInfo: blockMap[initialBlock], effect: initialEffect, - blockList: &blockList, accessStack: &accessStack) + blockList: &blockList, accessStack: &accessStack, mode: mode) while let block = blockList.pop() { let blockInfo = blockMap[block] var currentEffect: ForwardDataFlowEffect? @@ -933,7 +942,7 @@ extension LocalVariableReachableAccess { return false } forwardPropagateEffect(in: block, blockInfo: blockInfo, effect: currentEffect, blockList: &blockList, - accessStack: &accessStack) + accessStack: &accessStack, mode: mode) } log("\n\(accessMap)") log(prefix: false, "Reachable access:\n\(accessStack.map({ String(describing: $0)}).joined(separator: "\n"))") @@ -945,16 +954,18 @@ extension LocalVariableReachableAccess { private func forwardPropagateEffect(in block: BasicBlock, blockInfo: BlockInfo?, effect: ForwardDataFlowEffect?, blockList: inout BasicBlockWorklist, - accessStack: inout Stack) { + accessStack: inout Stack, + mode: DataFlowMode) { switch effect { case .none, .read, .modify, .escape: if let blockInfo, blockInfo.hasDealloc { break } - if block.terminator.isFunctionExiting { - // Record any reachable function exit as .outgoingArgument. + // Assume that only a ReturnInst can return a live-out value. + // All other function exits are considered dead-ends. + if block.terminator is ReturnInstruction, accessMap.isLiveOut { accessStack.push(LocalVariableAccess(.outgoingArgument, block.terminator)) - } else if block.successors.isEmpty { + } else if block.successors.isEmpty, mode == .livenessUses { accessStack.push(LocalVariableAccess(.deadEnd, block.terminator)) } else { for successor in block.successors { blockList.pushIfNotVisited(successor) } From 78e64faba8f8e4736142e0134752b71f969fc6ac Mon Sep 17 00:00:00 2001 From: Andrew Trick Date: Fri, 10 Oct 2025 17:18:35 -0700 Subject: [PATCH 15/19] Test lifetime reassignment --- .../lifetime_dependence/semantics.swift | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test/SILOptimizer/lifetime_dependence/semantics.swift b/test/SILOptimizer/lifetime_dependence/semantics.swift index e81b716846888..7c83a7b94f095 100644 --- a/test/SILOptimizer/lifetime_dependence/semantics.swift +++ b/test/SILOptimizer/lifetime_dependence/semantics.swift @@ -484,6 +484,38 @@ struct TestInoutUnsafePointerExclusivity { } } +// ============================================================================= +// Copied dependence on mutable values +// ============================================================================= + +@_lifetime(dest: copy source) +func reassign(dest: inout T, source: T) { + dest = source +} + +@_lifetime(span: borrow array) +func testReassignToBorrowGood(span: inout Span, array: [Int]) { + reassign(dest: &span, source: array.span()) +} + +// Reassign from a local owned value +@_lifetime(span: copy span) +func testReassignToLocal(span: inout Span) { // expected-error{{lifetime-dependent variable 'span' escapes its scope}} + let array = [1, 2] // expected-note{{it depends on the lifetime of variable 'array'}} + reassign(dest: &span, source: array.span()) +} // expected-note{{this use causes the lifetime-dependent value to escape}} + +@_lifetime(span: copy span) +func testReassignToConsuming(span: inout Span, array: consuming [Int]) { // expected-error{{lifetime-dependent variable 'span' escapes its}} + reassign(dest: &span, source: array.span()) // expected-note{{it depends on this scoped access to variable 'array'}} +} // expected-note{{this use causes the lifetime-dependent value to escape}} + +@_lifetime(span: copy span) +func testReassignToBorrowBad(span: inout Span, array: [Int]) { // expected-error{{lifetime-dependent variable 'span' escapes its scope}} + // expected-note@-1{{it depends on the lifetime of argument 'array'}} + reassign(dest: &span, source: array.span()) +} // expected-note{{this use causes the lifetime-dependent value to escape}} + // ============================================================================= // Scoped dependence on property access // ============================================================================= From 285a6ce46bdfb9a4f7f0fc415239d7bc2fbebfa3 Mon Sep 17 00:00:00 2001 From: Andrew Trick Date: Sun, 19 Oct 2025 14:29:49 -0700 Subject: [PATCH 16/19] Test noescape closure capture lifetimes --- .../lifetime_dependence/semantics.swift | 66 +++++++++++++++++++ .../verify_diagnostics.swift | 20 ++++++ 2 files changed, 86 insertions(+) diff --git a/test/SILOptimizer/lifetime_dependence/semantics.swift b/test/SILOptimizer/lifetime_dependence/semantics.swift index 7c83a7b94f095..b15d5ddae3b17 100644 --- a/test/SILOptimizer/lifetime_dependence/semantics.swift +++ b/test/SILOptimizer/lifetime_dependence/semantics.swift @@ -74,6 +74,10 @@ struct MutableSpan: ~Escapable, ~Copyable { } } +extension MutableSpan { + mutating func update() {} +} + extension Array { @_lifetime(borrow self) borrowing func span() -> Span { @@ -516,6 +520,68 @@ func testReassignToBorrowBad(span: inout Span, array: [Int]) { // expected- reassign(dest: &span, source: array.span()) } // expected-note{{this use causes the lifetime-dependent value to escape}} +// ============================================================================= +// Copied dependence on mutable captures +// ============================================================================= + +func runClosure(_ body: ()->()) {} + +func testNoEscapeClosureCaptureMutation(spanArg: consuming MutableSpan) { + var spanCapture = spanArg + runClosure { + spanCapture.update() + } + _ = spanCapture +} + +func testNoEscapeClosureCaptureMutationDirect(spanArg: consuming MutableSpan) { + var spanCapture = spanArg; + { + spanCapture.update() + }() + _ = spanCapture +} + +func testNoEscapClosureCaptureUnsupported(spanArg: Span, array: consuming [Int]) { + var spanCapture = spanArg // expected-error{{lifetime-dependent variable 'spanCapture' escapes its scope}} + // expected-note@-2{{it depends on a closure capture; this is not yet supported}} + _ = spanCapture; + runClosure { + spanCapture = array.span() + } // expected-note{{this use causes the lifetime-dependent value to escape}} +} + +func testNoEscapClosureCaptureUnsupportedDirect(spanArg: Span, array: consuming [Int]) { + var spanCapture = spanArg // expected-error{{lifetime-dependent variable 'spanCapture' escapes its scope}} + // expected-note@-2{{it depends on a closure capture; this is not yet supported}} + _ = spanCapture; + { + spanCapture = array.span() + }() // expected-note{{this use causes the lifetime-dependent value to escape}} +} + +// 'spanBox' escapes to 'closure' before it is captured by 'escapedSpan'. The 'inout_aliasable' capture is considered +// escaping because the closure contains a 'begin_access [dynamic]'. +func testNoEscapClosureCaptureHasEscaped(spanArg: Span, array: consuming [Int]) { + // expected-error@-1{{lifetime-dependent variable 'spanArg' escapes its scope}} + // expected-note@-2{{it depends on the lifetime of argument 'spanArg'}} + // expected-note@-3{{it depends on a closure capture; this is not yet supported}} + var spanBox = spanArg // expected-error{{lifetime-dependent variable 'spanBox' escapes its scope}} + // expected-note@-1{{it depends on a closure capture; this is not yet supported}} + // expected-note@-2{{this use causes the lifetime-dependent value to escape}} + let closure = { spanBox } // expected-note{{this use causes the lifetime-dependent value to escape}} + + let escapedSpan = { + spanBox = array.span() // expected-error{{lifetime-dependent value escapes its scope}} + // expected-note@-1{{this use causes the lifetime-dependent value to escape}} + return closure() // expected-error{{lifetime-dependent value escapes its scope}} + // expected-note@-1{{it depends on the lifetime of this parent value}} + // expected-note@-2{{this use causes the lifetime-dependent value to escape}} + }() + _ = consume array + _ = escapedSpan +} + // ============================================================================= // Scoped dependence on property access // ============================================================================= diff --git a/test/SILOptimizer/lifetime_dependence/verify_diagnostics.swift b/test/SILOptimizer/lifetime_dependence/verify_diagnostics.swift index 2177a91345ec7..f7054e2443aca 100644 --- a/test/SILOptimizer/lifetime_dependence/verify_diagnostics.swift +++ b/test/SILOptimizer/lifetime_dependence/verify_diagnostics.swift @@ -409,3 +409,23 @@ func returnTempBorrow() -> Borrow { func test(inline: InlineInt) { inline.span.withUnsafeBytes { _ = $0 } } + +// ============================================================================= +// Closures +// ============================================================================= + +/// Test an autoclosure that invokes a mutable method where `Self: ~Escapable`. +/// The @inout_aliasable argument has an implicit @_lifetime(capture: copy capture), +/// and no begin_access [dynamic] is present in the closure. +extension MutableSpan { + @_lifetime(self: copy self) + public mutating func canUpdate() -> Bool { return true } + + @_lifetime(self: copy self) + public mutating func testAutoclosure(z: Bool) -> Bool { + if z && canUpdate() { + return true + } + return false + } +} From 44bffe3ecbdfae80f280fd19100023920af5c443 Mon Sep 17 00:00:00 2001 From: Andrew Trick Date: Sun, 19 Oct 2025 16:52:28 -0700 Subject: [PATCH 17/19] Update tests for local variable reachability for debug output --- .../lifetime_dependence_util.sil | 1 + .../lifetime_dependence/local_var_util.sil | 22 +++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/test/SILOptimizer/lifetime_dependence/lifetime_dependence_util.sil b/test/SILOptimizer/lifetime_dependence/lifetime_dependence_util.sil index e582f211c3685..d3b0f41194cb1 100644 --- a/test/SILOptimizer/lifetime_dependence/lifetime_dependence_util.sil +++ b/test/SILOptimizer/lifetime_dependence/lifetime_dependence_util.sil @@ -212,6 +212,7 @@ entry(%0 : @owned $C, %1 : @owned $D, %2 : @guaranteed $D, %3 : $*D, %4 : $*D): // CHECK-LABEL: dependence_access: lifetime_dependence_use with: %0 // CHECK: LifetimeDependence uses of: %0 = argument of bb0 : $*NE // CHECK-NEXT: Leaf use: operand #0 of end_access %{{.*}} : $*NE +// CHECK-NEXT: Returned inout: %0 = argument of bb0 : $*NE // CHECK-NEXT: dependence_access: lifetime_dependence_use with: %0 sil [ossa] @dependence_access : $@convention(thin) (@inout NE, @owned C) -> () { bb0(%0 : $*NE, %1 : @owned $C): diff --git a/test/SILOptimizer/lifetime_dependence/local_var_util.sil b/test/SILOptimizer/lifetime_dependence/local_var_util.sil index 3215b3770ad3d..bd6e48b81135c 100644 --- a/test/SILOptimizer/lifetime_dependence/local_var_util.sil +++ b/test/SILOptimizer/lifetime_dependence/local_var_util.sil @@ -18,20 +18,20 @@ sil @makeDepNE : $@convention(thin) (@inout NE) -> @lifetime(borrow address_for_ // CHECK-LABEL: testNEInitNoEscape: local_variable_reachable_uses with: %1, @instruction // CHECK: ### Access map: // CHECK-NEXT: Access map for: %{{.*}} = alloc_box ${ var NE }, var, name "self" -// CHECK-NEXT: assign: true, store destroy_value %{{.*}} : ${ var NE } -// CHECK-NEXT: assign: false, escape %{{.*}} = address_to_pointer %{{.*}} : $*NE to $Builtin.RawPointer -// CHECK-NEXT: assign: true, beginAccess %{{.*}} = begin_access [modify] [unknown] %{{.*}} : $*NE -// CHECK-NEXT: assign: false, load %{{.*}} = load [copy] %{{.*}} : $*NE -// CHECK-NEXT: assign: true, beginAccess %{{.*}} = begin_access [modify] [unknown] %{{.*}} : $*NE +// CHECK-NEXT: assign: value, hasEscaped: unknown, store, destroy_value %{{.*}} : ${ var NE } +// CHECK-NEXT: assign: no, hasEscaped: true, escape, %{{.*}} = address_to_pointer %{{.*}} : $*NE to $Builtin.RawPointer +// CHECK-NEXT: assign: value, hasEscaped: unknown, beginAccess, %{{.*}} = begin_access [modify] [unknown] %{{.*}} : $*NE +// CHECK-NEXT: assign: no, hasEscaped: unknown, load, %{{.*}} = load [copy] %{{.*}} : $*NE +// CHECK-NEXT: assign: value, hasEscaped: unknown, beginAccess, %{{.*}} = begin_access [modify] [unknown] %{{.*}} : $*NE // CHECK-NEXT: ### Modify: %{{.*}} = begin_access [modify] [unknown] %4 : $*NE // CHECK-NEXT: ### Reachable access: -// CHECK-NEXT: load %{{.*}} = load [copy] %{{.*}} : $*NE +// CHECK-NEXT: load, %{{.*}} = load [copy] %{{.*}} : $*NE // CHECK-NEXT: testNEInitNoEscape: local_variable_reachable_uses with: %1, @instruction // CHECK-LABEL: testNEInitNoEscape: local_variable_reaching_assignments with: %1, @instruction // CHECK: ### Instruction: end_access %{{.*}} : $*NE // CHECK-NEXT: ### Reachable assignments: -// CHECK-NEXT: beginAccess %21 = begin_access [modify] [unknown] %4 : $*NE +// CHECK-NEXT: beginAccess, %21 = begin_access [modify] [unknown] %4 : $*NE // CHECK-NEXT: testNEInitNoEscape: local_variable_reaching_assignments with: %1, @instruction sil [ossa] @testNEInitNoEscape : $@convention(thin) (@inout NE) -> @lifetime(borrow 0) @owned NE { bb0(%0 : $*NE): @@ -86,10 +86,10 @@ bb3: // CHECK-LABEL: testNEInitEscape: local_variable_reachable_uses with: %1, @instruction // CHECK: ### Access map: // CHECK-NEXT: Access map for: %{{.*}} = alloc_box ${ var NE }, var, name "self" -// CHECK-NEXT: assign: true, store destroy_value %{{.*}} : ${ var NE } -// CHECK-NEXT: assign: false, load %{{.*}} = load [copy] %{{.*}} : $*NE -// CHECK-NEXT: assign: true, beginAccess %{{.*}} = begin_access [modify] [unknown] %{{.*}} : $*NE -// CHECK-NEXT: assign: false, escape %6 = address_to_pointer %4 : $*NE to $Builtin.RawPointer +// CHECK-NEXT: assign: value, hasEscaped: unknown, store, destroy_value %{{.*}} : ${ var NE } +// CHECK-NEXT: assign: no, hasEscaped: unknown, load, %{{.*}} = load [copy] %{{.*}} : $*NE +// CHECK-NEXT: assign: value, hasEscaped: unknown, beginAccess, %{{.*}} = begin_access [modify] [unknown] %{{.*}} : $*NE +// CHECK-NEXT: assign: no, hasEscaped: true, escape, %6 = address_to_pointer %4 : $*NE to $Builtin.RawPointer // CHECK-NEXT: ### Modify: %{{.*}} = begin_access [modify] [unknown] %4 : $*NE // CHECK-NEXT: !!! Reachable escape // CHECK-NEXT: testNEInitEscape: local_variable_reachable_uses with: %1, @instruction From e37fc23f03ea95cf2b3a19cea601d2aedbac6747 Mon Sep 17 00:00:00 2001 From: Andrew Trick Date: Mon, 20 Oct 2025 21:41:04 -0700 Subject: [PATCH 18/19] Update lifetime tests for improved diagnostics --- .../SILOptimizer/lifetime_dependence/stdlib_span.swift | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/test/SILOptimizer/lifetime_dependence/stdlib_span.swift b/test/SILOptimizer/lifetime_dependence/stdlib_span.swift index 04e88bae63ca8..d1d45445b5864 100644 --- a/test/SILOptimizer/lifetime_dependence/stdlib_span.swift +++ b/test/SILOptimizer/lifetime_dependence/stdlib_span.swift @@ -54,11 +54,9 @@ func testUBPStorageCopy(ubp: UnsafeRawBufferPointer) -> RawSpan { @available(SwiftStdlib 6.2, *) func testUBPStorageEscape(array: [Int64]) { - var span = RawSpan() - array.withUnsafeBytes { - span = $0.storage // expected-error {{lifetime-dependent value escapes its scope}} - // expected-note @-2{{it depends on the lifetime of argument '$0'}} - // expected-note @-2{{this use causes the lifetime-dependent value to escape}} - } + var span = RawSpan() // expected-error{{lifetime-dependent variable 'span' escapes its scope}} + array.withUnsafeBytes { // expected-note{{it depends on the lifetime of argument '$0'}} + span = $0.storage + } // expected-note{{this use causes the lifetime-dependent value to escape}} read(span) } From 630f28d8ed2242dffba5ac485dfd4d187176b545 Mon Sep 17 00:00:00 2001 From: Andrew Trick Date: Wed, 22 Oct 2025 21:46:30 -0700 Subject: [PATCH 19/19] Enable a test case for addressable lifetimes. --- .../addressable_lifetime_with_resilience.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/SILOptimizer/lifetime_dependence/addressable_lifetime_with_resilience.swift b/test/SILOptimizer/lifetime_dependence/addressable_lifetime_with_resilience.swift index 2f3e01a460334..1f7d5059c1dbb 100644 --- a/test/SILOptimizer/lifetime_dependence/addressable_lifetime_with_resilience.swift +++ b/test/SILOptimizer/lifetime_dependence/addressable_lifetime_with_resilience.swift @@ -17,9 +17,6 @@ public struct Resilient { borrowing func getSpan() -> RawSpan { fatalError() } } -/* -// TODO (rdar://151268401): We still get spurious errors about escaping `self` -// in cases where the wrapped type is concretely addressable-for-dependencies. internal struct AFDWrapper { let inner: AFDResilient @@ -32,4 +29,3 @@ public struct AFDResilient { @_lifetime(borrow self) borrowing func getSpan() -> RawSpan { fatalError() } } -*/