//===--- ComputeSideEffects.swift ------------------------------------------==// // // This source file is part of the Swift.org open source project // // Copyright (c) 2014 - 2022 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 // //===----------------------------------------------------------------------===// import SIL /// Computes function side effects. /// /// Computes the `SideEffects` for a function, which consists of argument- and global /// effects. /// For example, if a function writes to the first argument and reads from a global variable, /// the side effects /// ``` /// [%0: write v**] /// [global: read] /// ``` /// are computed. /// let computeSideEffects = FunctionPass(name: "compute-side-effects") { (function: Function, context: FunctionPassContext) in if function.isDefinedExternally { // We cannot assume anything about function, which are defined in another module, // even if the serialized SIL of its body is available in the current module. // If the other module was compiled with library evolution, the implementation // (and it's effects) might change in future versions of the other module/library. // // TODO: only do this for functions which are de-serialized from library-evolution modules. return } var collectedEffects = CollectedEffects(function: function, context) // Collect effects from all instructions. for block in function.blocks { for inst in block.instructions { collectedEffects.addInstructionEffects(inst) } } // If an argument has unknown uses, we must add all previously collected // global effects to the argument, because we don't know to which "global" // side-effect instruction the argument might have escaped. for argument in function.arguments { collectedEffects.addEffectsForEscapingArgument(argument: argument) collectedEffects.addEffectsForConsumingArgument(argument: argument) } let globalEffects: SideEffects.GlobalEffects do { let computed = collectedEffects.globalEffects // Combine computed global effects with effects defined by the function's effect attribute, if it has one. // The defined and computed global effects of a function with an effect attribute should be treated as // worst case global effects of the function. // This means a global effect should only occur iff it is computed AND defined to occur. let defined = function.definedGlobalEffects globalEffects = SideEffects.GlobalEffects( memory: SideEffects.Memory(read: defined.memory.read && computed.memory.read, write: defined.memory.write && computed.memory.write), ownership: SideEffects.Ownership(copy: defined.ownership.copy && computed.ownership.copy, destroy: defined.ownership.destroy && computed.ownership.destroy), allocates: defined.allocates && computed.allocates, isDeinitBarrier: defined.isDeinitBarrier && computed.isDeinitBarrier ) } // Obtain the argument effects of the function. var argumentEffects = collectedEffects.argumentEffects // `[readnone]` and `[readonly]` functions can still access the value fields // of their indirect arguments, permitting v** read and write effects. If // additional read or write effects are computed, we can replace. switch function.effectAttribute { case .readNone: for i in (0..(ofFunctions callees: FunctionArray?, withArguments arguments: Arguments) where Arguments.Element == (calleeArgumentIndex: Int, callerArgument: Value) { // The argument summary for @in_cxx is insufficient in OSSA because the function body does not contain the // destroy. But the call is still effectively a release from the caller's perspective. guard let callees = callees else { // We don't know which function(s) are called. globalEffects = .worstEffects for (_, argument) in arguments { addEffects(.worstEffects, to: argument) } return } for callee in callees { if let sideEffects = callee.effects.sideEffects { globalEffects.merge(with: sideEffects.global) } else { // The callee doesn't have any computed effects. At least we can do better // if it has any defined effect attribute (like e.g. `[readnone]`). globalEffects.merge(with: callee.definedGlobalEffects) } } for (calleeArgIdx, argument) in arguments { for callee in callees { if let sideEffects = callee.effects.sideEffects { let calleeEffect = sideEffects.getArgumentEffects(for: calleeArgIdx) // Merge the callee effects into this function's effects if let calleePath = calleeEffect.read { addEffects(.read, to: argument, fromInitialPath: calleePath) } if let calleePath = calleeEffect.write { addEffects(.write, to: argument, fromInitialPath: calleePath) } if let calleePath = calleeEffect.copy { addEffects(.copy, to: argument, fromInitialPath: calleePath) } if let calleePath = calleeEffect.destroy { addEffects(.destroy, to: argument, fromInitialPath: calleePath) } } else { let convention = callee.argumentConventions[calleeArgIdx] let wholeArgument = argument.at(defaultPath(for: argument)) let calleeEffects = callee.getSideEffects(forArgument: wholeArgument, atIndex: calleeArgIdx, withConvention: convention) addEffects(calleeEffects.restrictedTo(argument: wholeArgument, withConvention: convention), to: argument) } } } } /// Adds effects to a specific value. /// /// If the value comes from an argument (or multiple arguments), then the effects are added /// to the corresponding `argumentEffects`. Otherwise they are added to the `global` effects. private mutating func addEffects(_ effects: SideEffects.GlobalEffects, to value: Value) { addEffects(effects, to: value, fromInitialPath: defaultPath(for: value)) } private mutating func addEffects(_ effects: SideEffects.GlobalEffects, to value: Value, fromInitialPath: SmallProjectionPath) { /// Collects the (non-address) roots of a value. struct GetRootsWalker : ValueUseDefWalker { // All function-argument roots of the value, including the path from the arguments to the values. var roots: Stack<(FunctionArgument, SmallProjectionPath)> // True, if the value has at least one non function-argument root. var nonArgumentRootsFound = false var walkUpCache = WalkerCache() init(_ context: FunctionPassContext) { self.roots = Stack(context) } mutating func rootDef(value: Value, path: SmallProjectionPath) -> WalkResult { if let arg = value as? FunctionArgument { roots.push((arg, path)) } else if value is Allocation { // Ignore effects on local allocations - even if those allocations escape. // Effects on local (potentially escaping) allocations cannot be relevant in the caller. return .continueWalk } else { nonArgumentRootsFound = true } return .continueWalk } } var findRoots = GetRootsWalker(context) if value.type.isAddress { let accessPath = value.getAccessPath(fromInitialPath: fromInitialPath) switch accessPath.base { case .stack: // We don't care about read and writes from/to stack locations (because they are // not observable from outside the function). But we need to consider copies and destroys. // For example, an argument could be "moved" to a stack location, which is eventually destroyed. // In this case it's in fact the original argument value which is destroyed. globalEffects.ownership.merge(with: effects.ownership) return case .argument(let arg): // The `value` is an address projection of an indirect argument. argumentEffects[arg.index].merge(effects, with: accessPath.projectionPath) return default: // Handle address `value`s which are are field projections from class references in direct arguments. if !findRoots.visitAccessStorageRoots(of: accessPath) { findRoots.nonArgumentRootsFound = true } } } else { _ = findRoots.walkUp(value: value, path: fromInitialPath) } // Because of phi-arguments, a single (non-address) `value` can come from multiple arguments. while let (arg, path) = findRoots.roots.pop() { argumentEffects[arg.index].merge(effects, with: path) } if findRoots.nonArgumentRootsFound { // The `value` comes from some non-argument root, e.g. a load instruction. globalEffects.merge(with: effects) } } } private func defaultPath(for value: Value) -> SmallProjectionPath { if value.type.isAddress { return SmallProjectionPath(.anyValueFields) } if value.type.isClass { return SmallProjectionPath(.anyValueFields).push(.anyClassField) } return SmallProjectionPath(.anyValueFields).push(.anyClassField).push(.anyValueFields) } /// Checks if an argument escapes to some unknown user. private struct ArgumentEscapingWalker : ValueDefUseWalker, AddressDefUseWalker { var walkDownCache = WalkerCache() private let calleeAnalysis: CalleeAnalysis /// True if the argument escapes to a load which (potentially) "takes" the memory location. private(set) var foundTakingLoad = false /// True, if the argument escapes to a closure context which might be destroyed when called. private(set) var foundConsumingPartialApply = false init(_ context: FunctionPassContext) { self.calleeAnalysis = context.calleeAnalysis } mutating func hasUnknownUses(argument: FunctionArgument) -> Bool { if argument.type.isAddress { return walkDownUses(ofAddress: argument, path: UnusedWalkingPath()) == .abortWalk } else if argument.hasTrivialNonPointerType { return false } else { return walkDownUses(ofValue: argument, path: UnusedWalkingPath()) == .abortWalk } } mutating func leafUse(value: Operand, path: UnusedWalkingPath) -> WalkResult { switch value.instruction { case is RefTailAddrInst, is RefElementAddrInst, is ProjectBoxInst: return walkDownUses(ofAddress: value.instruction as! SingleValueInstruction, path: path) // Warning: all instruction listed here, must also be handled in `CollectedEffects.addInstructionEffects` case is CopyValueInst, is RetainValueInst, is StrongRetainInst, is DestroyValueInst, is ReleaseValueInst, is StrongReleaseInst, is DebugValueInst, is UnconditionalCheckedCastInst, is ReturnInst: return .continueWalk case let apply as ApplySite: if let pa = apply as? PartialApplyInst, !pa.isOnStack { foundConsumingPartialApply = true } // `CollectedEffects.handleApply` only handles argument operands of an apply, but not the callee operand. if let calleeArgIdx = apply.calleeArgumentIndex(of: value), let callees = calleeAnalysis.getCallees(callee: apply.callee) { // If an argument escapes in a called function, we don't know anything about the argument's side effects. // For example, it could escape to the return value and effects might occur in the caller. for callee in callees { if callee.effects.escapeEffects.canEscape(argumentIndex: calleeArgIdx, path: SmallProjectionPath.init(.anyValueFields)) { return .abortWalk } } return .continueWalk } return .abortWalk default: return .abortWalk } } mutating func leafUse(address: Operand, path: UnusedWalkingPath) -> WalkResult { let inst = address.instruction let function = inst.parentFunction switch inst { case let copy as CopyAddrInst: if address == copy.sourceOperand && !address.value.hasTrivialType && (!function.hasOwnership || copy.isTakeOfSource) { foundTakingLoad = true } return .continueWalk case let load as LoadInst: if !address.value.hasTrivialType && // In non-ossa SIL we don't know if a load is taking. (!function.hasOwnership || load.loadOwnership == .take) { foundTakingLoad = true } return .continueWalk case is LoadWeakInst, is LoadUnownedInst, is LoadBorrowInst: if !function.hasOwnership && !address.value.hasTrivialType { foundTakingLoad = true } return .continueWalk // Warning: all instruction listed here, must also be handled in `CollectedEffects.addInstructionEffects` case is StoreInst, is StoreWeakInst, is StoreUnownedInst, is ApplySite, is DestroyAddrInst, is DebugValueInst: return .continueWalk default: return .abortWalk } } } private extension SideEffects.GlobalEffects { static var read: Self { Self(memory: SideEffects.Memory(read: true)) } static var write: Self { Self(memory: SideEffects.Memory(write: true)) } static var copy: Self { Self(ownership: SideEffects.Ownership(copy: true)) } static var destroy: Self { Self(ownership: SideEffects.Ownership(destroy: true)) } } private extension SideEffects.ArgumentEffects { mutating func merge(_ effects: SideEffects.GlobalEffects, with path: SmallProjectionPath) { if effects.memory.read { read.merge(with: path) } if effects.memory.write { write.merge(with: path) } if effects.ownership.copy { copy.merge(with: path) } if effects.ownership.destroy { destroy.merge(with: path) } } } private extension PartialApplyInst { func canBeAppliedInFunction(_ context: FunctionPassContext) -> Bool { struct EscapesToApply : EscapeVisitor { func visitUse(operand: Operand, path: EscapePath) -> UseResult { switch operand.instruction { case is FullApplySite: // Any escape to apply - regardless if it's an argument or the callee operand - might cause // the closure to be called. return .abort case is ReturnInst: return .ignore default: return .continueWalk } } var followTrivialTypes: Bool { true } } return self.isEscaping(using: EscapesToApply(), initialWalkingDirection: .down, context) } }