diff --git a/SwiftCompilerSources/Sources/Optimizer/FunctionPasses/LifetimeDependenceDiagnostics.swift b/SwiftCompilerSources/Sources/Optimizer/FunctionPasses/LifetimeDependenceDiagnostics.swift index b7bd49bb7923a..d472f714f1f54 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) { @@ -184,7 +189,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 @@ -257,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 @@ -265,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. @@ -287,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) } } @@ -358,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) { @@ -552,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/AddressUtils.swift b/SwiftCompilerSources/Sources/Optimizer/Utilities/AddressUtils.swift index 3f1b5b50cea8b..b913801cc23ca 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,8 @@ extension AccessBase { // modification of memory. struct AddressInitializationWalker: AddressDefUseWalker, AddressUseVisitor { let baseAddress: Value + let requireFullyAssigned: IsFullyAssigned + let onRead: WalkResult let context: any Context var walkDownCache = WalkerCache() @@ -318,18 +320,22 @@ 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, + allowRead: Bool = true, _ context: some Context) -> AccessBase.Initializer? { - var walker = AddressInitializationWalker(baseAddress: baseAddr, 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, _ 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") @@ -387,12 +393,26 @@ 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 { - if operand.isAddressInitialization { + switch apply.fullyAssigns(operand: operand) { + case .no: + if onRead == .abortWalk { + return .abortWalk + } + break + case .lifetime: + if onRead == .abortWalk { + return .abortWalk + } + if requireFullyAssigned == .value { + break + } + fallthrough + case .value: return setInitializer(instruction: operand.instruction) } guard let convention = apply.convention(of: operand) else { @@ -403,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 { @@ -629,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/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.. 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 @@ -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,18 +1048,15 @@ 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) } - 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 +1103,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: @@ -1098,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 @@ -1180,8 +1216,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..d80d711d830b6 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 @@ -158,7 +164,7 @@ struct LocalVariableAccess: CustomStringConvertible { str += "deadEnd" } if let inst = instruction { - str += "\(inst)" + str += ", \(inst)" } return str } @@ -168,7 +174,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 +186,38 @@ class LocalVariableAccessInfo: CustomStringConvertible { case .beginAccess: switch (localAccess.instruction as! BeginAccessInst).accessKind { case .read, .deinit: - self._isFullyAssigned = false + self._isFullyAssigned = .no case .`init`, .modify: break // lazily compute full assignment } case .load, .dependenceSource, .dependenceDest: - self._isFullyAssigned = false + self._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 + 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 = false + self._isFullyAssigned = .no } case .escape: - self._isFullyAssigned = false + self._isFullyAssigned = .no self.hasEscaped = true case .inoutYield: - self._isFullyAssigned = false + self._isFullyAssigned = .no case .incomingArgument, .outgoingArgument, .deadEnd: fatalError("Function arguments are never mapped to LocalVariableAccessInfo") } @@ -216,8 +230,8 @@ 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. - func isFullyAssigned(_ context: Context) -> Bool { + /// access? Only returns '.value' if this access does not read the incoming value. + func isFullyAssigned(_ context: Context) -> IsFullyAssigned { if let cached = _isFullyAssigned { return cached } @@ -226,13 +240,22 @@ 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, + allowRead: false, context) + != nil { + _isFullyAssigned = .value + } else if AddressInitializationWalker.findSingleInitializer(ofAddress: beginAccess, + requireFullyAssigned: .lifetime, context) != nil { + _isFullyAssigned = .lifetime + } else { + _isFullyAssigned = .no + } return _isFullyAssigned! } var description: String { return "assign: \(_isFullyAssigned == nil ? "unknown" : String(describing: _isFullyAssigned!)), " + + "hasEscaped: \(hasEscaped == nil ? "unknown" : String(describing: hasEscaped!)), " + "\(access)" } @@ -258,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. // @@ -270,6 +294,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 @@ -280,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 } @@ -307,18 +341,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 } @@ -553,12 +595,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 +650,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 +695,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 +766,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 +783,14 @@ 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!) + assert(accessMap.isLiveIn) + accessStack.push(LocalVariableAccess(.incomingArgument, nil)) } - case .assign: + case .assignValue: break case .escape: return false @@ -704,7 +799,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 +809,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 +824,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) { - assert(accessMap.liveInAccess!.kind == .incomingArgument, "only an argument access is live in to the function") + /// 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.isLiveIn, "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 +854,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,68 +873,76 @@ 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) + blockList: &blockList, accessStack: &accessStack, mode: mode) 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, - accessStack: &accessStack) + accessStack: &accessStack, mode: mode) } log("\n\(accessMap)") log(prefix: false, "Reachable access:\n\(accessStack.map({ String(describing: $0)}).joined(separator: "\n"))") @@ -848,17 +952,20 @@ 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) { + accessStack: inout Stack, + mode: DataFlowMode) { switch effect { case .none, .read, .modify, .escape: if let blockInfo, blockInfo.hasDealloc { break } - if block.terminator.isFunctionExiting { + // 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) } @@ -869,24 +976,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 || accessInfo.isFullyAssigned(context) != .value { accessStack.push(accessInfo.access) } return currentEffect case .escape: - if !lifetime { + if mode == .dependentUses { log("Local variable: \(accessMap.allocation)\n escapes at \(inst)") return currentEffect } @@ -993,7 +1100,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 } diff --git a/SwiftCompilerSources/Sources/SIL/ApplySite.swift b/SwiftCompilerSources/Sources/SIL/ApplySite.swift index 2aa5098412604..ce4d502ba2221 100644 --- a/SwiftCompilerSources/Sources/SIL/ApplySite.swift +++ b/SwiftCompilerSources/Sources/SIL/ApplySite.swift @@ -79,6 +79,15 @@ public struct ApplyOperandConventions : Collection { calleeArgumentIndex(ofOperandIndex: operandIndex)!] } + public func parameterDependence(targetOperandIndex: Int, sourceOperandIndex: Int) -> 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 @@ -113,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 } @@ -247,6 +272,30 @@ extension ApplySite { : operandConventions[parameterDependencies: idx] } + public func parameterDependence(target: Operand, source: Operand) -> LifetimeDependenceConvention? { + 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) } diff --git a/SwiftCompilerSources/Sources/SIL/Argument.swift b/SwiftCompilerSources/Sources/SIL/Argument.swift index 020918dafa80a..837489c4b3a5e 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,37 +298,36 @@ 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) } + /// 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. @@ -365,14 +364,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] 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.. 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() } } -*/ 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 diff --git a/test/SILOptimizer/lifetime_dependence/semantics.swift b/test/SILOptimizer/lifetime_dependence/semantics.swift index e81b716846888..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 { @@ -484,6 +488,100 @@ 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}} + +// ============================================================================= +// 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/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) } 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 + } +}