|
| 1 | +//===--- EmbeddedSwiftDiagnostics.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 AST |
| 14 | +import SIL |
| 15 | + |
| 16 | +/// Diagnoses violations of Embedded Swift language restrictions. |
| 17 | +/// |
| 18 | +let embeddedSwiftDiagnostics = ModulePass(name: "embedded-swift-diagnostics") { |
| 19 | + (moduleContext: ModulePassContext) in |
| 20 | + |
| 21 | + guard moduleContext.options.enableEmbeddedSwift, |
| 22 | + // Skip all embedded diagnostics if asked. This is used from SourceKit to avoid reporting |
| 23 | + // false positives when WMO is turned off for indexing purposes. |
| 24 | + moduleContext.enableWMORequiredDiagnostics |
| 25 | + else { |
| 26 | + return |
| 27 | + } |
| 28 | + |
| 29 | + // Try to start with public and exported functions to get better caller information in the diagnostics. |
| 30 | + let allFunctions = Array(moduleContext.functions.lazy.filter { !$0.isGeneric }) |
| 31 | + .sorted(by: { $0.priority < $1.priority }) |
| 32 | + |
| 33 | + var checker = FunctionChecker(moduleContext) |
| 34 | + defer { checker.deinitialize() } |
| 35 | + |
| 36 | + for function in allFunctions { |
| 37 | + do { |
| 38 | + assert(checker.callStack.isEmpty) |
| 39 | + try checker.checkFunction(function) |
| 40 | + } catch let error as Diagnostic { |
| 41 | + checker.diagnose(error) |
| 42 | + } catch { |
| 43 | + fatalError("unknown error thrown") |
| 44 | + } |
| 45 | + } |
| 46 | + |
| 47 | + checkVTables(moduleContext) |
| 48 | +} |
| 49 | + |
| 50 | +private struct FunctionChecker { |
| 51 | + let context: ModulePassContext |
| 52 | + var visitedFunctions = Set<Function>() |
| 53 | + var visitedConformances = Set<Conformance>() |
| 54 | + var callStack: Stack<CallSite> |
| 55 | + |
| 56 | + init(_ context: ModulePassContext) { |
| 57 | + self.context = context |
| 58 | + self.callStack = Stack(context) |
| 59 | + } |
| 60 | + |
| 61 | + mutating func deinitialize() { |
| 62 | + callStack.deinitialize() |
| 63 | + } |
| 64 | + |
| 65 | + mutating func checkFunction(_ function: Function) throws { |
| 66 | + guard function.isDefinition, |
| 67 | + // Avoid infinite recursion |
| 68 | + visitedFunctions.insert(function).inserted |
| 69 | + else { |
| 70 | + return |
| 71 | + } |
| 72 | + |
| 73 | + for inst in function.instructions { |
| 74 | + try checkInstruction(inst) |
| 75 | + } |
| 76 | + } |
| 77 | + |
| 78 | + mutating func checkInstruction(_ instruction: Instruction) throws { |
| 79 | + switch instruction { |
| 80 | + case is OpenExistentialMetatypeInst, |
| 81 | + is InitExistentialMetatypeInst: |
| 82 | + throw Diagnostic(.embedded_swift_metatype_type, instruction.operands[0].value.type, at: instruction.location) |
| 83 | + |
| 84 | + case is OpenExistentialBoxInst, |
| 85 | + is OpenExistentialBoxValueInst, |
| 86 | + is OpenExistentialValueInst, |
| 87 | + is OpenExistentialAddrInst, |
| 88 | + is InitExistentialAddrInst, |
| 89 | + is InitExistentialValueInst, |
| 90 | + is ExistentialMetatypeInst: |
| 91 | + throw Diagnostic(.embedded_swift_existential_type, instruction.operands[0].value.type, at: instruction.location) |
| 92 | + |
| 93 | + case let aeb as AllocExistentialBoxInst: |
| 94 | + throw Diagnostic(.embedded_swift_existential_type, aeb.type, at: instruction.location) |
| 95 | + |
| 96 | + case let ier as InitExistentialRefInst: |
| 97 | + for conf in ier.conformances { |
| 98 | + try checkConformance(conf, location: ier.location) |
| 99 | + } |
| 100 | + |
| 101 | + case is ValueMetatypeInst, |
| 102 | + is MetatypeInst: |
| 103 | + let metaType = (instruction as! SingleValueInstruction).type |
| 104 | + if metaType.representationOfMetatype != .thin { |
| 105 | + let rawType = metaType.canonicalType.rawType.instanceTypeOfMetatype |
| 106 | + let type = rawType.isDynamicSelf ? rawType.staticTypeOfDynamicSelf : rawType |
| 107 | + if !type.isClass { |
| 108 | + throw Diagnostic(.embedded_swift_metatype_type, type, at: instruction.location) |
| 109 | + } |
| 110 | + } |
| 111 | + |
| 112 | + case is KeyPathInst: |
| 113 | + throw Diagnostic(.embedded_swift_keypath, at: instruction.location) |
| 114 | + |
| 115 | + case is CheckedCastAddrBranchInst, |
| 116 | + is UnconditionalCheckedCastAddrInst: |
| 117 | + throw Diagnostic(.embedded_swift_dynamic_cast, at: instruction.location) |
| 118 | + |
| 119 | + case let abi as AllocBoxInst: |
| 120 | + // It needs a bit of work to support alloc_box of generic non-copyable structs/enums with deinit, |
| 121 | + // because we need to specialize the deinit functions, though they are not explicitly referenced in SIL. |
| 122 | + // Until this is supported, give an error in such cases. Otherwise IRGen would crash. |
| 123 | + if abi.allocsGenericValueTypeWithDeinit { |
| 124 | + throw Diagnostic(.embedded_capture_of_generic_value_with_deinit, at: abi.location) |
| 125 | + } |
| 126 | + fallthrough |
| 127 | + case is AllocRefInst, |
| 128 | + is AllocRefDynamicInst: |
| 129 | + if context.options.noAllocations { |
| 130 | + throw Diagnostic(.embedded_swift_allocating_type, (instruction as! SingleValueInstruction).type, |
| 131 | + at: instruction.location) |
| 132 | + } |
| 133 | + case is BeginApplyInst: |
| 134 | + throw Diagnostic(.embedded_swift_allocating_coroutine, at: instruction.location) |
| 135 | + case is ThunkInst: |
| 136 | + throw Diagnostic(.embedded_swift_allocating, at: instruction.location) |
| 137 | + |
| 138 | + case let pai as PartialApplyInst: |
| 139 | + if context.options.noAllocations && !pai.isOnStack { |
| 140 | + throw Diagnostic(.embedded_swift_allocating_closure, at: instruction.location) |
| 141 | + } |
| 142 | + |
| 143 | + case let bi as BuiltinInst: |
| 144 | + switch bi.id { |
| 145 | + case .AllocRaw: |
| 146 | + if context.options.noAllocations { |
| 147 | + throw Diagnostic(.embedded_swift_allocating, at: instruction.location) |
| 148 | + } |
| 149 | + case .BuildOrdinaryTaskExecutorRef, |
| 150 | + .BuildOrdinarySerialExecutorRef, |
| 151 | + .BuildComplexEqualitySerialExecutorRef: |
| 152 | + // Those builtins implicitly create an existential. |
| 153 | + try checkConformance(bi.substitutionMap.conformances[0], location: bi.location) |
| 154 | + |
| 155 | + default: |
| 156 | + break |
| 157 | + } |
| 158 | + |
| 159 | + case let apply as ApplySite: |
| 160 | + if context.options.noAllocations && apply.isAsync { |
| 161 | + throw Diagnostic(.embedded_swift_allocating_type, at: instruction.location) |
| 162 | + } |
| 163 | + |
| 164 | + if !apply.callee.type.hasValidSignatureForEmbedded, |
| 165 | + // Some runtime functions have generic parameters in SIL, which are not used in IRGen. |
| 166 | + // Therefore exclude runtime functions at all. |
| 167 | + !apply.callsEmbeddedRuntimeFunction |
| 168 | + { |
| 169 | + switch apply.callee { |
| 170 | + case let cmi as ClassMethodInst: |
| 171 | + throw Diagnostic(.embedded_cannot_specialize_class_method, cmi.member, at: instruction.location) |
| 172 | + case let wmi as WitnessMethodInst: |
| 173 | + throw Diagnostic(.embedded_cannot_specialize_witness_method, wmi.member, at: instruction.location) |
| 174 | + default: |
| 175 | + throw Diagnostic(.embedded_call_generic_function, at: instruction.location) |
| 176 | + } |
| 177 | + } |
| 178 | + |
| 179 | + // Although all (non-generic) functions are initially put into the worklist there are two reasons |
| 180 | + // to call `checkFunction` recursively: |
| 181 | + // * To get a better caller info in the diagnostics. |
| 182 | + // * When passing an opened existential to a generic function, it's valid in Embedded swift even if the |
| 183 | + // generic is not specialized. We need to check such generic functions, too. |
| 184 | + if let callee = apply.referencedFunction { |
| 185 | + callStack.push(CallSite(apply: apply, callee: callee)) |
| 186 | + try checkFunction(callee) |
| 187 | + _ = callStack.pop() |
| 188 | + } |
| 189 | + |
| 190 | + default: |
| 191 | + break |
| 192 | + } |
| 193 | + } |
| 194 | + |
| 195 | + // Check for any violations in witness tables for existentials. |
| 196 | + mutating func checkConformance(_ conformance: Conformance, location: Location) throws { |
| 197 | + guard conformance.isConcrete, |
| 198 | + // Avoid infinite recursion |
| 199 | + visitedConformances.insert(conformance).inserted, |
| 200 | + let witnessTable = context.lookupWitnessTable(for: conformance) |
| 201 | + else { |
| 202 | + return |
| 203 | + } |
| 204 | + if !conformance.protocol.requiresClass { |
| 205 | + throw Diagnostic(.embedded_swift_existential_protocol, conformance.protocol.name, at: location) |
| 206 | + } |
| 207 | + |
| 208 | + for entry in witnessTable.entries { |
| 209 | + switch entry { |
| 210 | + case .invalid, .associatedType: |
| 211 | + break |
| 212 | + case .method(let requirement, let witness): |
| 213 | + if let witness = witness { |
| 214 | + callStack.push(CallSite(location: location, kind: .conformance)) |
| 215 | + if witness.isGeneric { |
| 216 | + throw Diagnostic(.embedded_cannot_specialize_witness_method, requirement, at: witness.location) |
| 217 | + } |
| 218 | + try checkFunction(witness) |
| 219 | + _ = callStack.pop() |
| 220 | + } |
| 221 | + case .baseProtocol(_, let witness): |
| 222 | + try checkConformance(witness, location: location) |
| 223 | + case .associatedConformance(_, let assocConf): |
| 224 | + // If it's not a class protocol, the associated type can never be used to create |
| 225 | + // an existential. Therefore this witness entry is never used at runtime in embedded swift. |
| 226 | + if assocConf.protocol.requiresClass { |
| 227 | + try checkConformance(assocConf, location: location) |
| 228 | + } |
| 229 | + } |
| 230 | + } |
| 231 | + } |
| 232 | + |
| 233 | + mutating func diagnose(_ error: Diagnostic) { |
| 234 | + var diagPrinted = false |
| 235 | + if error.position != nil { |
| 236 | + context.diagnosticEngine.diagnose(error) |
| 237 | + diagPrinted = true |
| 238 | + } |
| 239 | + |
| 240 | + // If the original instruction doesn't have a location (e.g. because it's in a stdlib function), |
| 241 | + // search the callstack and use the location from a call site. |
| 242 | + while let callSite = callStack.pop() { |
| 243 | + if !diagPrinted { |
| 244 | + if callSite.location.hasValidLineNumber { |
| 245 | + context.diagnosticEngine.diagnose(error.id, error.arguments, at: callSite.location) |
| 246 | + diagPrinted = true |
| 247 | + } |
| 248 | + } else { |
| 249 | + // Print useful callsite information as a note (see `CallSite`) |
| 250 | + switch callSite.kind { |
| 251 | + case .constructorCall: |
| 252 | + context.diagnosticEngine.diagnose(.embedded_constructor_called, at: callSite.location) |
| 253 | + case .specializedCall: |
| 254 | + context.diagnosticEngine.diagnose(.embedded_specialization_called_from, at: callSite.location) |
| 255 | + case .conformance: |
| 256 | + context.diagnosticEngine.diagnose(.embedded_existential_created, at: callSite.location) |
| 257 | + case .call: |
| 258 | + break |
| 259 | + } |
| 260 | + } |
| 261 | + } |
| 262 | + if !diagPrinted { |
| 263 | + context.diagnosticEngine.diagnose(error) |
| 264 | + } |
| 265 | + } |
| 266 | +} |
| 267 | + |
| 268 | +// Print errors for generic functions in vtables, which is not allowed in embedded Swift. |
| 269 | +private func checkVTables(_ context: ModulePassContext) { |
| 270 | + for vTable in context.vTables { |
| 271 | + if !vTable.class.isGenericAtAnyLevel || vTable.isSpecialized { |
| 272 | + for entry in vTable.entries where entry.implementation.isGeneric { |
| 273 | + context.diagnosticEngine.diagnose(.embedded_cannot_specialize_class_method, entry.methodDecl, |
| 274 | + at: entry.methodDecl.location) |
| 275 | + } |
| 276 | + } |
| 277 | + } |
| 278 | +} |
| 279 | + |
| 280 | +/// Relevant call site information for diagnostics. |
| 281 | +/// This information is printed as additional note(s) after the original diagnostic. |
| 282 | +private struct CallSite { |
| 283 | + enum Kind { |
| 284 | + // A regular function call. Not every function call in the call stack is printed in diagnostics. |
| 285 | + // This is only used if the original instruction doesn't have a location. |
| 286 | + case call |
| 287 | + |
| 288 | + // If the error is in a constructor, this is the place where the object/value is created. |
| 289 | + case constructorCall |
| 290 | + |
| 291 | + // If the error is in a specialized function, this is the place where the generic function is originally |
| 292 | + // specialized with concrete types. This is useful if a specialized type is relevant for the error. |
| 293 | + case specializedCall |
| 294 | + |
| 295 | + // If the error is in a protocol witness method, this is the place where the existential is created. |
| 296 | + case conformance |
| 297 | + } |
| 298 | + |
| 299 | + let location: Location |
| 300 | + let kind: Kind |
| 301 | + |
| 302 | + init(apply: ApplySite, callee: Function) { |
| 303 | + self.location = apply.location |
| 304 | + if let d = callee.location.decl, d is ConstructorDecl { |
| 305 | + self.kind = .constructorCall |
| 306 | + } else if callee.isSpecialization && !apply.parentFunction.isSpecialization { |
| 307 | + self.kind = .specializedCall |
| 308 | + } else { |
| 309 | + self.kind = .call |
| 310 | + } |
| 311 | + } |
| 312 | + |
| 313 | + init(location: Location, kind: Kind) { |
| 314 | + self.location = location |
| 315 | + self.kind = kind |
| 316 | + } |
| 317 | +} |
| 318 | + |
| 319 | +private extension Function { |
| 320 | + // The priority (1 = highest) which defines the order in which functions are checked. |
| 321 | + // This is important to get good caller information in diagnostics. |
| 322 | + var priority: Int { |
| 323 | + // There might be functions without a location, e.g. `swift_readAtKeyPath` generated by SILGen for keypaths. |
| 324 | + // It's okay to skip the ctor/dtor/method detection logic for those. |
| 325 | + if location.hasValidLineNumber { |
| 326 | + if let decl = location.decl { |
| 327 | + if decl is DestructorDecl || decl is ConstructorDecl { |
| 328 | + return 4 |
| 329 | + } |
| 330 | + if let parent = decl.parent, parent is ClassDecl { |
| 331 | + return 2 |
| 332 | + } |
| 333 | + } |
| 334 | + } |
| 335 | + if isPossiblyUsedExternally { |
| 336 | + return 1 |
| 337 | + } |
| 338 | + return 3 |
| 339 | + } |
| 340 | +} |
| 341 | + |
| 342 | +private extension AllocBoxInst { |
| 343 | + var allocsGenericValueTypeWithDeinit: Bool { |
| 344 | + type.getBoxFields(in: parentFunction).contains { $0.hasGenericValueDeinit(in: parentFunction) } |
| 345 | + } |
| 346 | +} |
| 347 | + |
| 348 | +private extension ApplySite { |
| 349 | + var callsEmbeddedRuntimeFunction: Bool { |
| 350 | + if let callee = referencedFunction, |
| 351 | + !callee.isDefinition, |
| 352 | + !callee.name.startsWith("$e") |
| 353 | + { |
| 354 | + return true |
| 355 | + } |
| 356 | + return false |
| 357 | + } |
| 358 | +} |
| 359 | + |
| 360 | +private extension Type { |
| 361 | + func hasGenericValueDeinit(in function: Function) -> Bool { |
| 362 | + guard isMoveOnly, let nominal = nominal else { |
| 363 | + return false |
| 364 | + } |
| 365 | + |
| 366 | + if nominal.isGenericAtAnyLevel && nominal.valueTypeDestructor != nil { |
| 367 | + return true |
| 368 | + } |
| 369 | + |
| 370 | + if isStruct { |
| 371 | + if let fields = getNominalFields(in: function) { |
| 372 | + return fields.contains { $0.hasGenericValueDeinit(in: function) } |
| 373 | + } |
| 374 | + } else if isEnum { |
| 375 | + if let enumCases = getEnumCases(in: function) { |
| 376 | + return enumCases.contains { $0.payload?.hasGenericValueDeinit(in: function) ?? false } |
| 377 | + } |
| 378 | + } |
| 379 | + return false |
| 380 | + } |
| 381 | +} |
0 commit comments