//===--- LocalVariableUtils.swift - Utilities for local variable access ---===// // // This source file is part of the Swift.org open source project // // Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See https://swift.org/LICENSE.txt for license information // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// /// /// SIL operates on three kinds of addressable memory: /// /// 1. Temporary RValues. These are recognized by AddressInitializationWalker. These largely disappear with opaque SIL /// values. /// /// 2. Local variables. These are always introduced by either a VarDeclInstruction or a Function argument with non-nil /// Argument.varDecl. They are accessed according to the structural rules define in this file. Loading or reassigning a /// property requires a formal access (begin_access). /// /// 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 private let verbose = false private func log(prefix: Bool = true, _ message: @autoclosure () -> String) { if verbose { debugLog(prefix: prefix, message()) } } // Local variables are accessed in one of these ways. // // Note: @in is only immutable up to when it is destroyed, so still requires a local live range. // // A .dependenceSource access creates a new dependent value that keeps this local alive. // // %local = alloc_stack // this local // %md = mark_dependence %val on %local // mark_dependence_addr %adr on %local // // The effect of .dependenceSource on reachability is like a load of this local. The dependent value depends on any // value in this local that reaches this point. // // A .dependenceDest access is the point where another value becomes dependent on this local: // // %local = alloc_stack // this local // %md = mark_dependence %local on %val // mark_dependence_addr %local on %val // // The effect of .dependenceDest on reachability is like a store of this local. All uses of this local reachable from // this point are uses of the dependence base. // // Note that the same mark_dependence_addr often involves two locals: // // mark_dependence_addr %localDest on %localSource // struct LocalVariableAccess: CustomStringConvertible { enum Kind { case incomingArgument // @in, @inout, @inout_aliasable case outgoingArgument // @inout, @inout_aliasable case inoutYield // indirect yield from this accessor case beginAccess // Reading or reassigning a 'var' case load // Reading a 'let'. Returning 'var' from an initializer. case dependenceSource // A value/address depends on this local here (like a load) case dependenceDest // This local depends on another value/address here (like a store) case store // 'var' initialization and destruction case storeBorrow // scoped initialization of temporaries case apply // indirect arguments case escape // alloc_box captures case deadEnd // unreachable } let kind: Kind // All access have an operand except .incomingArgument and .outgoingArgument. let operand: Operand? // All access have a representative instruction except .incomingArgument. var instruction: Instruction? init(_ kind: Kind, _ operand: Operand) { self.kind = kind self.operand = operand self.instruction = operand.instruction } init(_ kind: Kind, _ instruction: Instruction?) { self.kind = kind self.operand = nil self.instruction = instruction } /// Does this access either fully or partially modify the variable? var isModify: Bool { switch kind { case .beginAccess: switch (instruction as! BeginAccessInst).accessKind { case .read, .deinit: return false case .`init`, .modify: return true } case .load, .dependenceSource, .dependenceDest, .deadEnd: return false case .incomingArgument, .outgoingArgument, .store, .storeBorrow, .inoutYield: return true case .apply: let apply = instruction as! FullApplySite if let convention = apply.convention(of: operand!) { // A direct argument may modify a captured variable. return !convention.isIndirectIn } // A callee argument may modify a captured variable. return true case .escape: return true } } var isEscape: Bool { switch kind { case .escape: return true default: return false } } var description: String { var str = "" switch self.kind { case .incomingArgument: str += "incomingArgument" case .outgoingArgument: str += "outgoingArgument" case .inoutYield: str += "inoutYield" case .beginAccess: str += "beginAccess" case .load: str += "load" case .dependenceSource: str += "dependenceSource" case .dependenceDest: str += "dependenceDest" case .store: str += "store" case .storeBorrow: str += "storeBorrow" case .apply: str += "apply" case .escape: str += "escape" case .deadEnd: str += "deadEnd" } if let inst = instruction { str += ", \(inst)" } return str } } /// Class instance for caching local variable information. class LocalVariableAccessInfo: CustomStringConvertible { let access: LocalVariableAccess private var _isFullyAssigned: IsFullyAssigned? /// Cache whether the allocation has escaped prior to this access. /// This returns `nil` until reachability is computed. var hasEscaped: Bool? init(localAccess: LocalVariableAccess) { self.access = localAccess switch localAccess.kind { case .beginAccess: switch (localAccess.instruction as! BeginAccessInst).accessKind { case .read, .deinit: self._isFullyAssigned = .no case .`init`, .modify: break // lazily compute full assignment } case .load, .dependenceSource, .dependenceDest: self._isFullyAssigned = .no case .store, .storeBorrow: if let store = localAccess.instruction as? StoringInstruction { self._isFullyAssigned = LocalVariableAccessInfo.isBase(address: store.destination) ? .value : .no } else { self._isFullyAssigned = .value } case .apply: // This logic is consistent with AddressInitializationWalker.appliedAddressUse() let apply = localAccess.instruction as! FullApplySite 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 } case .escape: self._isFullyAssigned = .no self.hasEscaped = true case .inoutYield: self._isFullyAssigned = .no case .incomingArgument, .outgoingArgument, .deadEnd: fatalError("Function arguments are never mapped to LocalVariableAccessInfo") } } var instruction: Instruction { access.instruction! } var isModify: Bool { access.isModify } 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? Only returns '.value' if this access does not read the incoming value. func isFullyAssigned(_ context: Context) -> IsFullyAssigned { if let cached = _isFullyAssigned { return cached } if access.kind != .beginAccess { fatalError("Invalid LocalVariableAccess") } assert(isModify) let beginAccess = access.instruction as! BeginAccessInst 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)" } // Does this address correspond to the local variable's base address? Any writes to this address will be a full // assignment. This should match any instructions that the LocalVariableAccessMap initializer below recognizes as an // allocation. static private func isBase(address: Value) -> Bool { // TODO: create an API alternative to 'accessPath' that bails out on the first path component and succeeds on the // first begin_access. let path = address.accessPath return path.base.isLocal && path.projectionPath.isEmpty } } /// Model the formal accesses of an addressable variable introduced by an alloc_box, alloc_stack, or indirect /// FunctionArgument. /// /// This instantiates a unique LocalVariableAccessInfo instances for each access instruction, caching it an an access /// map. /// /// TODO: In addition to isFullyAssigned, consider adding a lazily computed access path if any need arises. struct LocalVariableAccessMap: Collection, CustomStringConvertible { let context: Context let allocation: Value let isLiveIn: Bool let isLiveOut: Bool // All mapped accesses have a valid instruction. // // TODO: replace the List,Dictionary with an OrderedDictionary. private var accessList: [LocalVariableAccessInfo] private var accessMap: Dictionary var function: Function { allocation.parentFunction } 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 } return false } init?(allocation: Value, _ context: Context) { switch allocation { case is AllocBoxInst, is AllocStackInst: self.isLiveIn = false self.isLiveOut = false case let arg as FunctionArgument: switch arg.convention { 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 } default: return nil } self.context = context self.allocation = allocation accessList = [] accessMap = [:] if walkAccesses(context) == .abortWalk { return nil } } private mutating func walkAccesses(_ context: Context) -> WalkResult { var walker = LocalVariableAccessWalker(context) defer { walker.deinitialize() } if walker.walkDown(allocation: allocation) == .abortWalk { return .abortWalk } var escapedOnEntry = false for localAccess in walker.accessStack { let info = LocalVariableAccessInfo(localAccess: localAccess) // 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 } var startIndex: Int { 0 } var endIndex: Int { accessList.count } func index(after index: Int) -> Int { return index + 1 } subscript(_ accessIndex: Int) -> LocalVariableAccessInfo { accessList[accessIndex] } subscript(instruction: Instruction) -> LocalVariableAccessInfo? { accessMap[instruction] } var description: String { "Access map for: \(allocation)\n" + map({String(describing: $0)}).joined(separator: "\n") } } /// Gather the accesses of a local allocation: alloc_box, alloc_stack, @in, @inout. /// /// This is used to populate LocalVariableAccessMap. /// /// Start walk: /// walkDown(allocation:) /// /// TODO: This should only handle allocations that have a var decl. And SIL verification should guarantee that the /// allocated address is never used by a path projection outside of an access. struct LocalVariableAccessWalker { let context: Context var visitedValues: ValueSet var accessStack: Stack init(_ context: Context) { self.context = context self.visitedValues = ValueSet(context) self.accessStack = Stack(context) } mutating func deinitialize() { visitedValues.deinitialize() accessStack.deinitialize() } mutating func walkDown(allocation: Value) -> WalkResult { if allocation.type.isAddress { return walkDownAddressUses(address: allocation) } return walkDown(root: allocation) } private mutating func visit(_ localAccess: LocalVariableAccess) { accessStack.push(localAccess) } } // Extend ForwardingDefUseWalker to walk down uses of the box. extension LocalVariableAccessWalker : ForwardingDefUseWalker { mutating func needWalk(for value: Value) -> Bool { visitedValues.insert(value) } mutating func nonForwardingUse(of operand: Operand) -> WalkResult { if operand.instruction.isIncidentalUse { return .continueWalk } switch operand.instruction { case let pbi as ProjectBoxInst: return walkDownAddressUses(address: pbi) case let transition as OwnershipTransitionInstruction: return walkDownUses(of: transition.ownershipResult, using: operand) case is DestroyValueInst: visit(LocalVariableAccess(.store, operand)) case is DeallocBoxInst: break case let markDep as MarkDependenceInst: assert(markDep.baseOperand == operand) visit(LocalVariableAccess(.dependenceSource, operand)) default: visit(LocalVariableAccess(.escape, operand)) } return .continueWalk } mutating func deadValue(_ value: Value, using operand: Operand?) -> WalkResult { return .continueWalk } } // Extend AddressUseVisitor to find all access scopes, initializing stores, and captures. extension LocalVariableAccessWalker: AddressUseVisitor { private mutating func walkDownAddressUses(address: Value) -> WalkResult { for operand in address.uses.ignoreTypeDependence { if classifyAddress(operand: operand) == .abortWalk { return .abortWalk } } return .continueWalk } // Handle storage type projections, like MarkUninitializedInst. Path projections are visited for field // initialization because SILGen does not emit begin_access [init] consistently. // // Stack-allocated temporaries are also treated like local variables for the purpose of finding all uses. Such // temporaries do not have access scopes, so we need to walk down any projection that may be used to initialize the // temporary. mutating func projectedAddressUse(of operand: Operand, into value: Value) -> WalkResult { if let md = value as? MarkDependenceInst { if operand == md.valueOperand { // Intercept mark_dependence destination to record an access point which can be used like a store when finding // all uses that affect the base after the point that the dependence was marked. visit(LocalVariableAccess(.dependenceDest, operand)) // walk down the forwarded address as usual... } else { // A dependence is similar to loading from its source. Downstream uses are not accesses of the original local. visit(LocalVariableAccess(.dependenceSource, operand)) return .continueWalk } } return walkDownAddressUses(address: value) } mutating func scopedAddressUse(of operand: Operand) -> WalkResult { switch operand.instruction { case is BeginAccessInst: visit(LocalVariableAccess(.beginAccess, operand)) return .continueWalk case is BeginApplyInst: visit(LocalVariableAccess(.apply, operand)) return .continueWalk case is LoadBorrowInst: visit(LocalVariableAccess(.load, operand)) return .continueWalk case is StoreBorrowInst: visit(LocalVariableAccess(.storeBorrow, operand)) return .continueWalk default: // A StoreBorrow should be guarded by an access scope. // // TODO: verify that we never hit this case. return .abortWalk // unexpected } } mutating func scopeEndingAddressUse(of operand: Operand) -> WalkResult { return .abortWalk // unexpected } mutating func leafAddressUse(of operand: Operand) -> WalkResult { switch operand.instruction { case is StoringInstruction, is SourceDestAddrInstruction, is DestroyAddrInst, is DeinitExistentialAddrInst, is InjectEnumAddrInst, is TupleAddrConstructorInst, is InitBlockStorageHeaderInst, is PackElementSetInst: // Handle instructions that initialize both temporaries and local variables. If operand's address is the same as // the local variable's address, then this fully kills operand liveness. The original value in operand's address // cannot be used in any way. visit(LocalVariableAccess(.store, operand)) case let md as MarkDependenceAddrInst: assert(operand == md.addressOperand) visit(LocalVariableAccess(.dependenceDest, operand)) case is SwitchEnumAddrInst: // switch_enum_addr is truly a leaf address use. It does not produce a new value. But in every other respect it is // like a load. visit(LocalVariableAccess(.load, operand)) case is DeallocStackInst: break default: if !operand.instruction.isIncidentalUse { visit(LocalVariableAccess(.escape, operand)) } } return .continueWalk } mutating func appliedAddressUse(of operand: Operand, by apply: FullApplySite) -> WalkResult { visit(LocalVariableAccess(.apply, operand)) return .continueWalk } mutating func yieldedAddressUse(of operand: Operand) -> WalkResult { visit(LocalVariableAccess(.inoutYield, operand)) return .continueWalk } mutating func dependentAddressUse(of operand: Operand, dependentValue value: Value) -> WalkResult { // Find all uses of partial_apply [on_stack]. if let pai = value as? PartialApplyInst, !pai.mayEscape { var walker = NonEscapingClosureDefUseWalker(context) defer { walker.deinitialize() } if walker.walkDown(closure: pai) == .abortWalk { return .abortWalk } for operand in walker.applyOperandStack { visit(LocalVariableAccess(.apply, operand)) } } // No other dependent uses can access to memory at this address. return .continueWalk } mutating func dependentAddressUse(of operand: Operand, dependentAddress address: Value) -> WalkResult { // No other dependent uses can access to memory at this address. return .continueWalk } mutating func loadedAddressUse(of operand: Operand, intoValue value: Value) -> WalkResult { visit(LocalVariableAccess(.load, operand)) return .continueWalk } mutating func loadedAddressUse(of operand: Operand, intoAddress address: Operand) -> WalkResult { visit(LocalVariableAccess(.load, operand)) return .continueWalk } mutating func escapingAddressUse(of operand: Operand) -> WalkResult { visit(LocalVariableAccess(.escape, operand)) return .continueWalk } mutating func unknownAddressUse(of operand: Operand) -> WalkResult { return .abortWalk } } /// Map LocalVariableAccesses to basic blocks. /// /// This caches flow-insensitive information about the local variable's accesses, for use with a flow-sensitive /// analysis. /// /// This allocates a dictionary for the block state rather than using BasicBlockSets in case the client wants to cache /// 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 -> assignLifetime -> assignValue enum BlockEffect: Int { case read // no modification or escape case modify // no full assignment or escape case escape // no full assignment 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 { guard let other else { return self } return other.rawValue > self.rawValue ? other : self } } struct BlockInfo { var effect: BlockEffect? var hasDealloc: Bool } var blockAccess: Dictionary subscript(_ block: BasicBlock) -> BlockInfo? { blockAccess[block] } init(accessMap: LocalVariableAccessMap) { blockAccess = [:] for accessInfo in accessMap { let block = accessInfo.instruction.parentBlock let oldEffect = blockAccess[block]?.effect let newEffect = BlockEffect(for: accessInfo, accessMap.context).meet(oldEffect) blockAccess[block] = BlockInfo(effect: newEffect, hasDealloc: false) } // Find blocks that end the variable's scope. This is destroy_value for boxes. // // TODO: SIL verify that owned boxes are never forwarded. let deallocations = accessMap.allocation.uses.lazy.filter { $0.instruction is Deallocation || $0.instruction is DestroyValueInst } for dealloc in deallocations { let block = dealloc.instruction.parentBlock blockAccess[block, default: BlockInfo(effect: nil, hasDealloc: true)].hasDealloc = true } } } extension LocalVariableAccessBlockMap.BlockEffect { init(for accessInfo: LocalVariableAccessInfo, _ context: some Context) { // Assign from the lowest to the highest lattice values... self = .read if accessInfo.isModify { self = .modify } if accessInfo.isEscape { self = .escape } switch accessInfo.isFullyAssigned(context) { case .no: break case .lifetime: self = .assignLifetime case .value: self = .assignValue } } } /// Map an allocation (alloc_box, alloc_stack, @in, @inout) onto its reachable accesses. class LocalVariableReachabilityCache { var cache = Dictionary() func reachability(for allocation: Value, _ context: some Context) -> LocalVariableReachableAccess? { if let reachability = cache[allocation.hashable] { return reachability } if let reachabilty = LocalVariableReachableAccess(allocation: allocation, context) { cache[allocation.hashable] = reachabilty return reachabilty } return nil } } /// Flow-sensitive, pessimistic data flow of local variable access. This finds all potentially reachable uses of an /// assignment. This does not determine whether the assignment is available at each use; that would require optimistic, /// iterative data flow. The only data flow state is pessimistic reachability, which is implicit in the block worklist. struct LocalVariableReachableAccess { let context: Context let accessMap: LocalVariableAccessMap let blockMap: LocalVariableAccessBlockMap init?(allocation: Value, _ context: Context) { guard let accessMap = LocalVariableAccessMap(allocation: allocation, context) else { return nil } self.context = context self.accessMap = accessMap self.blockMap = LocalVariableAccessBlockMap(accessMap: accessMap) } } 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 // ignored and the nearest assignments above 'instruction' are still gathered. func gatherReachingAssignments(for instruction: Instruction, in accessStack: inout Stack) -> Bool { var blockList = BasicBlockWorklist(context) defer { blockList.deinitialize() } var initialEffect: BlockEffect? = nil if let prev = instruction.previous { initialEffect = backwardScanAccesses(before: prev, accessStack: &accessStack) } if !backwardPropagateEffect(in: instruction.parentBlock, effect: initialEffect, blockList: &blockList, accessStack: &accessStack) { return false } while let block = blockList.pop() { let blockInfo = blockMap[block] var currentEffect = blockInfo?.effect // lattice: none -> read -> modify -> escape -> assign // // `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, .assignLifetime: break case .assignValue: currentEffect = backwardScanAccesses(before: block.instructions.reversed().first!, accessStack: &accessStack) } if !backwardPropagateEffect(in: block, effect: currentEffect, blockList: &blockList, accessStack: &accessStack) { return false } } // TODO: Verify that the accessStack.isEmpty condition never occurs. return !accessStack.isEmpty } private func backwardPropagateEffect(in block: BasicBlock, effect: BlockEffect?, blockList: inout BasicBlockWorklist, accessStack: inout Stack) -> Bool { switch effect { case .none, .read, .modify, .assignLifetime: if block != accessMap.allocation.parentBlock { for predecessor in block.predecessors { blockList.pushIfNotVisited(predecessor) } } else if block == accessMap.function.entryBlock { assert(accessMap.isLiveIn) accessStack.push(LocalVariableAccess(.incomingArgument, nil)) } case .assignValue: break case .escape: return false } return true } // 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 .assignValue stops the scan. private func backwardScanAccesses(before first: Instruction, accessStack: inout Stack) -> BlockEffect? { var currentEffect: BlockEffect? for inst in ReverseInstructionList(first: first) { guard let accessInfo = accessMap[inst] else { continue } currentEffect = BlockEffect(for: accessInfo, accessMap.context).meet(currentEffect) switch currentEffect! { case .read, .modify, .assignLifetime: continue case .assignValue: accessStack.push(accessInfo.access) case .escape: break } break } return currentEffect } } // Find reachable accesses... extension LocalVariableReachableAccess { /// 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, mode: .livenessUses) return } gatherKnownLivenessUsesFromEntry(in: &accessStack) } /// 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, mode: .livenessUses) } /// 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`. /// /// Returns true if all possible reachable uses were visited. Returns false if any escapes may reach `modifyInst` or /// are reachable from `modifyInst`. /// /// 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 gatherAllReachableDependentUses(of modifyInst: Instruction, in accessStack: inout Stack) -> Bool { guard let accessInfo = accessMap[modifyInst] else { return false } if accessInfo.hasEscaped == nil { findAllEscapesPriorToAccess() } if accessInfo.hasEscaped! { return 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`. /// /// For DataFlowMode.livenessUses, this gathers the full known live range, including destroys and reassignments /// continuing past escapes. /// /// 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, mode: DataFlowMode) -> Bool { if let term = begin as? TermInst { for succ in term.successors { if !gatherReachableUses(onOrAfter: succ.instructions.first!, in: &accessStack, mode: mode) { return false } } return true } else { 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`. /// /// 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, mode: DataFlowMode) -> Bool { var blockList = BasicBlockWorklist(context) defer { blockList.deinitialize() } let initialBlock = begin.parentBlock 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, mode: mode) while let block = blockList.pop() { let blockInfo = blockMap[block] 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 below, except when // forwardScanAccesses finds an early disallowed escape before the assign. switch currentEffect { case .none: break case .escape: if mode == .dependentUses { break } fallthrough case .read, .modify, .assign: let firstInst = block.instructions.first! currentEffect = forwardScanAccesses(after: firstInst, accessStack: &accessStack, mode: mode) } if mode == .dependentUses, currentEffect == .escape { return false } forwardPropagateEffect(in: block, blockInfo: blockInfo, effect: currentEffect, blockList: &blockList, accessStack: &accessStack, mode: mode) } log("\n\(accessMap)") log(prefix: false, "Reachable access:\n\(accessStack.map({ String(describing: $0)}).joined(separator: "\n"))") return true } typealias BlockEffect = LocalVariableAccessBlockMap.BlockEffect typealias BlockInfo = LocalVariableAccessBlockMap.BlockInfo private func forwardPropagateEffect(in block: BasicBlock, blockInfo: BlockInfo?, effect: ForwardDataFlowEffect?, blockList: inout BasicBlockWorklist, accessStack: inout Stack, mode: DataFlowMode) { switch effect { case .none, .read, .modify, .escape: if let blockInfo, blockInfo.hasDealloc { break } // 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, mode == .livenessUses { accessStack.push(LocalVariableAccess(.deadEnd, block.terminator)) } else { for successor in block.successors { blockList.pushIfNotVisited(successor) } } case .assign: break } } // 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 for .dependentUses. private func forwardScanAccesses(after first: Instruction, accessStack: inout Stack, mode: DataFlowMode) -> ForwardDataFlowEffect? { var currentEffect: ForwardDataFlowEffect? for inst in InstructionList(first: first) { guard let accessInfo = accessMap[inst] else { continue } currentEffect = mode.getForwardEffect(BlockEffect(for: accessInfo, accessMap.context)).meet(currentEffect) switch currentEffect! { case .assign: if mode == .livenessUses || accessInfo.isFullyAssigned(context) != .value { accessStack.push(accessInfo.access) } return currentEffect case .escape: if mode == .dependentUses { log("Local variable: \(accessMap.allocation)\n escapes at \(inst)") return currentEffect } fallthrough case .read, .modify: accessStack.push(accessInfo.access) } } return currentEffect } } // Find prior escapes... extension LocalVariableReachableAccess { /// For alloc_box only, find escapes (captures) of the box prior to each access. /// As a result, AccessInfo.hasEscaped will be non-nil for every access. /// /// This is an optimistic forward dataflow that propagates the escape bit to accesses. /// A block can be scanned at most twice. Once after it is marked visited to find any escapes within the block. The /// second time after it is marked escaped to propagate the hasEscaped bit to accesses within the block. private func findAllEscapesPriorToAccess() { var visitedBlocks = BasicBlockSet(context) var escapedBlocks = BasicBlockSet(context) var blockList = Stack(context) defer { visitedBlocks.deinitialize() escapedBlocks.deinitialize() blockList.deinitialize() } let forwardPropagate = { (from: BasicBlock, hasEscaped: Bool) in if let blockInfo = blockMap[from], blockInfo.hasDealloc { return } for successor in from.successors { if hasEscaped { if escapedBlocks.insert(successor) { blockList.push(successor) } } else if visitedBlocks.insert(successor) { blockList.push(successor) } } } var hasEscaped = propagateEscapeInBlock(after: accessMap.allocation.nextInstruction, hasEscaped: false) forwardPropagate(accessMap.allocation.parentBlock, hasEscaped) while let block = blockList.pop() { hasEscaped = escapedBlocks.contains(block) hasEscaped = propagateEscapeInBlock(after: block.instructions.first!, hasEscaped: hasEscaped) forwardPropagate(block, hasEscaped) } } private func propagateEscapeInBlock(after begin: Instruction, hasEscaped: Bool) -> Bool { var hasEscaped = hasEscaped for inst in InstructionList(first: begin) { guard let accessInfo = accessMap[inst] else { continue } if accessInfo.isEscape { hasEscaped = true } else { accessInfo.hasEscaped = hasEscaped } } return hasEscaped } } let localVariableReachingAssignmentsTest = FunctionTest("local_variable_reaching_assignments") { function, arguments, context in let allocation = arguments.takeValue() let instruction = arguments.takeInstruction() print("### Allocation: \(allocation)") let localReachabilityCache = LocalVariableReachabilityCache() guard let localReachability = localReachabilityCache.reachability(for: allocation, context) else { print("No reachability") return } print("### Access map:") print(localReachability.accessMap) print("### Instruction: \(instruction)") var reachingAssignments = Stack(context) defer { reachingAssignments.deinitialize() } guard localReachability.gatherReachingAssignments(for: instruction, in: &reachingAssignments) else { print("!!! Reaching escape") return } print("### Reachable assignments:") print(reachingAssignments.map({ String(describing: $0)}).joined(separator: "\n")) } let localVariableReachableUsesTest = FunctionTest("local_variable_reachable_uses") { function, arguments, context in let allocation = arguments.takeValue() let modify = arguments.takeInstruction() print("### Allocation: \(allocation)") let localReachabilityCache = LocalVariableReachabilityCache() guard let localReachability = localReachabilityCache.reachability(for: allocation, context) else { print("No reachability") return } print("### Access map:") print(localReachability.accessMap) print("### Modify: \(modify)") var reachableUses = Stack(context) defer { reachableUses.deinitialize() } guard localReachability.gatherAllReachableDependentUses(of: modify, in: &reachableUses) else { print("!!! Reachable escape") return } print("### Reachable access:") print(reachableUses.map({ String(describing: $0)}).joined(separator: "\n")) }