1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247
|
//===--- 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 {
print((prefix ? "### " : "") + message())
}
}
let lifetimeDependenceScopeFixupPass = FunctionPass(
name: "lifetime-dependence-scope-fixup")
{ (function: Function, context: FunctionPassContext) in
if !context.options.hasFeature(.NonescapableTypes) {
return
}
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'
/// accesss 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 endAcceses = [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 {
endAcceses.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
}
assert(!range.ends.isEmpty)
// 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)
}
}
// Delete original end_access instructions
for endAccess in endAcceses {
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 returnedDependence(result operand: Operand) -> WalkResult {
dependsOnCaller = true
return visitor(operand)
}
mutating func returnedDependence(address: FunctionArgument,
using operand: Operand) -> WalkResult {
dependsOnCaller = true
return visitor(operand)
}
mutating func yieldedDependence(result: Operand) -> WalkResult {
return .continueWalk
}
}
|