Skip to content

Commit 645d84f

Browse files
committed
LifetimeDependenceDiagnostics pass
Prototype diagnostic pass to bootstrap support for ~Escapable types and other lifetime dependence constructs.
1 parent 21f4163 commit 645d84f

File tree

8 files changed

+514
-0
lines changed

8 files changed

+514
-0
lines changed

SwiftCompilerSources/Sources/Optimizer/FunctionPasses/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ swift_compiler_sources(Optimizer
1818
DeinitDevirtualizer.swift
1919
InitializeStaticGlobals.swift
2020
LetPropertyLowering.swift
21+
LifetimeDependenceDiagnostics.swift
2122
ObjectOutliner.swift
2223
ObjCBridgingOptimization.swift
2324
MergeCondFails.swift
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
//===--- LifetimeDependenceDiagnostics.swift - Lifetime dependence --------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 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+
15+
private let verbose = false
16+
17+
private func log(_ message: @autoclosure () -> String) {
18+
if verbose {
19+
print("### \(message())")
20+
}
21+
}
22+
23+
/// Diagnostic pass.
24+
///
25+
/// Find the roots of all non-escapable values in this function. All
26+
/// non-escapable values either depend on a NonEscapingScope, or they
27+
/// are produced by a LifetimeDependentInstruction that has no
28+
/// dependence on a parent value (@_unsafeNonEscapableResult).
29+
let lifetimeDependenceDiagnosticsPass = FunctionPass(
30+
name: "lifetime-dependence-diagnostics")
31+
{ (function: Function, context: FunctionPassContext) in
32+
if !context.options.hasFeature(.NonescapableTypes) {
33+
return
34+
}
35+
log("Diagnosing lifetime dependence in \(function.name)")
36+
37+
for argument in function.arguments where !argument.type.isEscapable {
38+
let lifetimeDep = LifetimeDependence(argument, context)
39+
analyze(dependence: lifetimeDep, context)
40+
}
41+
for instruction in function.instructions {
42+
guard let markDep = instruction as? MarkDependenceInst else { continue }
43+
if let lifetimeDep = LifetimeDependence(markDep, context) {
44+
analyze(dependence: lifetimeDep, context)
45+
}
46+
}
47+
}
48+
49+
/// Analyze a single Lifetime dependence and trigger diagnostics.
50+
///
51+
/// 1. Compute the LifetimeDependence scope.
52+
///
53+
/// 2. Walk down all dependent values checking that they are within range.
54+
private func analyze(dependence: LifetimeDependence,
55+
_ context: FunctionPassContext) {
56+
log("Diagnosing lifetime dependence: \(dependence)")
57+
58+
// Compute this dependence scope.
59+
var range = dependence.scope.computeRange(context)
60+
defer { range?.deinitialize() }
61+
62+
let diagnostics =
63+
DiagnoseDependence(dependence: dependence, range: range, context: context)
64+
65+
// Check each lifetime-dependent use via a def-use visitor
66+
var walker = DiagnoseDependenceWalker(diagnostics, context)
67+
defer { walker.deinitialize() }
68+
let result = walker.walkDown(root: dependence.parentValue)
69+
70+
// The walker overrides anything that might return .abortWalk in
71+
// order to print a detailed SIL crash report.
72+
assert(result == .continueWalk,
73+
"unimplemented lifetime dependence check")
74+
}
75+
76+
/// Analyze and diagnose a single LifetimeDependence.
77+
private struct DiagnoseDependence {
78+
let dependence: LifetimeDependence
79+
let range: InstructionRange?
80+
let context: FunctionPassContext
81+
82+
var function: Function { dependence.function }
83+
84+
/// Check that this use is inside the dependence scope.
85+
func checkInScope(operand: Operand) {
86+
if let range, !range.inclusiveRangeContains(operand.instruction) {
87+
reportError(operand: operand, diagID: .lifetime_outside_scope_use)
88+
return
89+
}
90+
log("Dependence scope contains: \(operand.instruction)")
91+
}
92+
93+
func reportEscaping(operand: Operand) {
94+
reportError(operand: operand, diagID: .lifetime_outside_scope_escape)
95+
}
96+
97+
func reportUnknown(operand: Operand) {
98+
standardError.write("Unknown use: \(operand)\n\(function)")
99+
reportEscaping(operand: operand)
100+
}
101+
102+
func checkFunctionResult(operand: Operand) {
103+
// TODO: Get the argument dependence for this result. Check that it is the
104+
// same as the current dependence scope
105+
106+
// TODO: Take ResultInfo as an argument and provide better
107+
// diagnostics for missing lifetime dependencies.
108+
reportEscaping(operand: operand)
109+
}
110+
111+
func reportError(operand: Operand, diagID: DiagID) {
112+
// Identify the escaping variable.
113+
let escapingVar = LifetimeVariable(dependent: operand.value, context)
114+
let varName = escapingVar.name
115+
if let varName {
116+
context.diagnosticEngine.diagnose(escapingVar.sourceLoc,
117+
.lifetime_variable_outside_scope,
118+
varName)
119+
} else {
120+
context.diagnosticEngine.diagnose(escapingVar.sourceLoc,
121+
.lifetime_value_outside_scope)
122+
}
123+
// Identify the dependence scope.
124+
//
125+
// TODO: add bridging for function argument locations
126+
// [SILArgument.getDecl().getLoc()]
127+
//
128+
// TODO: For clear diagnostics: switch on dependence.scope.
129+
// For an access, report both the accessed variable, and the access.
130+
let parentSourceLoc =
131+
dependence.parentValue.definingInstruction?.location.sourceLoc
132+
context.diagnosticEngine.diagnose(parentSourceLoc,
133+
.lifetime_outside_scope_parent)
134+
135+
// Identify the use point.
136+
let userSourceLoc = operand.instruction.location.sourceLoc
137+
context.diagnosticEngine.diagnose(userSourceLoc, diagID)
138+
}
139+
}
140+
141+
private extension Instruction {
142+
func findVarDecl() -> VarDecl? {
143+
if let varDeclInst = self as? VarDeclInstruction {
144+
return varDeclInst.varDecl
145+
}
146+
for result in results {
147+
for use in result.uses {
148+
if let debugVal = use.instruction as? DebugValueInst {
149+
return debugVal.varDecl
150+
}
151+
}
152+
}
153+
return nil
154+
}
155+
}
156+
157+
// Identify a best-effort variable declaration based on a defining SIL
158+
// value or any lifetime dependent use of that SIL value.
159+
private struct LifetimeVariable {
160+
var varDecl: VarDecl?
161+
var sourceLoc: SourceLoc?
162+
163+
var name: String? {
164+
return varDecl?.userFacingName
165+
}
166+
167+
init(introducer: Value) {
168+
if introducer.type.isAddress {
169+
switch introducer.enclosingAccessScope {
170+
case let .scope(beginAccess):
171+
// TODO: report both the access point and original variable.
172+
self = LifetimeVariable(introducer: beginAccess.operand.value)
173+
return
174+
case .base(_):
175+
// TODO: use an address walker to get the allocation point.
176+
break
177+
}
178+
}
179+
// TODO: handle function arguments
180+
self.sourceLoc = introducer.definingInstruction?.location.sourceLoc
181+
self.varDecl = introducer.definingInstruction?.findVarDecl()
182+
if let varDecl {
183+
sourceLoc = varDecl.sourceLoc
184+
}
185+
}
186+
187+
init(dependent value: Value, _ context: Context) {
188+
// TODO: consider diagnosing multiple variable introducers. It's
189+
// unclear how more than one can happen.
190+
var introducers = Stack<Value>(context)
191+
gatherBorrowIntroducers(for: value, in: &introducers, context)
192+
if let firstIntroducer = introducers.pop() {
193+
self = LifetimeVariable(introducer: firstIntroducer)
194+
return
195+
}
196+
self.varDecl = nil
197+
self.sourceLoc = nil
198+
}
199+
}
200+
201+
/// Walk down lifetime depenence uses. For each check that all dependent
202+
/// leaf uses are non-escaping and within the dependence scope. The walk
203+
/// starts with add address for .access dependencies. The walk can
204+
/// transition from an address to a value at a load. The walk can
205+
/// transition from a value to an address as follows:
206+
///
207+
/// %dependent_addr = mark_dependence [nonescaping] %base_addr on %value
208+
///
209+
/// TODO: handle stores to singly initialized temporaries like copies using a standard reaching-def analysis.
210+
private struct DiagnoseDependenceWalker {
211+
let diagnostics: DiagnoseDependence
212+
let context: Context
213+
var visitedValues: ValueSet
214+
215+
var function: Function { diagnostics.function }
216+
217+
init(_ diagnostics: DiagnoseDependence, _ context: Context) {
218+
self.diagnostics = diagnostics
219+
self.context = context
220+
self.visitedValues = ValueSet(context)
221+
}
222+
223+
mutating func deinitialize() {
224+
visitedValues.deinitialize()
225+
}
226+
}
227+
228+
extension DiagnoseDependenceWalker : LifetimeDependenceDefUseWalker {
229+
mutating func needWalk(for value: Value) -> Bool {
230+
visitedValues.insert(value)
231+
}
232+
233+
mutating func leafUse(of operand: Operand) -> WalkResult {
234+
diagnostics.checkInScope(operand: operand)
235+
return .continueWalk
236+
}
237+
238+
mutating func deadValue(_ value: Value, using operand: Operand?)
239+
-> WalkResult {
240+
// Ignore a dead root value. It never escapes.
241+
if let operand {
242+
diagnostics.checkInScope(operand: operand)
243+
}
244+
return .continueWalk
245+
}
246+
247+
mutating func escapingDependence(on operand: Operand) -> WalkResult {
248+
if operand.instruction is ReturnInst {
249+
} else {
250+
diagnostics.reportEscaping(operand: operand)
251+
}
252+
return .continueWalk
253+
}
254+
255+
mutating func returnedDependence(result: Operand) -> WalkResult {
256+
diagnostics.checkFunctionResult(operand: result)
257+
return .continueWalk
258+
}
259+
260+
mutating func returnedDependence(address: FunctionArgument,
261+
using operand: Operand) -> WalkResult {
262+
diagnostics.checkFunctionResult(operand: operand)
263+
return .continueWalk
264+
}
265+
266+
// Override AddressUseVisitor here because LifetimeDependenceDefUseWalker
267+
// returns .abortWalk, and we want a more useful crash report.
268+
mutating func unknownAddressUse(of operand: Operand) -> WalkResult {
269+
diagnostics.reportUnknown(operand: operand)
270+
return .continueWalk
271+
}
272+
}

SwiftCompilerSources/Sources/Optimizer/PassManager/PassRegistration.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ private func registerSwiftPasses() {
8989
registerPass(redundantLoadElimination, { redundantLoadElimination.run($0) })
9090
registerPass(earlyRedundantLoadElimination, { earlyRedundantLoadElimination.run($0) })
9191
registerPass(deinitDevirtualizer, { deinitDevirtualizer.run($0) })
92+
registerPass(lifetimeDependenceDiagnosticsPass, { lifetimeDependenceDiagnosticsPass.run($0) })
9293

9394
// Instruction passes
9495
registerForSILCombine(BeginCOWMutationInst.self, { run(BeginCOWMutationInst.self, $0) })

include/swift/AST/DiagnosticsSIL.def

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -916,11 +916,21 @@ ERROR(deinit_not_visible, none,
916916

917917
ERROR(lifetime_value_outside_scope, none,
918918
"lifetime-dependent value escapes its scope", ())
919+
919920
ERROR(vector_capacity_not_constant, none,
920921
"vector capacity needs to be constant", ())
921922

922923
ERROR(fixed_arrays_not_available, none,
923924
"fixed arrays are only available with -enable-experimental-feature FixedArrays", ())
924925

926+
ERROR(lifetime_variable_outside_scope, none,
927+
"lifetime-dependent variable '%0' escapes its scope", (Identifier))
928+
NOTE(lifetime_outside_scope_parent, none,
929+
"it depends on the lifetime of this parent value", ())
930+
NOTE(lifetime_outside_scope_use, none,
931+
"this use of the lifetime-dependent value is out of scope", ())
932+
NOTE(lifetime_outside_scope_escape, none,
933+
"this use causes the lifetime-dependent value to escape", ())
934+
925935
#define UNDEFINE_DIAGNOSTIC_MACROS
926936
#include "DefineDiagnosticMacros.h"

include/swift/SILOptimizer/PassManager/Passes.def

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,9 @@ PASS(LateCodeMotion, "late-codemotion",
284284
"Late Code Motion with Release Hoisting")
285285
PASS(LateDeadFunctionAndGlobalElimination, "late-deadfuncelim",
286286
"Late Dead Function and Global Elimination")
287+
SWIFT_FUNCTION_PASS(LifetimeDependenceDiagnostics,
288+
"lifetime-dependence-diagnostics",
289+
"Diagnose Lifetime Dependence")
287290
PASS(LoopCanonicalizer, "loop-canonicalizer",
288291
"Loop Canonicalization")
289292
PASS(LoopInfoPrinter, "loop-info-printer",

lib/SILOptimizer/PassManager/PassPipeline.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ static void addDefiniteInitialization(SILPassPipelinePlan &P) {
116116
static void addMandatoryDiagnosticOptPipeline(SILPassPipelinePlan &P) {
117117
P.startPipeline("Mandatory Diagnostic Passes + Enabling Optimization Passes");
118118
P.addDiagnoseInvalidEscapingCaptures();
119+
P.addLifetimeDependenceDiagnostics();
119120
P.addReferenceBindingTransform();
120121
P.addDiagnoseStaticExclusivity();
121122
P.addNestedSemanticFunctionCheck();

0 commit comments

Comments
 (0)