//===--- 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 addressible memory: /// /// 1. Temporary RValues. These are recognied 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. /// //===----------------------------------------------------------------------===// import SIL private let verbose = false private func log(_ message: @autoclosure () -> String) { if verbose { print("### \(message())") } } // Local variables are accessed in one of these ways. // // Note: @in is only immutable up to when it is destroyed, so still requies a local live range. struct LocalVariableAccess: CustomStringConvertible { enum Kind { case incomingArgument // @in, @inout, @inout_aliasable case outgoingArgument // @inout, @inout_aliasable case beginAccess // Reading or reassinging a 'var' case load // Reading a 'let'. Returning 'var' from an initializer. case store // 'var' initialization and destruction case apply // indirect arguments case escape // alloc_box captures } 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: return false case .incomingArgument, .outgoingArgument, .store: 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 .beginAccess: str += "beginAccess" case .load: str += "load" case .store: str += "store" case .apply: str += "apply" case .escape: str += "escape" } if let inst = instruction { str += "\(inst)" } return str } } /// Class instance for caching local variable information. class LocalVariableAccessInfo: CustomStringConvertible { let access: LocalVariableAccess private var _isFullyAssigned: Bool? /// Cache whether the allocation has escaped prior to this access. /// For alloc_box, 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 = false case .`init`, .modify: break // lazily compute full assignment } case .load: self._isFullyAssigned = false case .store: self._isFullyAssigned = true case .apply: let apply = localAccess.instruction as! FullApplySite if let convention = apply.convention(of: localAccess.operand!) { self._isFullyAssigned = convention.isIndirectOut } else { self._isFullyAssigned = false } case .escape: self._isFullyAssigned = false self.hasEscaped = true case .incomingArgument, .outgoingArgument: 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. func isFullyAssigned(_ context: Context) -> Bool { if let cached = _isFullyAssigned { return cached } if access.kind != .beginAccess { fatalError("Invalid LocalVariableAccess") } assert(isModify) let beginAccess = access.instruction as! BeginAccessInst let initializer = AddressInitializationWalker.findSingleInitializer(ofAddress: beginAccess, context: context) _isFullyAssigned = (initializer != nil) ? true : false return _isFullyAssigned! } var description: String { return "full-assign: \(_isFullyAssigned == nil ? "unknown" : String(describing: _isFullyAssigned!)) " + "\(access)" } } /// Model the formal accesses of an addressible 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 liveInAccess: LocalVariableAccess? // 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 } 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.liveInAccess = nil break case let arg as FunctionArgument: switch arg.convention { case .indirectIn, .indirectInout, .indirectInoutAliasable: self.liveInAccess = LocalVariableAccess(.incomingArgument, nil) 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 } 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 { // Boxed allocation requires reachability to determine whether the box escaped prior to assignment. info.hasEscaped = info.isEscape } accessMap[localAccess.instruction!] = info accessList.append(info) } 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] } public var description: String { "Access map:\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 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 should not be visited. They only // occur inside the access. // // Exception: stack-allocated temporaries may be 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 { // TODO: we need an abstraction for path projections. For local variables, these cannot occur outside of an access. switch operand.instruction { case is StructElementAddrInst, is TupleElementAddrInst, is IndexAddrInst, is TailAddrInst, is UncheckedTakeEnumDataAddrInst, is OpenExistentialAddrInst: return .abortWalk // Projections used to initialize a temporary case is InitEnumDataAddrInst, is InitExistentialAddrInst: fallthrough default: 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 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. visit(LocalVariableAccess(.store, 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 dependentAddressUse(of operand: Operand, into 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 loadedAddressUse(of operand: Operand, into value: Value) -> WalkResult { visit(LocalVariableAccess(.load, operand)) return .continueWalk } mutating func loadedAddressUse(of operand: Operand, into 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 -> assign 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. /// 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 } if accessInfo.isFullyAssigned(context) { self = .assign } } } /// 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) } } // Find reaching assignments... extension LocalVariableReachableAccess { // Gather all fully assigned accesses that reach `instruction`. func gatherReachingAssignments(for instruction: Instruction, in accessStack: inout Stack) -> Bool { var blockList = BasicBlockWorklist(context) defer { blockList.deinitialize() } let initialEffect = backwardScanAccesses(before: instruction, 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 after an assign. switch currentEffect { case .none, .read, .modify, .escape: break case .assign: 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: if block != accessMap.allocation.parentBlock { for predecessor in block.predecessors { blockList.pushIfNotVisited(predecessor) } } else if block == accessMap.function.entryBlock { accessStack.push(accessMap.liveInAccess!) } case .assign: 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 .assign 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: continue case .assign: accessStack.push(accessInfo.access) break case .escape: break } } return currentEffect } } // Find reachable accesses... extension LocalVariableReachableAccess { /// This performs a forward CFG walk to find known reachable uses from `assignment`. This ignores aliasing and /// escapes. func gatherKnownReachableUses(from assignment: LocalVariableAccess, in accessStack: inout Stack) { if let modifyInst = assignment.instruction { _ = gatherReachableUses(after: modifyInst, in: &accessStack, allowEscape: true) } gatherKnownReachableUsesFromEntry(in: &accessStack) } /// This performs a forward CFG walk to find known reachable uses from the function entry. This ignores aliasing and /// escapes. private func gatherKnownReachableUsesFromEntry(in accessStack: inout Stack) { assert(accessMap.liveInAccess!.kind == .incomingArgument, "only an argument access is live in to the function") let firstInst = accessMap.function.entryBlock.instructions.first! _ = gatherReachableUses(onOrAfter: firstInst, in: &accessStack, allowEscape: true) } /// 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. /// /// Returns true if all possible reachable uses were visited. Returns false if any escapes may reach `modifyInst` 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. /// /// 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 { guard let accessInfo = accessMap[modifyInst] else { return false } if accessInfo.hasEscaped == nil { findAllEscapesPriorToAccess() } if accessInfo.hasEscaped! { return false } return gatherReachableUses(after: modifyInst, in: &accessStack, allowEscape: false) } /// This performs a forward CFG walk to find all uses of this local variable reachable after `begin`. /// /// If `allowEscape` is true, then this returns false if the walk ended early because of a reachable escape. private func gatherReachableUses(after begin: Instruction, in accessStack: inout Stack, allowEscape: Bool) -> Bool { if let term = begin as? TermInst { for succ in term.successors { if !gatherReachableUses(onOrAfter: succ.instructions.first!, in: &accessStack, allowEscape: allowEscape) { return false } } return true } else { return gatherReachableUses(onOrAfter: begin.next!, in: &accessStack, allowEscape: allowEscape) } } /// This performs a forward CFG walk to find all uses of this local variable reachable after and including `begin`. /// /// If `allowEscape` is true, then this returns false if the walk ended early because of a reachable escape. private func gatherReachableUses(onOrAfter begin: Instruction, in accessStack: inout Stack, allowEscape: Bool) -> Bool { var blockList = BasicBlockWorklist(context) defer { blockList.deinitialize() } let initialBlock = begin.parentBlock let initialEffect = forwardScanAccesses(after: begin, accessStack: &accessStack, allowEscape: allowEscape) if !allowEscape, initialEffect == .escape { return false } forwardPropagateEffect(in: initialBlock, blockInfo: blockMap[initialBlock], effect: initialEffect, blockList: &blockList, accessStack: &accessStack) while let block = blockList.pop() { let blockInfo = blockMap[block] var currentEffect = blockInfo?.effect // lattice: none -> read -> modify -> escape -> assign // // `blockInfo.effect` is the same as `currentEffect` returned by forwardScanAccesses, except when an early // disallowed escape happens before an assign. switch currentEffect { case .none: break case .escape: if !allowEscape { break } fallthrough case .read, .modify, .assign: let firstInst = block.instructions.first! currentEffect = forwardScanAccesses(after: firstInst, accessStack: &accessStack, allowEscape: allowEscape) } if !allowEscape, currentEffect == .escape { return false } forwardPropagateEffect(in: block, blockInfo: blockInfo, effect: currentEffect, blockList: &blockList, accessStack: &accessStack) } log("\(accessMap)") log("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: BlockEffect?, blockList: inout BasicBlockWorklist, accessStack: inout Stack) { switch effect { case .none, .read, .modify, .escape: if let blockInfo, blockInfo.hasDealloc { break } if block.terminator.isFunctionExiting { accessStack.push(LocalVariableAccess(.outgoingArgument, 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 if allowEscape is false. private func forwardScanAccesses(after first: Instruction, accessStack: inout Stack, allowEscape: Bool) -> BlockEffect? { var currentEffect: BlockEffect? for inst in InstructionList(first: first) { guard let accessInfo = accessMap[inst] else { continue } currentEffect = BlockEffect(for: accessInfo, accessMap.context).meet(currentEffect) switch currentEffect! { case .assign: return currentEffect case .escape: if !allowEscape { 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 = BasicBlockWorklist(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.pushIfNotVisited(successor) } } else if visitedBlocks.insert(successor) { blockList.pushIfNotVisited(successor) } } } var hasEscaped = propagateEscapeInBlock(after: accessMap.allocation.nextInstruction, hasEscaped: false) forwardPropagate(accessMap.allocation.parentBlock, hasEscaped) while let block = blockList.pop() { hasEscaped = escapedBlocks.insert(block) hasEscaped = propagateEscapeInBlock(after: block.instructions.first!, hasEscaped: hasEscaped) forwardPropagate(accessMap.allocation.parentBlock, 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 } }