Files
swift-mirror/SwiftCompilerSources/Sources/Optimizer/InstructionSimplification/SimplifyAllocStack.swift
Erik Eckstein 56521f0c3d Optimizer: fix handling of dependent existential archetypes in alloc_stack and apply simplification
When replacing an opened existential type with the concrete type, we didn't consider that the existential archetype can also be a "dependent" type of the root archetype.
For now, just bail in this case. In future we can support dependent archetypes as well.

Fixes a compiler crash.
rdar://158594365
2025-08-26 18:01:10 +02:00

306 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.ignoreUses(ofType: 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)
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.ignoreUses(ofType: 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
}
}