Files
swift-mirror/SwiftCompilerSources/Sources/Optimizer/FunctionPasses/LifetimeDependenceScopeFixup.swift
2024-08-08 22:22:39 -07:00

262 lines
10 KiB
Swift

//===--- LifetimeDependenceScopeFixup.swift ----------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2023 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
//
//===---------------------------------------------------------------------===//
// For an apply that returns a lifetime dependent value,
// LifetimeDependenceInsertion inserts mark_dependence [unresolved] on parent
// value's access scope. LifetimeDependenceScopeFixup then extends the access
// scope to cover all uses of the dependent value.
// This pass must run after LifetimeDependenceInsertion and before
// LifetimeDependenceDiagnostics.
import SIL
private let verbose = false
private func log(prefix: Bool = true, _ message: @autoclosure () -> String) {
if verbose {
debugLog(prefix: prefix, message())
}
}
let lifetimeDependenceScopeFixupPass = FunctionPass(
name: "lifetime-dependence-scope-fixup")
{ (function: Function, context: FunctionPassContext) in
#if os(Windows)
if !context.options.hasFeature(.NonescapableTypes) {
return
}
#endif
log(prefix: false, "\n--- Scope fixup for lifetime dependence in \(function.name)")
let localReachabilityCache = LocalVariableReachabilityCache()
for instruction in function.instructions {
guard let markDep = instruction as? MarkDependenceInst else {
continue
}
guard let lifetimeDep = LifetimeDependence(markDep, context) else {
continue
}
if let arg = extendAccessScopes(dependence: lifetimeDep, localReachabilityCache, context) {
markDep.baseOperand.set(to: arg, context)
}
}
}
/// Extend all access scopes that enclose `dependence`. If dependence is on an access scope in the caller, then return
/// the function argument that represents the dependence scope.
private func extendAccessScopes(dependence: LifetimeDependence,
_ localReachabilityCache: LocalVariableReachabilityCache,
_ context: FunctionPassContext) -> FunctionArgument? {
log("Scope fixup for lifetime dependent instructions: \(dependence)")
guard case .access(let beginAccess) = dependence.scope else {
return nil
}
let function = beginAccess.parentFunction
// Get the range accessBase lifetime. The accessRange cannot exceed this without producing invalid SIL.
guard var ownershipRange = AddressOwnershipLiveRange.compute(for: beginAccess.address, at: beginAccess,
localReachabilityCache, context) else {
return nil
}
defer { ownershipRange.deinitialize() }
var accessRange = InstructionRange(begin: beginAccess, context)
defer {accessRange.deinitialize()}
var walker = LifetimeDependenceScopeFixupWalker(function, localReachabilityCache, context) {
// Do not extend the accessRange past the ownershipRange.
let dependentInst = $0.instruction
if ownershipRange.coversUse(dependentInst) {
accessRange.insert(dependentInst)
}
return .continueWalk
}
defer {walker.deinitialize()}
_ = walker.walkDown(root: dependence.dependentValue)
log("Scope fixup for dependent uses:\n\(accessRange)")
// Lifetime dependenent uses may not be dominated by the access. The dependent value may be used by a phi or stored
// into a memory location. The access may be conditional relative to such uses. If any use was not dominated, then
// `accessRange` will include the function entry.
let firstInst = function.entryBlock.instructions.first!
if firstInst != beginAccess, accessRange.contains(firstInst) {
return nil
}
if let arg = extendAccessScope(beginAccess: beginAccess, range: &accessRange, context) {
// If the dependent value is returned, then return the FunctionArgument that it depends on.
assert(walker.dependsOnCaller)
return arg
}
return nil
}
/// Extend this access scope to cover the dependent uses. Recursively extend outer accesses to maintain nesting.
///
/// Note that we cannot simply rewrite the `mark_dependence` to depend on an outer access scope. For 'read' access, this
/// could let us avoid extending the inner scope, but that would not accomplish anything useful because inner 'read's
/// can always be extended up to the extent of their outer 'read' (ignoring the special case when the dependence is on a
/// caller scope, which is handled separately). A nested 'read' access can never interfere with another access in the
/// same outer 'read', because it is impossible to nest a 'modify' access within a 'read'. For 'modify' accesses,
/// however, the inner scope must be extended for correctness. A 'modify' access can interfere with other 'modify'
/// access in the same scope. We rely on exclusivity diagnostics to report these interferences. For example:
///
/// sil @foo : $(@inout C) -> () {
/// bb0(%0 : $*C):
/// %a1 = begin_access [modify] %0
/// %d = apply @getDependent(%a1)
/// mark_dependence [unresolved] %d on %a1
/// end_access %a1
/// %a2 = begin_access [modify] %0
/// ...
/// end_access %a2
/// apply @useDependent(%d) // exclusivity violation
/// return
/// }
///
/// The call to `@useDependent` is an exclusivity violation because it uses a value that depends on a 'modify'
/// access. This scope fixup pass must extend '%a1' to cover the `@useDependent` but must not extend the base of the
/// `mark_dependence` to the outer access `%0`. This ensures that exclusivity diagnostics correctly reports the
/// violation, and that subsequent optimizations do not shrink the inner access `%a1`.
private func extendAccessScope(beginAccess: BeginAccessInst, range: inout InstructionRange,
_ context: FunctionPassContext) -> FunctionArgument? {
var endAccesses = [Instruction]()
// Collect the original end_access instructions and extend the range to to cover them. The resulting access scope must
// cover the original scope because it may protect other memory operations.
var requiresExtension = false
for end in beginAccess.endInstructions {
endAccesses.append(end)
if range.contains(end) {
// If any end_access is inside the new range, then all end_accesses must be rewritten.
requiresExtension = true
} else {
range.insert(end)
}
}
if !requiresExtension {
return nil
}
// Create new end_access at the end of extended uses
var dependsOnCaller = false
for end in range.ends {
let location = end.location.autoGenerated
if end is ReturnInst {
dependsOnCaller = true
let endAccess = Builder(before: end, location: location, context).createEndAccess(beginAccess: beginAccess)
range.insert(endAccess)
continue
}
Builder.insert(after: end, location: location, context) {
let endAccess = $0.createEndAccess(beginAccess: beginAccess)
// This scope should be nested in any outer scopes.
range.insert(endAccess)
}
}
for exitInst in range.exits {
let location = exitInst.location.autoGenerated
let endAccess = Builder(before: exitInst, location: location, context).createEndAccess(beginAccess: beginAccess)
range.insert(endAccess)
}
// Delete original end_access instructions
for endAccess in endAccesses {
context.erase(instruction: endAccess)
}
// TODO: Add SIL support for lifetime dependence and write unit test for nested access scopes
switch beginAccess.address.enclosingAccessScope {
case let .scope(enclosingBeginAccess):
return extendAccessScope(beginAccess: enclosingBeginAccess, range: &range, context)
case let .base(accessBase):
if case let .argument(arg) = accessBase, dependsOnCaller {
return arg
}
return nil
}
}
private struct LifetimeDependenceScopeFixupWalker : LifetimeDependenceDefUseWalker {
let function: Function
let context: Context
let visitor: (Operand) -> WalkResult
let localReachabilityCache: LocalVariableReachabilityCache
var visitedValues: ValueSet
/// Set to true if the dependence is returned from the current function.
var dependsOnCaller = false
init(_ function: Function, _ localReachabilityCache: LocalVariableReachabilityCache, _ context: Context,
visitor: @escaping (Operand) -> WalkResult) {
self.function = function
self.context = context
self.visitor = visitor
self.localReachabilityCache = localReachabilityCache
self.visitedValues = ValueSet(context)
}
mutating func deinitialize() {
visitedValues.deinitialize()
}
mutating func needWalk(for value: Value) -> Bool {
visitedValues.insert(value)
}
mutating func deadValue(_ value: Value, using operand: Operand?)
-> WalkResult {
if let operand {
return visitor(operand)
}
return .continueWalk
}
mutating func leafUse(of operand: Operand) -> WalkResult {
return visitor(operand)
}
mutating func escapingDependence(on operand: Operand) -> WalkResult {
log(">>> Escaping dependence: \(operand)")
_ = visitor(operand)
// Make a best-effort attempt to extend the access scope regardless of escapes. It is possible that some mandatory
// pass between scope fixup and diagnostics will make it possible for the LifetimeDependenceDefUseWalker to analyze
// this use.
return .continueWalk
}
mutating func inoutDependence(argument: FunctionArgument, on operand: Operand) -> WalkResult {
dependsOnCaller = true
return visitor(operand)
}
mutating func returnedDependence(result operand: Operand) -> WalkResult {
dependsOnCaller = true
return visitor(operand)
}
mutating func returnedDependence(address: FunctionArgument,
on operand: Operand) -> WalkResult {
dependsOnCaller = true
return visitor(operand)
}
mutating func yieldedDependence(result: Operand) -> WalkResult {
return .continueWalk
}
mutating func storeToYieldDependence(address: Value, of operand: Operand) -> WalkResult {
return .continueWalk
}
}