mirror of
https://github.com/apple/swift.git
synced 2025-12-14 20:36:38 +01:00
This showed up on and off again on the source-compatibility testsuite project hummingbird. The gist of the problem is that transformations may not rewrite the type of an inlined instance of a variable without also createing a deep copy of the inlined function with a different name (and e.g., a specialization suffix). Otherwise the modified inlined variable will cause an inconsistency when later compiler passes try to create the abstract declaration of that inlined function as there would be conflicting declarations for that variable. Since SILDebugScope isn't yet available in the SwiftCompilerSources this fix just drop these variables, but it would be absolutely possible to preserve them by using the same mechanism that SILCloner uses to create a deep copy of the inlined function scopes. rdar://163167975
317 lines
12 KiB
Swift
317 lines
12 KiB
Swift
//===--- SimplifyAllocStack.swift -----------------------------------------===//
|
|
//
|
|
// This source file is part of the Swift.org open source project
|
|
//
|
|
// Copyright (c) 2014 - 2025 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
|
|
import AST
|
|
|
|
extension AllocStackInst : Simplifiable, SILCombineSimplifiable {
|
|
func simplify(_ context: SimplifyContext) {
|
|
if optimizeEnum(context) {
|
|
return
|
|
}
|
|
_ = optimizeExistential(context)
|
|
}
|
|
}
|
|
|
|
private extension AllocStackInst {
|
|
/// Replaces an alloc_stack of an enum by an alloc_stack of the payload if only one enum case (with payload)
|
|
/// is stored to that location.
|
|
///
|
|
/// For example:
|
|
/// ```
|
|
/// %0 = alloc_stack $Optional<T>
|
|
/// %1 = init_enum_data_addr %loc
|
|
/// store %2 to %1
|
|
/// ...
|
|
/// %3 = unchecked_take_enum_data_addr %0
|
|
/// %4 = load %3
|
|
/// ```
|
|
/// is transformed to
|
|
/// ```
|
|
/// %0 = alloc_stack $T
|
|
/// store %2 to %0
|
|
/// ...
|
|
/// %4 = load %0
|
|
/// ```
|
|
func optimizeEnum(_ context: SimplifyContext) -> Bool {
|
|
guard let (payloadType, isSingleInitTakePair) = getEnumInfo() else {
|
|
return false
|
|
}
|
|
|
|
let builder = Builder(before: self, context)
|
|
let newAlloc = builder.createAllocStack(payloadType,
|
|
hasDynamicLifetime: hasDynamicLifetime,
|
|
isLexical: isLexical,
|
|
isFromVarDecl: isFromVarDecl,
|
|
usesMoveableValueDebugInfo: usesMoveableValueDebugInfo)
|
|
let oldAllocType = type
|
|
if let varInfo = debugVariable {
|
|
builder.createDebugValue(value: Undef.get(type: oldAllocType, context), debugVariable: varInfo)
|
|
}
|
|
self.replace(with: newAlloc, context)
|
|
|
|
for use in newAlloc.uses {
|
|
switch use.instruction {
|
|
case let iea as InjectEnumAddrInst:
|
|
context.erase(instruction: iea)
|
|
case let da as DestroyAddrInst:
|
|
if isSingleInitTakePair {
|
|
// It's not possible that the enum has a payload at the destroy_addr, because it must have already
|
|
// been taken by the take of the single init-take pair.
|
|
// We _have_ to remove the destroy_addr, because we also remove all inject_enum_addrs which might
|
|
// inject a payload-less case before the destroy_addr.
|
|
// Otherwise the enum payload can still be valid at the destroy_addr, so we have to keep the destroy_addr.
|
|
// Just replace the enum with the payload (and because it's not a singleInitTakePair, we can be sure
|
|
// that the enum cannot have any other case than the payload case).
|
|
context.erase(instruction: da)
|
|
}
|
|
case let ieda as InitEnumDataAddrInst:
|
|
ieda.replace(with: newAlloc, context)
|
|
case let uteda as UncheckedTakeEnumDataAddrInst:
|
|
uteda.replace(with: newAlloc, context)
|
|
case is DeallocStackInst:
|
|
break
|
|
case let dv as DebugValueInst:
|
|
// TODO: Add support for op_enum_fragment
|
|
dv.operand.set(to: Undef.get(type: oldAllocType, context), context)
|
|
default:
|
|
fatalError("unexpected alloc_stack user");
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func getEnumInfo() -> (payloadType: Type, isSingleInitTakePair: Bool)? {
|
|
if !type.isEnum {
|
|
return nil
|
|
}
|
|
var numInits = 0
|
|
var numTakes = 0
|
|
var initBlock: BasicBlock? = nil
|
|
var takeBlock: BasicBlock? = nil
|
|
var caseIndex: Int? = nil
|
|
var payloadType: Type? = nil
|
|
for use in uses {
|
|
switch use.instruction {
|
|
case is DestroyAddrInst,
|
|
is DeallocStackInst,
|
|
is DebugValueInst,
|
|
// We'll check init_enum_addr below.
|
|
is InjectEnumAddrInst:
|
|
break
|
|
case let ieda as InitEnumDataAddrInst:
|
|
if let previouslyFoundCase = caseIndex, previouslyFoundCase != ieda.caseIndex {
|
|
return nil
|
|
}
|
|
caseIndex = ieda.caseIndex
|
|
assert(payloadType == nil || payloadType! == ieda.type)
|
|
payloadType = ieda.type
|
|
numInits += 1
|
|
initBlock = ieda.parentBlock
|
|
case let uted as UncheckedTakeEnumDataAddrInst:
|
|
if let previouslyFoundCase = caseIndex, previouslyFoundCase != uted.caseIndex {
|
|
return nil
|
|
}
|
|
caseIndex = uted.caseIndex
|
|
numTakes += 1
|
|
takeBlock = uted.parentBlock
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
guard let caseIndex, let payloadType else {
|
|
return nil
|
|
}
|
|
|
|
// If the enum has a single init-take pair in a single block, we know that the enum cannot contain any
|
|
// valid payload outside that init-take pair.
|
|
//
|
|
// This also means that we can ignore any inject_enum_addr of another enum case, because this can only
|
|
// inject a case without a payload.
|
|
if numInits == 1 && numTakes == 1 && initBlock == takeBlock {
|
|
return (payloadType, isSingleInitTakePair: true)
|
|
}
|
|
// No single init-take pair: We cannot ignore inject_enum_addrs with a mismatching case.
|
|
if uses.users(ofType: InjectEnumAddrInst.self).contains(where: { $0.caseIndex != caseIndex}) {
|
|
return nil
|
|
}
|
|
return (payloadType, isSingleInitTakePair: false)
|
|
}
|
|
|
|
/// Replaces an alloc_stack of an existential by an alloc_stack of the concrete type.
|
|
///
|
|
/// For example:
|
|
/// ```
|
|
/// %0 = alloc_stack $any P
|
|
/// %1 = init_existential_addr %0, $T
|
|
/// use %1
|
|
/// ```
|
|
/// is transformed to
|
|
/// ```
|
|
/// %0 = alloc_stack $T
|
|
/// use %0
|
|
/// ```
|
|
///
|
|
/// Also, if the alloc_stack is already an opened existential and the concrete type is known,
|
|
/// replace it as well:
|
|
/// ```
|
|
/// %0 = metatype $@thick T.Type
|
|
/// %1 = init_existential_metatype %0, $@thick any P.Type
|
|
/// %2 = open_existential_metatype %1 : $@thick any P.Type to $@thick (@opened("X", P) Self).Type
|
|
/// ...
|
|
/// %3 = alloc_stack $@opened("X", any P) Self
|
|
/// use %3
|
|
/// ```
|
|
/// is transformed to
|
|
/// ```
|
|
/// ...
|
|
/// %3 = alloc_stack $T
|
|
/// use %3
|
|
/// ```
|
|
func optimizeExistential(_ context: SimplifyContext) -> Bool {
|
|
// TODO: support non-root existential archetypes
|
|
guard type.isExistential || type.isRootExistentialArchetype,
|
|
let concreteFormalType = getConcreteTypeOfExistential()
|
|
else {
|
|
return false
|
|
}
|
|
|
|
let builder = Builder(before: self, context)
|
|
let newAlloc = builder.createAllocStack(concreteFormalType.loweredType(in: parentFunction),
|
|
hasDynamicLifetime: hasDynamicLifetime,
|
|
isLexical: isLexical,
|
|
isFromVarDecl: isFromVarDecl,
|
|
usesMoveableValueDebugInfo: usesMoveableValueDebugInfo)
|
|
for use in uses {
|
|
switch use.instruction {
|
|
case let dea as DeinitExistentialAddrInst:
|
|
context.erase(instruction: dea)
|
|
case let iea as InitExistentialAddrInst:
|
|
if iea.type != newAlloc.type {
|
|
// We need a cast if the concrete type of the init_existential_addr is itself an opened existential
|
|
// for which we know the concrete type (which is differnt).
|
|
let builder = Builder(before: iea, context)
|
|
let addrCast = builder.createUncheckedAddrCast(from: newAlloc, to: iea.type)
|
|
iea.replace(with: addrCast, context)
|
|
} else {
|
|
iea.replace(with: newAlloc, context)
|
|
}
|
|
case let oea as OpenExistentialAddrInst:
|
|
assert(oea.uses.ignore(usersOfType: DestroyAddrInst.self).isEmpty)
|
|
oea.replace(with: newAlloc, context)
|
|
case let cab as CheckedCastAddrBranchInst:
|
|
let builder = Builder(before: cab, context)
|
|
builder.createCheckedCastAddrBranch(
|
|
source: newAlloc, sourceFormalType: concreteFormalType,
|
|
destination: cab.destination, targetFormalType: cab.targetFormalType,
|
|
options: cab.checkedCastOptions,
|
|
consumptionKind: cab.consumptionKind,
|
|
successBlock: cab.successBlock, failureBlock: cab.failureBlock)
|
|
context.erase(instruction: cab)
|
|
case let ucca as UnconditionalCheckedCastAddrInst:
|
|
let builder = Builder(before: ucca, context)
|
|
builder.createUnconditionalCheckedCastAddr(
|
|
options: ucca.checkedCastOptions,
|
|
source: newAlloc, sourceFormalType: concreteFormalType,
|
|
destination: ucca.destination, targetFormalType: ucca.targetFormalType)
|
|
context.erase(instruction: ucca)
|
|
case let dv as DebugValueInst:
|
|
if dv.location.isInlined {
|
|
// We cannot change the type of an inlined instance of a variable
|
|
// without renaming the inlined function to get a unique
|
|
// specialization suffix (prior art exists in
|
|
// SILCloner::remapFunction()).
|
|
// For now, just remove affected inlined variables.
|
|
use.set(to: Undef.get(type: type, context), context)
|
|
} else {
|
|
use.set(to: newAlloc, context)
|
|
}
|
|
default:
|
|
use.set(to: newAlloc, context)
|
|
}
|
|
}
|
|
context.erase(instruction: self)
|
|
return true
|
|
}
|
|
|
|
// Returns the concrete type of this alloc_stack if known.
|
|
// Assuming that its type is either an existential or an opened existential.
|
|
private func getConcreteTypeOfExistential() -> CanonicalType? {
|
|
var initExistential: InitExistentialAddrInst? = nil
|
|
var requiresLegalFormalType = false
|
|
|
|
for use in uses {
|
|
switch use.instruction {
|
|
case is DestroyAddrInst,
|
|
is DeinitExistentialAddrInst,
|
|
is DeallocStackInst,
|
|
is DebugValueInst:
|
|
break
|
|
case let oea as OpenExistentialAddrInst:
|
|
if !oea.uses.ignore(usersOfType: DestroyAddrInst.self).isEmpty {
|
|
return nil
|
|
}
|
|
case let iea as InitExistentialAddrInst:
|
|
if initExistential != nil {
|
|
return nil
|
|
}
|
|
initExistential = iea
|
|
case is CheckedCastAddrBranchInst, is UnconditionalCheckedCastAddrInst:
|
|
// To construct a new cast instruction we need a formal type.
|
|
requiresLegalFormalType = true
|
|
if use != use.instruction.operands[0] {
|
|
return nil
|
|
}
|
|
case is UncheckedAddrCastInst:
|
|
if self.type.isExistential {
|
|
// Bail if the address of the original existential escapes.
|
|
// This is not a problem if the alloc_stack already contains the opened existential.
|
|
return nil
|
|
}
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
let concreteType: CanonicalType
|
|
if let initExistential {
|
|
assert(self.type.isExistential)
|
|
if let cft = initExistential.concreteTypeOfDependentExistentialArchetype {
|
|
// Case 1: We will replace the alloc_stack of an existential with the concrete type.
|
|
// `alloc_stack $any P` -> `alloc_stack $ConcreteType`
|
|
concreteType = cft
|
|
} else {
|
|
// The instruction or argument which defines the archetype must dominate the alloc_stack
|
|
// because after the transformation, the alloc_stack will use the archetype.
|
|
for typeDependentOp in initExistential.typeDependentOperands {
|
|
if !typeDependentOp.value.triviallyDominates(self) {
|
|
return nil
|
|
}
|
|
}
|
|
// Case 2: We will replace the alloc_stack of an existential with the existential archetype.
|
|
// `alloc_stack $any P` -> `alloc_stack $@opened("...")`
|
|
concreteType = initExistential.type.canonicalType
|
|
}
|
|
} else if self.type.isExistentialArchetype, let cft = self.concreteTypeOfDependentExistentialArchetype {
|
|
// Case 3: We will replace the alloc_stack of an existential archetype with the concrete type:
|
|
// `alloc_stack $@opened("...")` -> `alloc_stack $ConcreteType`
|
|
concreteType = cft
|
|
} else {
|
|
return nil
|
|
}
|
|
if requiresLegalFormalType && !concreteType.isLegalFormalType {
|
|
return nil
|
|
}
|
|
return concreteType
|
|
}
|
|
}
|