Skip to content

Commit 6c31eb0

Browse files
committed
embedded: rewrite the diagnostic pass for embedded swift
1. move embedded diagnostics out of the PerformanceDiagnostics pass. It was completely separated from the other logic in this pass, anyway. 2. rewrite it in swift 3. fix several bugs, that means: missed diagnostics, which led to IRGen crashes * look at all methods in witness tables, including base protocols and associated conformances * visit all functions in the call tree, including generic functions with class bound generic arguments * handle all instructions, e.g. concurrency builtins 4. improve error messages by adding meaningful call-site information. For example: * if the error is in a specialized function, report where the generic function is originally specialized with concrete types * if the error is in a protocol witness method, report where the existential is created
1 parent d222cf2 commit 6c31eb0

File tree

15 files changed

+445
-270
lines changed

15 files changed

+445
-270
lines changed

SwiftCompilerSources/Sources/Optimizer/ModulePasses/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
swift_compiler_sources(Optimizer
1010
DiagnoseUnknownConstValues.swift
11+
EmbeddedSwiftDiagnostics.swift
1112
MandatoryPerformanceOptimizations.swift
1213
ReadOnlyGlobalVariables.swift
1314
StackProtection.swift
Lines changed: 381 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,381 @@
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+
}

SwiftCompilerSources/Sources/Optimizer/PassManager/PassRegistration.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ private func registerSwiftPasses() {
6767
registerPass(diagnoseUnknownConstValues, { diagnoseUnknownConstValues.run($0)})
6868
registerPass(readOnlyGlobalVariablesPass, { readOnlyGlobalVariablesPass.run($0) })
6969
registerPass(stackProtection, { stackProtection.run($0) })
70+
registerPass(embeddedSwiftDiagnostics, { embeddedSwiftDiagnostics.run($0) })
7071

7172
// Function passes
7273
registerPass(asyncDemotion, { asyncDemotion.run($0) })

0 commit comments

Comments
 (0)