|
| 1 | +//===--- SimplifyAllocStack.swift -----------------------------------------===// |
| 2 | +// |
| 3 | +// This source file is part of the Swift.org open source project |
| 4 | +// |
| 5 | +// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors |
| 6 | +// Licensed under Apache License v2.0 with Runtime Library Exception |
| 7 | +// |
| 8 | +// See https://swift.org/LICENSE.txt for license information |
| 9 | +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors |
| 10 | +// |
| 11 | +//===----------------------------------------------------------------------===// |
| 12 | + |
| 13 | +import SIL |
| 14 | +import AST |
| 15 | + |
| 16 | +extension AllocStackInst : Simplifiable, SILCombineSimplifiable { |
| 17 | + func simplify(_ context: SimplifyContext) { |
| 18 | + if optimizeEnum(context) { |
| 19 | + return |
| 20 | + } |
| 21 | + _ = optimizeExistential(context) |
| 22 | + } |
| 23 | +} |
| 24 | + |
| 25 | +private extension AllocStackInst { |
| 26 | + /// Replaces an alloc_stack of an enum by an alloc_stack of the payload if only one enum case (with payload) |
| 27 | + /// is stored to that location. |
| 28 | + /// |
| 29 | + /// For example: |
| 30 | + /// ``` |
| 31 | + /// %0 = alloc_stack $Optional<T> |
| 32 | + /// %1 = init_enum_data_addr %loc |
| 33 | + /// store %2 to %1 |
| 34 | + /// ... |
| 35 | + /// %3 = unchecked_take_enum_data_addr %0 |
| 36 | + /// %4 = load %3 |
| 37 | + /// ``` |
| 38 | + /// is transformed to |
| 39 | + /// ``` |
| 40 | + /// %0 = alloc_stack $T |
| 41 | + /// store %2 to %0 |
| 42 | + /// ... |
| 43 | + /// %4 = load %0 |
| 44 | + /// ``` |
| 45 | + func optimizeEnum(_ context: SimplifyContext) -> Bool { |
| 46 | + guard let (payloadType, isSingleInitTakePair) = getEnumInfo() else { |
| 47 | + return false |
| 48 | + } |
| 49 | + |
| 50 | + let builder = Builder(before: self, context) |
| 51 | + let newAlloc = builder.createAllocStack(payloadType, |
| 52 | + hasDynamicLifetime: hasDynamicLifetime, |
| 53 | + isLexical: isLexical, |
| 54 | + isFromVarDecl: isFromVarDecl, |
| 55 | + usesMoveableValueDebugInfo: usesMoveableValueDebugInfo) |
| 56 | + let oldAllocType = type |
| 57 | + if let varInfo = debugVariable { |
| 58 | + builder.createDebugValue(value: Undef.get(type: oldAllocType, context), debugVariable: varInfo) |
| 59 | + } |
| 60 | + self.replace(with: newAlloc, context) |
| 61 | + |
| 62 | + for use in newAlloc.uses { |
| 63 | + switch use.instruction { |
| 64 | + case let iea as InjectEnumAddrInst: |
| 65 | + context.erase(instruction: iea) |
| 66 | + case let da as DestroyAddrInst: |
| 67 | + if isSingleInitTakePair { |
| 68 | + // It's not possible that the enum has a payload at the destroy_addr, because it must have already |
| 69 | + // been taken by the take of the single init-take pair. |
| 70 | + // We _have_ to remove the destroy_addr, because we also remove all inject_enum_addrs which might |
| 71 | + // inject a payload-less case before the destroy_addr. |
| 72 | + // Otherwise the enum payload can still be valid at the destroy_addr, so we have to keep the destroy_addr. |
| 73 | + // Just replace the enum with the payload (and because it's not a singleInitTakePair, we can be sure |
| 74 | + // that the enum cannot have any other case than the payload case). |
| 75 | + context.erase(instruction: da) |
| 76 | + } |
| 77 | + case let ieda as InitEnumDataAddrInst: |
| 78 | + ieda.replace(with: newAlloc, context) |
| 79 | + case let uteda as UncheckedTakeEnumDataAddrInst: |
| 80 | + uteda.replace(with: newAlloc, context) |
| 81 | + case is DeallocStackInst: |
| 82 | + break |
| 83 | + case let dv as DebugValueInst: |
| 84 | + // TODO: Add support for op_enum_fragment |
| 85 | + dv.operand.set(to: Undef.get(type: oldAllocType, context), context) |
| 86 | + default: |
| 87 | + fatalError("unexpected alloc_stack user"); |
| 88 | + } |
| 89 | + } |
| 90 | + return true |
| 91 | + } |
| 92 | + |
| 93 | + func getEnumInfo() -> (payloadType: Type, isSingleInitTakePair: Bool)? { |
| 94 | + if !type.isEnum { |
| 95 | + return nil |
| 96 | + } |
| 97 | + var numInits = 0 |
| 98 | + var numTakes = 0 |
| 99 | + var initBlock: BasicBlock? = nil |
| 100 | + var takeBlock: BasicBlock? = nil |
| 101 | + var caseIndex: Int? = nil |
| 102 | + var payloadType: Type? = nil |
| 103 | + for use in uses { |
| 104 | + switch use.instruction { |
| 105 | + case is DestroyAddrInst, |
| 106 | + is DeallocStackInst, |
| 107 | + is DebugValueInst, |
| 108 | + // We'll check init_enum_addr below. |
| 109 | + is InjectEnumAddrInst: |
| 110 | + break |
| 111 | + case let ieda as InitEnumDataAddrInst: |
| 112 | + if let previouslyFoundCase = caseIndex, previouslyFoundCase != ieda.caseIndex { |
| 113 | + return nil |
| 114 | + } |
| 115 | + caseIndex = ieda.caseIndex |
| 116 | + assert(payloadType == nil || payloadType! == ieda.type) |
| 117 | + payloadType = ieda.type |
| 118 | + numInits += 1 |
| 119 | + initBlock = ieda.parentBlock |
| 120 | + case let uted as UncheckedTakeEnumDataAddrInst: |
| 121 | + if let previouslyFoundCase = caseIndex, previouslyFoundCase != uted.caseIndex { |
| 122 | + return nil |
| 123 | + } |
| 124 | + caseIndex = uted.caseIndex |
| 125 | + numTakes += 1 |
| 126 | + takeBlock = uted.parentBlock |
| 127 | + default: |
| 128 | + return nil |
| 129 | + } |
| 130 | + } |
| 131 | + |
| 132 | + guard let caseIndex, let payloadType else { |
| 133 | + return nil |
| 134 | + } |
| 135 | + |
| 136 | + // If the enum has a single init-take pair in a single block, we know that the enum cannot contain any |
| 137 | + // valid payload outside that init-take pair. |
| 138 | + // |
| 139 | + // This also means that we can ignore any inject_enum_addr of another enum case, because this can only |
| 140 | + // inject a case without a payload. |
| 141 | + if numInits == 1 && numTakes == 1 && initBlock == takeBlock { |
| 142 | + return (payloadType, isSingleInitTakePair: true) |
| 143 | + } |
| 144 | + // No single init-take pair: We cannot ignore inject_enum_addrs with a mismatching case. |
| 145 | + if uses.users(ofType: InjectEnumAddrInst.self).contains(where: { $0.caseIndex != caseIndex}) { |
| 146 | + return nil |
| 147 | + } |
| 148 | + return (payloadType, isSingleInitTakePair: false) |
| 149 | + } |
| 150 | + |
| 151 | + /// Replaces an alloc_stack of an existential by an alloc_stack of the concrete type. |
| 152 | + /// |
| 153 | + /// For example: |
| 154 | + /// ``` |
| 155 | + /// %0 = alloc_stack $any P |
| 156 | + /// %1 = init_existential_addr %0, $T |
| 157 | + /// use %1 |
| 158 | + /// ``` |
| 159 | + /// is transformed to |
| 160 | + /// ``` |
| 161 | + /// %0 = alloc_stack $T |
| 162 | + /// use %0 |
| 163 | + /// ``` |
| 164 | + /// |
| 165 | + /// Also, if the alloc_stack is already an opened existential and the concrete type is known, |
| 166 | + /// replace it as well: |
| 167 | + /// ``` |
| 168 | + /// %0 = metatype $@thick T.Type |
| 169 | + /// %1 = init_existential_metatype %0, $@thick any P.Type |
| 170 | + /// %2 = open_existential_metatype %1 : $@thick any P.Type to $@thick (@opened("X", P) Self).Type |
| 171 | + /// ... |
| 172 | + /// %3 = alloc_stack $@opened("X", any P) Self |
| 173 | + /// use %3 |
| 174 | + /// ``` |
| 175 | + /// is transformed to |
| 176 | + /// ``` |
| 177 | + /// ... |
| 178 | + /// %3 = alloc_stack $T |
| 179 | + /// use %3 |
| 180 | + /// ``` |
| 181 | + func optimizeExistential(_ context: SimplifyContext) -> Bool { |
| 182 | + guard type.astType.isExistential || type.astType.isExistentialArchetype, |
| 183 | + let concreteFormalType = getConcreteTypeOfExistential() |
| 184 | + else { |
| 185 | + return false |
| 186 | + } |
| 187 | + |
| 188 | + let builder = Builder(before: self, context) |
| 189 | + let newAlloc = builder.createAllocStack(concreteFormalType.loweredType(in: parentFunction), |
| 190 | + hasDynamicLifetime: hasDynamicLifetime, |
| 191 | + isLexical: isLexical, |
| 192 | + isFromVarDecl: isFromVarDecl, |
| 193 | + usesMoveableValueDebugInfo: usesMoveableValueDebugInfo) |
| 194 | + for use in uses { |
| 195 | + switch use.instruction { |
| 196 | + case let dea as DeinitExistentialAddrInst: |
| 197 | + context.erase(instruction: dea) |
| 198 | + case let iea as InitExistentialAddrInst: |
| 199 | + if iea.type != newAlloc.type { |
| 200 | + // We need a cast if the concrete type of the init_existential_addr is itself an opened existential |
| 201 | + // for which we know the concrete type (which is differnt). |
| 202 | + let builder = Builder(before: iea, context) |
| 203 | + let addrCast = builder.createUncheckedAddrCast(from: newAlloc, to: iea.type) |
| 204 | + iea.replace(with: addrCast, context) |
| 205 | + } else { |
| 206 | + iea.replace(with: newAlloc, context) |
| 207 | + } |
| 208 | + case let oea as OpenExistentialAddrInst: |
| 209 | + assert(oea.uses.ignoreUsers(ofType: DestroyAddrInst.self).isEmpty) |
| 210 | + oea.replace(with: newAlloc, context) |
| 211 | + case let cab as CheckedCastAddrBranchInst: |
| 212 | + let builder = Builder(before: cab, context) |
| 213 | + builder.createCheckedCastAddrBranch( |
| 214 | + source: newAlloc, sourceFormalType: concreteFormalType, |
| 215 | + destination: cab.destination, targetFormalType: cab.targetFormalType, |
| 216 | + consumptionKind: cab.consumptionKind, |
| 217 | + successBlock: cab.successBlock, failureBlock: cab.failureBlock) |
| 218 | + context.erase(instruction: cab) |
| 219 | + case let ucca as UnconditionalCheckedCastAddrInst: |
| 220 | + let builder = Builder(before: ucca, context) |
| 221 | + builder.createUnconditionalCheckedCastAddr( |
| 222 | + source: newAlloc, sourceFormalType: concreteFormalType, |
| 223 | + destination: ucca.destination, targetFormalType: ucca.targetFormalType) |
| 224 | + context.erase(instruction: ucca) |
| 225 | + default: |
| 226 | + use.set(to: newAlloc, context) |
| 227 | + } |
| 228 | + } |
| 229 | + context.erase(instruction: self) |
| 230 | + return true |
| 231 | + } |
| 232 | + |
| 233 | + // Returns the concrete type of this alloc_stack if known. |
| 234 | + // Assuming that its type is either an existential or an opened existential. |
| 235 | + private func getConcreteTypeOfExistential() -> CanonicalType? { |
| 236 | + var initExistential: InitExistentialAddrInst? = nil |
| 237 | + var requiresLegalFormalType = false |
| 238 | + |
| 239 | + for use in uses { |
| 240 | + switch use.instruction { |
| 241 | + case is DestroyAddrInst, |
| 242 | + is DeinitExistentialAddrInst, |
| 243 | + is DeallocStackInst, |
| 244 | + is DebugValueInst: |
| 245 | + break |
| 246 | + case let oea as OpenExistentialAddrInst: |
| 247 | + if !oea.uses.ignoreUsers(ofType: DestroyAddrInst.self).isEmpty { |
| 248 | + return nil |
| 249 | + } |
| 250 | + case let iea as InitExistentialAddrInst: |
| 251 | + if initExistential != nil { |
| 252 | + return nil |
| 253 | + } |
| 254 | + initExistential = iea |
| 255 | + case is CheckedCastAddrBranchInst, is UnconditionalCheckedCastAddrInst: |
| 256 | + // To construct a new cast instruction we need a formal type. |
| 257 | + requiresLegalFormalType = true |
| 258 | + fallthrough |
| 259 | + case is UncheckedAddrCastInst: |
| 260 | + if use != use.instruction.operands[0] { |
| 261 | + return nil |
| 262 | + } |
| 263 | + default: |
| 264 | + return nil |
| 265 | + } |
| 266 | + } |
| 267 | + let concreteType: CanonicalType |
| 268 | + if let initExistential { |
| 269 | + assert(self.type.astType.isExistential) |
| 270 | + if let cft = initExistential.concreteTypeOfDependentExistentialArchetype { |
| 271 | + // Case 1: We will replace the alloc_stack of an existential with the concrete type. |
| 272 | + // `alloc_stack $any P` -> `alloc_stack $ConcreteType` |
| 273 | + concreteType = cft |
| 274 | + } else { |
| 275 | + // The instruction which defines the existential archetype must dominate the alloc_stack, because |
| 276 | + // after the transformation the alloc_stack will use the existential archetype. |
| 277 | + for openArchetypeOp in initExistential.typeDependentOperands { |
| 278 | + if !openArchetypeOp.value.definingInstruction!.dominatesInSameBlock(self) { |
| 279 | + return nil |
| 280 | + } |
| 281 | + } |
| 282 | + // Case 2: We will replace the alloc_stack of an existential with the existential archetype. |
| 283 | + // `alloc_stack $any P` -> `alloc_stack $@opened("...")` |
| 284 | + concreteType = initExistential.type.astType |
| 285 | + } |
| 286 | + } else if self.type.astType.isExistentialArchetype, let cft = self.concreteTypeOfDependentExistentialArchetype { |
| 287 | + // Case 3: We will replace the alloc_stack of an existential archetype with the concrete type: |
| 288 | + // `alloc_stack $@opened("...")` -> `alloc_stack $ConcreteType` |
| 289 | + concreteType = cft |
| 290 | + } else { |
| 291 | + return nil |
| 292 | + } |
| 293 | + if requiresLegalFormalType && !concreteType.isLegalFormalType { |
| 294 | + return nil |
| 295 | + } |
| 296 | + return concreteType |
| 297 | + } |
| 298 | +} |
0 commit comments