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

View File

@@ -584,7 +584,7 @@ extension LifetimeDependenceDefUseWalker {
// Override ForwardingDefUseWalker. // Override ForwardingDefUseWalker.
mutating func walkDown(operand: Operand) -> WalkResult { 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. // walkDownDefault will be called for uses that forward ownership.
return classify(operand: operand) return classify(operand: operand)
} }
@@ -631,7 +631,7 @@ extension LifetimeDependenceDefUseWalker {
// like [nonescaping] even though they are not considered OSSA // like [nonescaping] even though they are not considered OSSA
// borrows until after resolution. // borrows until after resolution.
assert(operand == mdi.baseOperand) assert(operand == mdi.baseOperand)
return dependentUse(of: operand, into: mdi) return dependentUse(of: operand, dependentValue: mdi)
case is ExistentialMetatypeInst, is FixLifetimeInst, is WitnessMethodInst, case is ExistentialMetatypeInst, is FixLifetimeInst, is WitnessMethodInst,
is DynamicMethodBranchInst, is ValueMetatypeInst, is DynamicMethodBranchInst, is ValueMetatypeInst,
@@ -676,11 +676,24 @@ extension LifetimeDependenceDefUseWalker {
return escapingDependence(on: operand) 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 { -> WalkResult {
return walkDownUses(of: value, using: operand) 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, mutating func borrowingUse(of operand: Operand,
by borrowInst: BorrowingInstruction) by borrowInst: BorrowingInstruction)
-> WalkResult { -> WalkResult {
@@ -736,7 +749,7 @@ extension LifetimeDependenceDefUseWalker {
return visitAppliedUse(of: operand, by: apply) 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. // Record the load itself, in case the loaded value is Escapable.
if leafUse(of: operand) == .abortWalk { if leafUse(of: operand) == .abortWalk {
return .abortWalk return .abortWalk
@@ -745,7 +758,7 @@ extension LifetimeDependenceDefUseWalker {
} }
// copy_addr // 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 { if leafUse(of: operand) == .abortWalk {
return .abortWalk return .abortWalk
} }
@@ -761,18 +774,12 @@ extension LifetimeDependenceDefUseWalker {
} }
mutating func dependentAddressUse(of operand: Operand, dependentValue value: Value) -> WalkResult { mutating func dependentAddressUse(of operand: Operand, dependentValue value: Value) -> WalkResult {
walkDownUses(of: value, using: operand) dependentUse(of: operand, dependentValue: value)
} }
// mark_dependence_addr // mark_dependence_addr
mutating func dependentAddressUse(of operand: Operand, dependentAddress address: Value) -> WalkResult { 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. dependentUse(of: operand, dependentAddress: address)
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 escapingAddressUse(of operand: Operand) -> WalkResult { mutating func escapingAddressUse(of operand: Operand) -> WalkResult {
@@ -898,19 +905,30 @@ extension LifetimeDependenceDefUseWalker {
case .load: case .load:
switch localAccess.instruction! { switch localAccess.instruction! {
case let load as LoadInst: case let load as LoadInst:
return loadedAddressUse(of: localAccess.operand!, into: load) return loadedAddressUse(of: localAccess.operand!, intoValue: load)
case let load as LoadBorrowInst: case let load as LoadBorrowInst:
return loadedAddressUse(of: localAccess.operand!, into: load) return loadedAddressUse(of: localAccess.operand!, intoValue: load)
case let copyAddr as SourceDestAddrInstruction: case let copyAddr as SourceDestAddrInstruction:
return loadedAddressUse(of: localAccess.operand!, into: copyAddr.destinationOperand) return loadedAddressUse(of: localAccess.operand!, intoAddress: copyAddr.destinationOperand)
default: default:
return .abortWalk return .abortWalk
} }
case .dependence: case .dependenceSource:
// An address-forwarding mark_dependence is simply a marker that indicates the start of an in-memory switch localAccess.instruction! {
// dependent value. Typically, it has no uses. If it does have uses, then they are visited earlier by case let md as MarkDependenceInst:
// LocalVariableAccessWalker to record any other local accesses. if md.type.isAddress {
return .continueWalk 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: case .store:
let si = localAccess.operand!.instruction as! StoringInstruction let si = localAccess.operand!.instruction as! StoringInstruction
assert(si.sourceOperand == initialValue, "the only reachable store should be the current assignment") assert(si.sourceOperand == initialValue, "the only reachable store should be the current assignment")
@@ -948,13 +966,13 @@ extension LifetimeDependenceDefUseWalker {
// because a mark_dependence [nonescaping] represents the // because a mark_dependence [nonescaping] represents the
// dependence. // dependence.
if let result = apply.singleDirectResult, !result.isEscapable { if let result = apply.singleDirectResult, !result.isEscapable {
if dependentUse(of: operand, into: result) == .abortWalk { if dependentUse(of: operand, dependentValue: result) == .abortWalk {
return .abortWalk return .abortWalk
} }
} }
for resultAddr in apply.indirectResultOperands for resultAddr in apply.indirectResultOperands
where !resultAddr.value.isEscapable { where !resultAddr.value.isEscapable {
if visitStoredUses(of: operand, into: resultAddr.value) == .abortWalk { if dependentUse(of: operand, dependentAddress: resultAddr.value) == .abortWalk {
return .abortWalk return .abortWalk
} }
} }

View File

@@ -36,6 +36,29 @@ private func log(_ message: @autoclosure () -> String) {
// Local variables are accessed in one of these ways. // 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. // 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 { struct LocalVariableAccess: CustomStringConvertible {
enum Kind { enum Kind {
case incomingArgument // @in, @inout, @inout_aliasable case incomingArgument // @in, @inout, @inout_aliasable
@@ -43,7 +66,8 @@ struct LocalVariableAccess: CustomStringConvertible {
case inoutYield // indirect yield from this accessor case inoutYield // indirect yield from this accessor
case beginAccess // Reading or reassigning a 'var' case beginAccess // Reading or reassigning a 'var'
case load // Reading a 'let'. Returning 'var' from an initializer. 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 store // 'var' initialization and destruction
case apply // indirect arguments case apply // indirect arguments
case escape // alloc_box captures case escape // alloc_box captures
@@ -77,7 +101,7 @@ struct LocalVariableAccess: CustomStringConvertible {
case .`init`, .modify: case .`init`, .modify:
return true return true
} }
case .load, .dependence: case .load, .dependenceSource, .dependenceDest:
return false return false
case .incomingArgument, .outgoingArgument, .store, .inoutYield: case .incomingArgument, .outgoingArgument, .store, .inoutYield:
return true return true
@@ -116,8 +140,10 @@ struct LocalVariableAccess: CustomStringConvertible {
str += "beginAccess" str += "beginAccess"
case .load: case .load:
str += "load" str += "load"
case .dependence: case .dependenceSource:
str += "dependence" str += "dependenceSource"
case .dependenceDest:
str += "dependenceDest"
case .store: case .store:
str += "store" str += "store"
case .apply: case .apply:
@@ -152,7 +178,7 @@ class LocalVariableAccessInfo: CustomStringConvertible {
case .`init`, .modify: case .`init`, .modify:
break // lazily compute full assignment break // lazily compute full assignment
} }
case .load, .dependence: case .load, .dependenceSource, .dependenceDest:
self._isFullyAssigned = false self._isFullyAssigned = false
case .store: case .store:
if let store = localAccess.instruction as? StoringInstruction { if let store = localAccess.instruction as? StoringInstruction {
@@ -365,7 +391,7 @@ extension LocalVariableAccessWalker : ForwardingDefUseWalker {
break break
case let markDep as MarkDependenceInst: case let markDep as MarkDependenceInst:
assert(markDep.baseOperand == operand) assert(markDep.baseOperand == operand)
visit(LocalVariableAccess(.dependence, operand)) visit(LocalVariableAccess(.dependenceSource, operand))
default: default:
visit(LocalVariableAccess(.escape, operand)) visit(LocalVariableAccess(.escape, operand))
} }
@@ -381,10 +407,6 @@ extension LocalVariableAccessWalker : ForwardingDefUseWalker {
extension LocalVariableAccessWalker: AddressUseVisitor { extension LocalVariableAccessWalker: AddressUseVisitor {
private mutating func walkDownAddressUses(address: Value) -> WalkResult { private mutating func walkDownAddressUses(address: Value) -> WalkResult {
for operand in address.uses.ignoreTypeDependence { 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 { if classifyAddress(operand: operand) == .abortWalk {
return .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 // temporaries do not have access scopes, so we need to walk down any projection that may be used to initialize the
// temporary. // temporary.
mutating func projectedAddressUse(of operand: Operand, into value: Value) -> WalkResult { 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) return walkDownAddressUses(address: value)
} }
@@ -432,6 +461,9 @@ extension LocalVariableAccessWalker: AddressUseVisitor {
is PackElementSetInst: is PackElementSetInst:
// Handle instructions that initialize both temporaries and local variables. // Handle instructions that initialize both temporaries and local variables.
visit(LocalVariableAccess(.store, operand)) visit(LocalVariableAccess(.store, operand))
case let md as MarkDependenceAddrInst:
assert(operand == md.addressOperand)
visit(LocalVariableAccess(.dependenceDest, operand))
case is DeallocStackInst: case is DeallocStackInst:
break break
default: default:
@@ -473,12 +505,12 @@ extension LocalVariableAccessWalker: AddressUseVisitor {
return .continueWalk 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)) visit(LocalVariableAccess(.load, operand))
return .continueWalk 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)) visit(LocalVariableAccess(.load, operand))
return .continueWalk return .continueWalk
} }

View File

@@ -208,7 +208,8 @@ struct InteriorLivenessResult: CustomDebugStringConvertible {
/// - forwardingUse(of:isInnerlifetime:) /// - forwardingUse(of:isInnerlifetime:)
/// - interiorPointerUse(of:into:) /// - interiorPointerUse(of:into:)
/// - pointerEscapingUse(of:) /// - pointerEscapingUse(of:)
/// - dependentUse(of:into:) /// - dependentUse(of:dependentValue:)
/// - dependentUse(of:dependentAddress:)
/// - borrowingUse(of:by:) /// - borrowingUse(of:by:)
/// ///
/// This only visits the first level of uses. The implementation may transitively visit forwarded, borrowed, dependent, /// 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 /// there are no explicit scope-ending operations. Instead
/// BorrowingInstruction.scopeEndingOperands will return the final /// BorrowingInstruction.scopeEndingOperands will return the final
/// consumes in the dependent value's forwarding chain. /// 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. /// A use that is scoped to an inner borrow scope.
mutating func borrowingUse(of operand: Operand, by borrowInst: BorrowingInstruction) -> WalkResult mutating func borrowingUse(of operand: Operand, by borrowInst: BorrowingInstruction) -> WalkResult
@@ -384,6 +391,9 @@ extension OwnershipUseVisitor {
return forwardingUse(of: operand, isInnerLifetime: false) return forwardingUse(of: operand, isInnerLifetime: false)
case .pointerEscape: case .pointerEscape:
if let mdai = operand.instruction as? MarkDependenceAddrInst, operand == mdai.baseOperand {
return dependentUse(of: operand, dependentAddress: mdai.address)
}
return pointerEscapingUse(of: operand) return pointerEscapingUse(of: operand)
case .instantaneousUse, .forwardingUnowned, .unownedInstantaneousUse, .bitwiseEscape: case .instantaneousUse, .forwardingUnowned, .unownedInstantaneousUse, .bitwiseEscape:
@@ -413,6 +423,9 @@ extension OwnershipUseVisitor {
if operand.instruction is ProjectBoxInst { if operand.instruction is ProjectBoxInst {
return visitInteriorPointerUse(of: operand) 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) return pointerEscapingUse(of: operand)
case .instantaneousUse, .forwardingUnowned, .unownedInstantaneousUse, .bitwiseEscape, .endBorrow, .reborrow: case .instantaneousUse, .forwardingUnowned, .unownedInstantaneousUse, .bitwiseEscape, .endBorrow, .reborrow:
@@ -436,12 +449,14 @@ extension OwnershipUseVisitor {
switch operand.instruction { switch operand.instruction {
case let pai as PartialApplyInst: case let pai as PartialApplyInst:
assert(!pai.mayEscape) assert(!pai.mayEscape)
return dependentUse(of: operand, into: pai) return dependentUse(of: operand, dependentValue: pai)
case let mdi as MarkDependenceInst: 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) 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: case let bfi as BorrowedFromInst where !bfi.borrowedPhi.isReborrow:
return dependentUse(of: operand, into: bfi) return dependentUse(of: operand, dependentValue: bfi)
default: default:
return borrowingUse(of: operand, return borrowingUse(of: operand,
by: BorrowingInstruction(operand.instruction)!) by: BorrowingInstruction(operand.instruction)!)
@@ -586,7 +601,7 @@ extension InteriorUseWalker: OwnershipUseVisitor {
} }
// Handle partial_apply [on_stack] and mark_dependence [nonescaping]. // 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. // OSSA lifetime ignores trivial types.
if value.type.isTrivial(in: function) { if value.type.isTrivial(in: function) {
return .continueWalk return .continueWalk
@@ -602,6 +617,12 @@ extension InteriorUseWalker: OwnershipUseVisitor {
return walkDownUses(of: value) 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 { mutating func pointerEscapingUse(of operand: Operand) -> WalkResult {
if useVisitor(operand) == .abortWalk { if useVisitor(operand) == .abortWalk {
return .abortWalk return .abortWalk
@@ -699,12 +720,12 @@ extension InteriorUseWalker: AddressUseVisitor {
return .continueWalk return .continueWalk
} }
mutating func loadedAddressUse(of operand: Operand, into value: Value) mutating func loadedAddressUse(of operand: Operand, intoValue value: Value)
-> WalkResult { -> WalkResult {
return .continueWalk return .continueWalk
} }
mutating func loadedAddressUse(of operand: Operand, into address: Operand) mutating func loadedAddressUse(of operand: Operand, intoAddress address: Operand)
-> WalkResult { -> WalkResult {
return .continueWalk return .continueWalk
} }