Files
swift-mirror/SwiftCompilerSources/Sources/Optimizer/FunctionPasses/InitializeStaticGlobals.swift

632 lines
23 KiB
Swift

//===--- InitializeStaticGlobals.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
//
//===----------------------------------------------------------------------===//
import AST
import SIL
/// Converts a lazily initialized global to a statically initialized global variable.
///
/// When this pass runs on a global initializer `[global_init_once_fn]` it tries to
/// create a static initializer for the initialized global.
///
/// ```
/// sil [global_init_once_fn] @globalinit {
/// alloc_global @the_global
/// %a = global_addr @the_global
/// %i = some_const_initializer_insts
/// store %i to %a
/// }
/// ```
/// The pass creates a static initializer for the global:
/// ```
/// sil_global @the_global = {
/// %initval = some_const_initializer_insts
/// }
/// ```
/// and removes the allocation and store instructions from the initializer function:
/// ```
/// sil [global_init_once_fn] @globalinit {
/// %a = global_addr @the_global
/// %i = some_const_initializer_insts
/// }
/// ```
/// The initializer then becomes a side-effect free function which let's the builtin-
/// simplification remove the `builtin "once"` which calls the initializer.
///
let initializeStaticGlobalsPass = FunctionPass(name: "initialize-static-globals") {
(function: Function, context: FunctionPassContext) in
guard // In case of a preceding error, there is no guarantee that the SIL is valid.
!context.hadError,
// Is `function` a global init function?
let global = function.initializedGlobal,
// Even if the init value is a constant, the initializer can have side effects, e.g.
// let g = { print("hello"); return 27 }()
!function.hasSideEffects(besideStoringTo: global),
// Try to get a constant global init value.
let initValue = GlobalInitValue(of: function, context)
else {
return
}
initValue.materialize(into: global, from: function, context)
// The initial value can contain a `begin_access` if it references another global variable by address, e.g.
// var p = Point(x: 10, y: 20)
// let o = UnsafePointer(&p)
//
global.stripAccessInstructionFromInitializer(context)
function.removeAllInitializationCode(for: global, context)
}
/// A tree, which represents a constant init value of a global variable.
private indirect enum GlobalInitValue {
case undefined
// A value which is a SIL "constant", e.g. a literal instruction, a struct/tuple of literals, etc.
case constant(Value)
// A struct, tuple or vector, which is _not_ a SIL "constant".
// For example, a struct or vector which is initialized by storing its elements.
case aggregate([GlobalInitValue])
// An enum case without a payload of an address-only enum.
case enumCase(caseIndex: Int)
// An enum case which is not a SIL "constant", e.g. because it's address-only
case enumCaseWithPayload(caseIndex: Int, payload: GlobalInitValue)
init?(of globalInitFunction: Function, _ context: FunctionPassContext) {
self = .undefined
var global: GlobalVariable? = nil
// The init values for temporary stack locations. This is needed if e.g. a vector is initialized in a
// stack location and the loaded from it and stored in the global.
var stackValues = Dictionary<AllocStackInst, GlobalInitValue>()
for inst in globalInitFunction.instructions {
switch inst {
case let allocGlobal as AllocGlobalInst:
guard global == nil, allocGlobal.global.canBeInitializedStatically else {
return nil
}
global = allocGlobal.global
case let ga as GlobalAddrInst where ga.global == global:
// SILGen only creates a single global_addr in the init function. So no need to support multiple ones.
guard case .undefined = self,
let selfInitVal = GlobalInitValue(of: ga, context)
else {
return nil
}
self = selfInitVal
case let allocStack as AllocStackInst:
guard let stackInitVal = GlobalInitValue(of: allocStack, context) else {
return nil
}
stackValues[allocStack] = stackInitVal
default:
break
}
}
// At this point some `.constant` elements can be (or contain) loads from stack locations.
// Replace those elements with the init value of the corresponding stack location.
resolveLoads(from: &stackValues, context)
if !isValid(context) {
return nil
}
}
private init?(of address: Value, _ context: FunctionPassContext) {
var builder = InitValueBuilder(originalAddress: address)
if builder.walkDownUses(ofAddress: address, path: UnusedWalkingPath()) == .abortWalk {
return nil
}
self = builder.initValue
}
enum InitValue {
// The common case
case value(Value)
// For payload-less cases of address-only enums. Such cases are initialized purely with an `inject_enum_addr`,
// and we don't have a `Value` which represents the resulting enum(-case).
case enumCaseWithoutPayload(InjectEnumAddrInst)
var parentFunction: Function {
switch self {
case .value(let value): return value.parentFunction
case .enumCaseWithoutPayload(let iea): return iea.parentFunction
}
}
}
// Sets an element in the constant tree.
// Returns true if this was successful. One reason for being not successful is if a certain
// element is set twice, i.e. does not have a single defined value.
mutating func setElement(to value: InitValue, at path: SmallProjectionPath, type: Type) -> Bool {
let (kind, index, subPath) = path.pop()
switch kind {
case .root:
guard case .undefined = self else {
// The element was set twice.
return false
}
switch value {
case .value(let value):
self = .constant(value)
case .enumCaseWithoutPayload:
fatalError("should have been handled in the .enumCase of the SmallProjectionPath below")
}
return true
case .enumCase:
return setEnumCase(to: value, at: subPath, index: index, type: type)
case .structField:
guard let structFields = type.getNominalFields(in: value.parentFunction) else {
return false
}
return setField(to: value, at: subPath, index: index, type: structFields[index], numFields: structFields.count)
case .tupleField:
let tupleElements = type.tupleElements
return setField(to: value, at: subPath, index: index, type: tupleElements[index], numFields: tupleElements.count)
case .vectorBase:
let elementType = type.builtinFixedArrayElementType(in: value.parentFunction)
guard let vectorSize = type.builtinFixedArraySizeType.valueOfInteger else {
return false
}
let (indexKind, vectorIndex, vectorSubPath) = subPath.pop()
switch indexKind {
case .indexedElement:
return setField(to: value, at: vectorSubPath, index: vectorIndex, type: elementType, numFields: vectorSize)
case .anyIndexedElement:
// A non-constant index means: all elements are initialized with the same value.
// (that's what we checked in `storesToAllVectorElements`)
for i in 0..<vectorSize {
if !setField(to: value, at: vectorSubPath, index: i, type: elementType, numFields: vectorSize) {
return false
}
}
return true
default:
// A missing index in the path means: the first element
return setField(to: value, at: subPath, index: 0, type: elementType, numFields: vectorSize)
}
default:
return false
}
}
private mutating func setField(
to value: InitValue, at path: SmallProjectionPath,
index: Int, type: Type, numFields: Int
) -> Bool {
if case .undefined = self {
// Initialize the aggregate array if not done, yet.
self = .aggregate(Array(repeating: GlobalInitValue.undefined, count: numFields))
}
if case .aggregate(let fields) = self {
var newFields = fields
self = .undefined // avoid copy-on-write
if !newFields[index].setElement(to: value, at: path, type: type) {
return false
}
self = .aggregate(newFields)
return true
}
return false
}
private mutating func setEnumCase(to value: InitValue, at path: SmallProjectionPath, index: Int, type: Type) -> Bool {
if path.isEmpty, case .enumCaseWithoutPayload(let iea) = value {
guard case .undefined = self else {
// The enum was set twice.
return false
}
assert(index == iea.caseIndex)
self = .enumCase(caseIndex: index)
} else {
guard let payloadType = type.getEnumCases(in: value.parentFunction)!.getPayloadType(ofCaseIndex: index) else {
return false
}
switch self {
case .undefined:
// It's the first time we set the payload or a sub-field of it.
var payload = GlobalInitValue.undefined
if !payload.setElement(to: value, at: path, type: payloadType) {
return false
}
self = .enumCaseWithPayload(caseIndex: index, payload: payload)
case .enumCaseWithPayload(let existingIndex, var payload) where index == existingIndex:
// Some sub-field of the enum-payload was already set.
self = .undefined // avoid copy-on-write
if !payload.setElement(to: value, at: path, type: payloadType) {
return false
}
self = .enumCaseWithPayload(caseIndex: index, payload: payload)
default:
return false
}
}
return true
}
/// Creates SIL for this global init value in the initializer of the `global`.
func materialize(into global: GlobalVariable, from function: Function, _ context: FunctionPassContext) {
var cloner = Cloner(cloneToGlobal: global, context)
defer { cloner.deinitialize() }
let builder = Builder(staticInitializerOf: global, context)
_ = materializeRecursively(type: global.type, &cloner, builder, function)
}
private func materializeRecursively(
type: Type,
_ cloner: inout Cloner<FunctionPassContext>,
_ builder: Builder,
_ function: Function
) -> Value {
switch self {
case .undefined:
fatalError("cannot materialize undefined init value")
case .constant(let value):
return cloner.cloneRecursively(globalInitValue: value)
case .aggregate(let fields):
if type.isStruct {
let fieldTypes = type.getNominalFields(in: function)!
let fieldValues = zip(fields, fieldTypes).map {
$0.0.materializeRecursively(type: $0.1, &cloner, builder, function)
}
return builder.createStruct(type: type, elements: fieldValues)
}
if type.isTuple {
let elementValues = zip(fields, type.tupleElements).map {
$0.0.materializeRecursively(type: $0.1, &cloner, builder, function)
}
return builder.createTuple(type: type, elements: elementValues)
}
assert(type.isBuiltinFixedArray)
let elementType = type.builtinFixedArrayElementType(in: function)
let elementValues = fields.map {
$0.materializeRecursively(type: elementType, &cloner, builder, function)
}
return builder.createVector(type: type, arguments: elementValues)
case .enumCase(let caseIndex):
return builder.createEnum(caseIndex: caseIndex, payload: nil, enumType: type)
case .enumCaseWithPayload(let caseIndex, let payload):
let payloadType = type.getEnumCases(in: function)!.getPayloadType(ofCaseIndex: caseIndex)!
let payloadValue = payload.materializeRecursively(type: payloadType, &cloner, builder, function)
return builder.createEnum(caseIndex: caseIndex, payload: payloadValue, enumType: type)
}
}
// Replace `.constant` elements, which are (or contain) loads, init value of the corresponding stack location.
// Some from stack locations.
mutating func resolveLoads(
from stackValues: inout Dictionary<AllocStackInst, GlobalInitValue>,
_ context: FunctionPassContext
) {
var resolvedAllocStacks = InstructionSet(context)
defer { resolvedAllocStacks.deinitialize() }
resolveLoadsRecursively(from: &stackValues, &resolvedAllocStacks, context)
}
private mutating func resolveLoadsRecursively(
from stackValues: inout Dictionary<AllocStackInst, GlobalInitValue>,
_ resolvedAllocStacks: inout InstructionSet,
_ context: FunctionPassContext
) {
switch self {
case .undefined, .enumCase:
break
case .constant(let value):
if value.containsLoad(context) {
switch value {
case is StructInst, is TupleInst:
self = .aggregate((value as! Instruction).operands.lazy.map { .constant($0.value) })
resolveLoadsRecursively(from: &stackValues, &resolvedAllocStacks, context)
case let ei as EnumInst:
self = .enumCaseWithPayload(caseIndex: ei.caseIndex, payload: .constant(ei.payload!))
resolveLoadsRecursively(from: &stackValues, &resolvedAllocStacks, context)
case let li as LoadInst:
guard let allocStack = li.address as? AllocStackInst,
var stackInit = stackValues[allocStack]
else {
self = .undefined
return
}
if resolvedAllocStacks.insert(allocStack) {
stackInit.resolveLoadsRecursively(from: &stackValues, &resolvedAllocStacks, context)
stackValues[allocStack] = stackInit
}
self = stackInit
default:
break
}
}
case .aggregate(let fields):
var newFields = fields
self = .undefined // avoid copy-on-write
for i in 0..<fields.count {
newFields[i].resolveLoadsRecursively(from: &stackValues, &resolvedAllocStacks, context)
}
self = .aggregate(newFields)
case .enumCaseWithPayload(let caseIndex, var payload):
payload.resolveLoadsRecursively(from: &stackValues, &resolvedAllocStacks, context)
self = .enumCaseWithPayload(caseIndex: caseIndex, payload: payload)
}
}
func isValid(_ context: FunctionPassContext) -> Bool {
switch self {
case .undefined:
return false
case .constant(let value):
return value.isValidGlobalInitValue(context)
case .aggregate(let fields):
return fields.allSatisfy { $0.isValid(context) }
case .enumCase:
return true
case .enumCaseWithPayload(_, let payload):
return payload.isValid(context)
}
}
}
private extension Value {
/// Returns true if this value is a `load` or a struct/tuple/enum with a `load` operand.
func containsLoad(_ context: FunctionPassContext) -> Bool {
var worklist = ValueWorklist(context)
defer { worklist.deinitialize() }
worklist.pushIfNotVisited(self)
while let v = worklist.pop() {
switch v {
case is LoadInst:
return true
case is StructInst, is TupleInst:
worklist.pushIfNotVisited(contentsOf: (v as! Instruction).operands.values)
case let ei as EnumInst:
if let payload = ei.payload {
worklist.pushIfNotVisited(payload)
}
default:
break
}
}
return false
}
}
private struct InitValueBuilder: AddressDefUseWalker {
let originalAddress: Value
var initValue = GlobalInitValue.undefined
mutating func leafUse(address: Operand, path: UnusedWalkingPath) -> WalkResult {
switch address.instruction {
case let store as StoreInst:
let accessPath = store.destination.lookThroughRawLayoutAddress.constantAccessPath
switch accessPath.base {
case .global, .stack:
if !initValue.setElement(to: .value(store.source), at: accessPath.projectionPath, type: originalAddress.type) {
return .abortWalk
}
return .continueWalk
case .index(let indexAddr):
// If we have a non-constant index in the access path, check if this is from a loop which
// initializes all elements.
guard store.storesToAllVectorElements(using: indexAddr) else {
return .abortWalk
}
let nonConstAccessPath = store.destination.accessPath
guard indexAddr.base.constantAccessPath.base == nonConstAccessPath.base else {
return .abortWalk
}
// The `nonConstAccessPath` now contains a single `.anyIndexedElement`.
if !initValue.setElement(to: .value(store.source), at: nonConstAccessPath.projectionPath, type: originalAddress.type) {
return .abortWalk
}
return .continueWalk
default:
fatalError("could not compute access path")
}
case let injectEnum as InjectEnumAddrInst:
if injectEnum.element.hasAssociatedValues {
if !injectEnum.operand.value.type.isLoadable(in: injectEnum.parentFunction) {
// TODO: we don't support non-loadable enum cases with payload yet, because IRGen support is missing.
// e.g. `var global: Atomic<Int>? = Atomic<Int>(0)`
// FixedTypeInfo (= used for non-loadable types) is missing the ability to pack a payload into an enum.
return .abortWalk
}
return .continueWalk
}
let accessPath = injectEnum.enum.getAccessPath(fromInitialPath: SmallProjectionPath(.enumCase,
index: injectEnum.caseIndex))
switch accessPath.base {
case .global, .stack:
if !initValue.setElement(to: .enumCaseWithoutPayload(injectEnum), at: accessPath.projectionPath, type: originalAddress.type) {
return .abortWalk
}
return .continueWalk
default:
return .abortWalk
}
case is LoadInst, is DeallocStackInst:
return .continueWalk
case let bi as BuiltinInst:
switch bi.id {
case .PrepareInitialization:
return .continueWalk
case .AddressOfRawLayout:
if let addr2Ptr = bi.uses.getSingleUser(ofType: PointerToAddressInst.self) {
return walkDownUses(ofAddress: addr2Ptr, path: path)
}
return .abortWalk
default:
return .abortWalk
}
default:
return .abortWalk
}
}
}
private extension StoreInst {
func storesToAllVectorElements(using indexAddr: IndexAddrInst) -> Bool {
if let vectorBase = indexAddr.base as? VectorBaseAddrInst,
let headerBr = vectorBase.parentBlock.terminator as? BranchInst,
headerBr.targetBlock == parentBlock,
let vectorSize = vectorBase.vector.type.builtinFixedArraySizeType.valueOfInteger,
let (start, loopCount, increment) = getLoopInductionInfo(of: indexAddr.index.lookThroughIndexScalarCast),
start == 0, loopCount == vectorSize, increment == 1
{
return true
}
return false
}
}
/// Matches the pattern:
/// ```
/// %startValue = integer_literal <start>
/// %incrementValue = integer_literal <increment>
/// %loopEndValue = integer_literal <loopCount>
/// br loopBlock(%startValue)
/// loopBlock(%inductionVar):
/// %add = builtin "sadd_with_overflow"(%inductionVar, %incrementValue, ...)
/// %incrementedInductionVar = tuple_extract %add, 0
/// %loopBreakCondition = builtin "cmp_eq"(%incrementedInductionVar, %loopEndValue)
/// cond_br %loopBreakCondition, exitBlock, backEdgeBlock
/// backEdgeBlock:
/// br loopBlock(%incrementedInductionVar)
/// exitBlock:
/// ```
private func getLoopInductionInfo(of inductionVar: Value) -> (start: Int, loopCount: Int, increment: Int)? {
let loopBlock = inductionVar.parentBlock
if let inductionVarArg = inductionVar as? Argument, inductionVarArg.parentBlock == loopBlock,
let inductionVarPhi = Phi(inductionVarArg),
let condBr = loopBlock.terminator as? CondBranchInst,
let loopBreakCondition = condBr.condition as? BuiltinInst, loopBreakCondition.id == .ICMP_EQ,
let incrementedInductionVar = loopBreakCondition.arguments[0] as? TupleExtractInst,
incrementedInductionVar.fieldIndex == 0,
let add = incrementedInductionVar.tuple as? BuiltinInst, add.id == .SAddOver,
add.arguments[0] == inductionVarArg,
let incrementValue = add.arguments[1] as? IntegerLiteralInst,
let increment = incrementValue.value,
let loopEndValue = loopBreakCondition.arguments[1] as? IntegerLiteralInst,
let loopCount = loopEndValue.value,
let backEdgeBranch = condBr.falseBlock.terminator as? BranchInst,
backEdgeBranch.targetBlock == loopBlock,
inductionVarPhi.incomingOperand(inPredecessor: backEdgeBranch.parentBlock).value == incrementedInductionVar,
let startValue = inductionVarPhi.incomingValue(notInBlock: backEdgeBranch.parentBlock) as? IntegerLiteralInst,
let start = startValue.value
{
return (start, loopCount, increment)
}
return nil
}
private extension Phi {
func incomingValue(notInBlock: BasicBlock) -> Value? {
if let block = predecessors.lazy.filter({ $0 != notInBlock }).singleElement {
return incomingOperand(inPredecessor: block).value
}
return nil
}
}
private extension Function {
func hasSideEffects(besideStoringTo global: GlobalVariable) -> Bool {
return instructions.contains { inst in
switch inst {
case is DeallocStackInst, is DebugStepInst, is DebugValueInst, is BeginAccessInst, is EndAccessInst:
return false
case let alloc as AllocGlobalInst where alloc.global == global:
return false
case let store as StoreInst:
return !store.destination.lookThroughRawLayoutAddress.isAddressOfStack(orGlobal: global)
case let injectEnum as InjectEnumAddrInst:
return !injectEnum.enum.isAddressOfStack(orGlobal: global)
case let bi as BuiltinInst where bi.id == .PrepareInitialization:
return false
default:
return inst.hasUnspecifiedSideEffects
}
}
}
func removeAllInitializationCode(for global: GlobalVariable, _ context: FunctionPassContext) {
for inst in instructions {
switch inst {
case let allocGlobal as AllocGlobalInst where allocGlobal.global == global:
context.erase(instruction: allocGlobal)
case let globalAddr as GlobalAddrInst where globalAddr.global == global:
context.erase(instructionIncludingAllUsers: globalAddr)
default:
break
}
}
context.removeTriviallyDeadInstructionsIgnoringDebugUses(in: self)
}
}
private extension Value {
func isAddressOfStack(orGlobal global: GlobalVariable) -> Bool {
switch accessBase {
case .global(let g) where g == global:
return true
case .stack:
return true
default:
return false
}
}
/// Looks through a `@_rawLayout` projection, which "type casts" a raw-layout struct to it's content, which
/// must match the like-type of the raw-layout, e.g.
/// ```
/// @_rawLayout(like: T)
/// struct S {}
///
/// %2 = builtin "addressOfRawLayout"<S>(%1 : $*S) : $Builtin.RawPointer
/// %3 = pointer_to_address %2 to $*T
/// ```
var lookThroughRawLayoutAddress: Value {
if let ptr2Addr = self as? PointerToAddressInst,
let builtin = ptr2Addr.pointer as? BuiltinInst, builtin.id == .AddressOfRawLayout,
let likeType = builtin.arguments[0].type.rawLayoutSubstitutedLikeType,
builtin.arguments[0].type.rawLayoutSubstitutedCountType == nil,
likeType.canonical == type.canonicalType
{
return builtin.arguments[0]
}
return self
}
}
private extension EnumCases {
func getPayloadType(ofCaseIndex caseIndex: Int) -> Type? {
return first(where: { $0.index == caseIndex })!.payload
}
}