LifetimeDependenceDefUseWalker: handle addressable dependencies

This commit is contained in:
Andrew Trick
2025-03-25 23:01:10 -07:00
parent d9dd93560d
commit 69b6b1d309
4 changed files with 120 additions and 49 deletions

View File

@@ -56,12 +56,12 @@ protocol AddressUseVisitor {
-> WalkResult
/// A loaded address use propagates the value at the address.
mutating func loadedAddressUse(of operand: Operand, into value: Value)
mutating func loadedAddressUse(of operand: Operand, intoValue value: Value)
-> WalkResult
/// A loaded address use propagates the value at the address to the
/// destination address operand.
mutating func loadedAddressUse(of operand: Operand, into address: Operand)
mutating func loadedAddressUse(of operand: Operand, intoAddress address: Operand)
-> WalkResult
/// Yielding an address may modify the value at the yield, but not past the yield. The yielded value may escape, but
@@ -137,14 +137,14 @@ extension AddressUseVisitor {
case is LoadInst, is LoadUnownedInst, is LoadWeakInst, is ValueMetatypeInst, is ExistentialMetatypeInst,
is PackElementGetInst:
let svi = operand.instruction as! SingleValueInstruction
return loadedAddressUse(of: operand, into: svi)
return loadedAddressUse(of: operand, intoValue: svi)
case is YieldInst:
return yieldedAddressUse(of: operand)
case let sdai as SourceDestAddrInstruction
where sdai.sourceOperand == operand:
return loadedAddressUse(of: operand, into: sdai.destinationOperand)
return loadedAddressUse(of: operand, intoAddress: sdai.destinationOperand)
case let sdai as SourceDestAddrInstruction
where sdai.destinationOperand == operand:
@@ -401,12 +401,12 @@ extension AddressInitializationWalker {
return convention.isIndirectIn ? .continueWalk : .abortWalk
}
mutating func loadedAddressUse(of operand: Operand, into value: Value)
mutating func loadedAddressUse(of operand: Operand, intoValue value: Value)
-> WalkResult {
return .continueWalk
}
mutating func loadedAddressUse(of operand: Operand, into address: Operand)
mutating func loadedAddressUse(of operand: Operand, intoAddress address: Operand)
-> WalkResult {
return .continueWalk
}

View File

@@ -584,7 +584,7 @@ extension LifetimeDependenceDefUseWalker {
// Override ForwardingDefUseWalker.
mutating func walkDown(operand: Operand) -> WalkResult {
// Initially delegate all usess to OwnershipUseVisitor.
// Initially delegate all uses to OwnershipUseVisitor.
// walkDownDefault will be called for uses that forward ownership.
return classify(operand: operand)
}
@@ -631,7 +631,7 @@ extension LifetimeDependenceDefUseWalker {
// like [nonescaping] even though they are not considered OSSA
// borrows until after resolution.
assert(operand == mdi.baseOperand)
return dependentUse(of: operand, into: mdi)
return dependentUse(of: operand, dependentValue: mdi)
case is ExistentialMetatypeInst, is FixLifetimeInst, is WitnessMethodInst,
is DynamicMethodBranchInst, is ValueMetatypeInst,
@@ -676,11 +676,24 @@ extension LifetimeDependenceDefUseWalker {
return escapingDependence(on: operand)
}
mutating func dependentUse(of operand: Operand, into value: Value)
// Handle address or non-address operands.
mutating func dependentUse(of operand: Operand, dependentValue value: Value)
-> WalkResult {
return walkDownUses(of: value, using: operand)
}
// Handle address or non-address operands.
mutating func dependentUse(of operand: Operand, dependentAddress address: Value)
-> WalkResult {
// Consider this a leaf use in addition to the dependent address uses, which might all occur earlier.
if leafUse(of: operand) == .abortWalk {
return .abortWalk
}
// The lifetime dependence is effectively "copied into" the dependent address. Find all uses of the dependent
// address as if this were a stored use.
return visitStoredUses(of: operand, into: address)
}
mutating func borrowingUse(of operand: Operand,
by borrowInst: BorrowingInstruction)
-> WalkResult {
@@ -736,7 +749,7 @@ extension LifetimeDependenceDefUseWalker {
return visitAppliedUse(of: operand, by: apply)
}
mutating func loadedAddressUse(of operand: Operand, into value: Value) -> WalkResult {
mutating func loadedAddressUse(of operand: Operand, intoValue value: Value) -> WalkResult {
// Record the load itself, in case the loaded value is Escapable.
if leafUse(of: operand) == .abortWalk {
return .abortWalk
@@ -745,7 +758,7 @@ extension LifetimeDependenceDefUseWalker {
}
// copy_addr
mutating func loadedAddressUse(of operand: Operand, into address: Operand) -> WalkResult {
mutating func loadedAddressUse(of operand: Operand, intoAddress address: Operand) -> WalkResult {
if leafUse(of: operand) == .abortWalk {
return .abortWalk
}
@@ -761,18 +774,12 @@ extension LifetimeDependenceDefUseWalker {
}
mutating func dependentAddressUse(of operand: Operand, dependentValue value: Value) -> WalkResult {
walkDownUses(of: value, using: operand)
dependentUse(of: operand, dependentValue: value)
}
// mark_dependence_addr
mutating func dependentAddressUse(of operand: Operand, dependentAddress address: Value) -> WalkResult {
// Consider this a leaf use in addition to the dependent address uses, which might all occur earlier.
if leafUse(of: operand) == .abortWalk {
return .abortWalk
}
// The lifetime dependence is effectively "copied into" the dependent address. Find all uses of the dependent
// address as if this were a stored use.
return visitStoredUses(of: operand, into: address)
dependentUse(of: operand, dependentAddress: address)
}
mutating func escapingAddressUse(of operand: Operand) -> WalkResult {
@@ -898,18 +905,29 @@ extension LifetimeDependenceDefUseWalker {
case .load:
switch localAccess.instruction! {
case let load as LoadInst:
return loadedAddressUse(of: localAccess.operand!, into: load)
return loadedAddressUse(of: localAccess.operand!, intoValue: load)
case let load as LoadBorrowInst:
return loadedAddressUse(of: localAccess.operand!, into: load)
return loadedAddressUse(of: localAccess.operand!, intoValue: load)
case let copyAddr as SourceDestAddrInstruction:
return loadedAddressUse(of: localAccess.operand!, into: copyAddr.destinationOperand)
return loadedAddressUse(of: localAccess.operand!, intoAddress: copyAddr.destinationOperand)
default:
return .abortWalk
}
case .dependence:
// An address-forwarding mark_dependence is simply a marker that indicates the start of an in-memory
// dependent value. Typically, it has no uses. If it does have uses, then they are visited earlier by
// LocalVariableAccessWalker to record any other local accesses.
case .dependenceSource:
switch localAccess.instruction! {
case let md as MarkDependenceInst:
if md.type.isAddress {
return loadedAddressUse(of: localAccess.operand!, intoAddress: md.valueOperand)
}
return loadedAddressUse(of: localAccess.operand!, intoValue: md)
case let md as MarkDependenceAddrInst:
return loadedAddressUse(of: localAccess.operand!, intoAddress: md.addressOperand)
default:
return .abortWalk
}
case .dependenceDest:
// Simply a marker that indicates the start of an in-memory dependent value. If this was a mark_dependence, uses
// of its forwarded address has were visited by LocalVariableAccessWalker and recorded as separate local accesses.
return .continueWalk
case .store:
let si = localAccess.operand!.instruction as! StoringInstruction
@@ -948,13 +966,13 @@ extension LifetimeDependenceDefUseWalker {
// because a mark_dependence [nonescaping] represents the
// dependence.
if let result = apply.singleDirectResult, !result.isEscapable {
if dependentUse(of: operand, into: result) == .abortWalk {
if dependentUse(of: operand, dependentValue: result) == .abortWalk {
return .abortWalk
}
}
for resultAddr in apply.indirectResultOperands
where !resultAddr.value.isEscapable {
if visitStoredUses(of: operand, into: resultAddr.value) == .abortWalk {
if dependentUse(of: operand, dependentAddress: resultAddr.value) == .abortWalk {
return .abortWalk
}
}

View File

@@ -36,6 +36,29 @@ private func log(_ message: @autoclosure () -> String) {
// 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
@@ -43,7 +66,8 @@ struct LocalVariableAccess: CustomStringConvertible {
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 dependence // A mark_dependence after an apply with an indirect result. No effect.
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 apply // indirect arguments
case escape // alloc_box captures
@@ -77,7 +101,7 @@ struct LocalVariableAccess: CustomStringConvertible {
case .`init`, .modify:
return true
}
case .load, .dependence:
case .load, .dependenceSource, .dependenceDest:
return false
case .incomingArgument, .outgoingArgument, .store, .inoutYield:
return true
@@ -116,8 +140,10 @@ struct LocalVariableAccess: CustomStringConvertible {
str += "beginAccess"
case .load:
str += "load"
case .dependence:
str += "dependence"
case .dependenceSource:
str += "dependenceSource"
case .dependenceDest:
str += "dependenceDest"
case .store:
str += "store"
case .apply:
@@ -152,7 +178,7 @@ class LocalVariableAccessInfo: CustomStringConvertible {
case .`init`, .modify:
break // lazily compute full assignment
}
case .load, .dependence:
case .load, .dependenceSource, .dependenceDest:
self._isFullyAssigned = false
case .store:
if let store = localAccess.instruction as? StoringInstruction {
@@ -365,7 +391,7 @@ extension LocalVariableAccessWalker : ForwardingDefUseWalker {
break
case let markDep as MarkDependenceInst:
assert(markDep.baseOperand == operand)
visit(LocalVariableAccess(.dependence, operand))
visit(LocalVariableAccess(.dependenceSource, operand))
default:
visit(LocalVariableAccess(.escape, operand))
}
@@ -381,10 +407,6 @@ extension LocalVariableAccessWalker : ForwardingDefUseWalker {
extension LocalVariableAccessWalker: AddressUseVisitor {
private mutating func walkDownAddressUses(address: Value) -> WalkResult {
for operand in address.uses.ignoreTypeDependence {
if let md = operand.instruction as? MarkDependenceInst, operand == md.valueOperand {
// Record the forwarding mark_dependence as a fake access before continuing to walk down.
visit(LocalVariableAccess(.dependence, operand))
}
if classifyAddress(operand: operand) == .abortWalk {
return .abortWalk
}
@@ -399,6 +421,13 @@ extension LocalVariableAccessWalker: AddressUseVisitor {
// 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 {
// 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.
if let md = value as? MarkDependenceInst {
assert(operand == md.valueOperand)
visit(LocalVariableAccess(.dependenceDest, operand))
// walk down the forwarded address as usual...
}
return walkDownAddressUses(address: value)
}
@@ -432,6 +461,9 @@ extension LocalVariableAccessWalker: AddressUseVisitor {
is PackElementSetInst:
// Handle instructions that initialize both temporaries and local variables.
visit(LocalVariableAccess(.store, operand))
case let md as MarkDependenceAddrInst:
assert(operand == md.addressOperand)
visit(LocalVariableAccess(.dependenceDest, operand))
case is DeallocStackInst:
break
default:
@@ -473,12 +505,12 @@ extension LocalVariableAccessWalker: AddressUseVisitor {
return .continueWalk
}
mutating func loadedAddressUse(of operand: Operand, into value: Value) -> WalkResult {
mutating func loadedAddressUse(of operand: Operand, intoValue value: Value) -> WalkResult {
visit(LocalVariableAccess(.load, operand))
return .continueWalk
}
mutating func loadedAddressUse(of operand: Operand, into address: Operand) -> WalkResult {
mutating func loadedAddressUse(of operand: Operand, intoAddress address: Operand) -> WalkResult {
visit(LocalVariableAccess(.load, operand))
return .continueWalk
}

View File

@@ -208,7 +208,8 @@ struct InteriorLivenessResult: CustomDebugStringConvertible {
/// - forwardingUse(of:isInnerlifetime:)
/// - interiorPointerUse(of:into:)
/// - pointerEscapingUse(of:)
/// - dependentUse(of:into:)
/// - dependentUse(of:dependentValue:)
/// - dependentUse(of:dependentAddress:)
/// - borrowingUse(of:by:)
///
/// This only visits the first level of uses. The implementation may transitively visit forwarded, borrowed, dependent,
@@ -269,7 +270,13 @@ protocol OwnershipUseVisitor {
/// there are no explicit scope-ending operations. Instead
/// BorrowingInstruction.scopeEndingOperands will return the final
/// consumes in the dependent value's forwarding chain.
mutating func dependentUse(of operand: Operand, into value: Value) -> WalkResult
mutating func dependentUse(of operand: Operand, dependentValue value: Value) -> WalkResult
/// A use that creates an implicit borrow scope over all reachable uses of a value stored in
/// `dependentAddress`. This could conservatively be handled has `pointerEscapingUse`, but note that accessing the
/// `dependentAddress` only keeps the original owner alive, it cannot modify the original (modifying a dependent
/// address is still just a "read" of the dependence source.
mutating func dependentUse(of operand: Operand, dependentAddress address: Value) -> WalkResult
/// A use that is scoped to an inner borrow scope.
mutating func borrowingUse(of operand: Operand, by borrowInst: BorrowingInstruction) -> WalkResult
@@ -384,6 +391,9 @@ extension OwnershipUseVisitor {
return forwardingUse(of: operand, isInnerLifetime: false)
case .pointerEscape:
if let mdai = operand.instruction as? MarkDependenceAddrInst, operand == mdai.baseOperand {
return dependentUse(of: operand, dependentAddress: mdai.address)
}
return pointerEscapingUse(of: operand)
case .instantaneousUse, .forwardingUnowned, .unownedInstantaneousUse, .bitwiseEscape:
@@ -413,6 +423,9 @@ extension OwnershipUseVisitor {
if operand.instruction is ProjectBoxInst {
return visitInteriorPointerUse(of: operand)
}
if let mdai = operand.instruction as? MarkDependenceAddrInst, operand == mdai.baseOperand {
return dependentUse(of: operand, dependentAddress: mdai.address)
}
return pointerEscapingUse(of: operand)
case .instantaneousUse, .forwardingUnowned, .unownedInstantaneousUse, .bitwiseEscape, .endBorrow, .reborrow:
@@ -436,12 +449,14 @@ extension OwnershipUseVisitor {
switch operand.instruction {
case let pai as PartialApplyInst:
assert(!pai.mayEscape)
return dependentUse(of: operand, into: pai)
return dependentUse(of: operand, dependentValue: pai)
case let mdi as MarkDependenceInst:
// .borrow operand ownership only applies to the base operand of a non-escaping markdep that forwards a
// non-address value.
assert(operand == mdi.baseOperand && mdi.isNonEscaping)
return dependentUse(of: operand, into: mdi)
return dependentUse(of: operand, dependentValue: mdi)
case let bfi as BorrowedFromInst where !bfi.borrowedPhi.isReborrow:
return dependentUse(of: operand, into: bfi)
return dependentUse(of: operand, dependentValue: bfi)
default:
return borrowingUse(of: operand,
by: BorrowingInstruction(operand.instruction)!)
@@ -586,7 +601,7 @@ extension InteriorUseWalker: OwnershipUseVisitor {
}
// Handle partial_apply [on_stack] and mark_dependence [nonescaping].
mutating func dependentUse(of operand: Operand, into value: Value) -> WalkResult {
mutating func dependentUse(of operand: Operand, dependentValue value: Value) -> WalkResult {
// OSSA lifetime ignores trivial types.
if value.type.isTrivial(in: function) {
return .continueWalk
@@ -602,6 +617,12 @@ extension InteriorUseWalker: OwnershipUseVisitor {
return walkDownUses(of: value)
}
mutating func dependentUse(of operand: Operand, dependentAddress address: Value) -> WalkResult {
// An mutable local variable depends a value that depends on the original interior pointer. This would require data
// flow to find local uses. InteriorUseWalker only walks the SSA uses.
pointerEscapingUse(of: operand)
}
mutating func pointerEscapingUse(of operand: Operand) -> WalkResult {
if useVisitor(operand) == .abortWalk {
return .abortWalk
@@ -699,12 +720,12 @@ extension InteriorUseWalker: AddressUseVisitor {
return .continueWalk
}
mutating func loadedAddressUse(of operand: Operand, into value: Value)
mutating func loadedAddressUse(of operand: Operand, intoValue value: Value)
-> WalkResult {
return .continueWalk
}
mutating func loadedAddressUse(of operand: Operand, into address: Operand)
mutating func loadedAddressUse(of operand: Operand, intoAddress address: Operand)
-> WalkResult {
return .continueWalk
}