mirror of
https://github.com/apple/swift.git
synced 2025-12-14 20:36:38 +01:00
875 lines
31 KiB
Swift
875 lines
31 KiB
Swift
//===--- 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 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 requires a local live range.
|
|
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 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, .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 .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.
|
|
/// 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:
|
|
if let store = localAccess.instruction as? StoringInstruction {
|
|
self._isFullyAssigned = LocalVariableAccessInfo.isBase(address: store.destination)
|
|
} else {
|
|
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 .inoutYield:
|
|
self._isFullyAssigned = false
|
|
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)"
|
|
}
|
|
|
|
// 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 {
|
|
switch address {
|
|
case is AllocBoxInst, is AllocStackInst, is BeginAccessInst:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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 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<Instruction, LocalVariableAccessInfo>
|
|
|
|
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<LocalVariableAccess>
|
|
|
|
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 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 {
|
|
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 SwitchEnumAddrInst, 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 yieldedAddressUse(of operand: Operand) -> WalkResult {
|
|
visit(LocalVariableAccess(.inoutYield, 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<BasicBlock, BlockInfo>
|
|
|
|
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<HashableValue, LocalVariableReachableAccess>()
|
|
|
|
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<LocalVariableAccess>)
|
|
-> 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<LocalVariableAccess>)
|
|
-> 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<LocalVariableAccess>)
|
|
-> 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<LocalVariableAccess>) {
|
|
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<LocalVariableAccess>) {
|
|
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<LocalVariableAccess>) -> 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<LocalVariableAccess>,
|
|
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<LocalVariableAccess>,
|
|
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<LocalVariableAccess>) {
|
|
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<LocalVariableAccess>,
|
|
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
|
|
}
|
|
}
|