Skip to content

Commit 1e9af24

Browse files
authored
Merge pull request swiftlang#71209 from atrick/lifetime-diagnostics
LifetimeDependenceDiagnostics pass
2 parents 0eef51b + ddceffa commit 1e9af24

31 files changed

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

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) })

SwiftCompilerSources/Sources/Optimizer/Utilities/AddressUtils.swift

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,19 @@ protocol AddressUseVisitor {
3434
/// An access scope: begin_access, begin_apply, load_borrow.
3535
mutating func scopedAddressUse(of operand: Operand) -> WalkResult
3636

37+
/// end_access, end_apply, abort_apply, end_borrow.
38+
mutating func scopeEndingAddressUse(of operand: Operand) -> WalkResult
39+
3740
/// A address leaf use cannot propagate the address bits beyond the
3841
/// instruction.
3942
///
40-
/// An apply or builtin propagates an address into the callee, but
41-
/// it is considered a leaf use as long as the argument does not escape.
43+
/// StoringInstructions are leaf uses.
4244
mutating func leafAddressUse(of operand: Operand) -> WalkResult
4345

46+
/// An address used by an apply.
47+
mutating func appliedAddressUse(of operand: Operand, by apply: FullApplySite)
48+
-> WalkResult
49+
4450
/// A loaded address use propagates the value at the address.
4551
mutating func loadedAddressUse(of operand: Operand, into value: Value)
4652
-> WalkResult
@@ -67,10 +73,12 @@ extension AddressUseVisitor {
6773
/// protocol methods above.
6874
mutating func classifyAddress(operand: Operand) -> WalkResult {
6975
switch operand.instruction {
70-
case is BeginAccessInst, is BeginApplyInst, is LoadBorrowInst,
71-
is StoreBorrowInst:
76+
case is BeginAccessInst, is LoadBorrowInst, is StoreBorrowInst:
7277
return scopedAddressUse(of: operand)
7378

79+
case is EndAccessInst, is EndApplyInst, is AbortApplyInst, is EndBorrowInst:
80+
return scopeEndingAddressUse(of: operand)
81+
7482
case let markDep as MarkDependenceInst:
7583
if markDep.valueOperand == operand {
7684
return projectedAddressUse(of: operand, into: markDep)
@@ -81,7 +89,7 @@ extension AddressUseVisitor {
8189
if markDep.type.isAddress {
8290
return projectedAddressUse(of: operand, into: markDep)
8391
}
84-
if LifetimeDependence(markDependence: markDep, context) != nil {
92+
if LifetimeDependence(markDep, context) != nil {
8593
// This is unreachable from InteriorUseVisitor because the
8694
// base address of a `mark_dependence [nonescaping]` must be a
8795
// `begin_access`, and interior liveness does not check uses of
@@ -97,7 +105,7 @@ extension AddressUseVisitor {
97105
case let pai as PartialApplyInst where !pai.isOnStack:
98106
return escapingAddressUse(of: operand)
99107

100-
case is AddressToPointerInst:
108+
case is ReturnInst, is ThrowInst, is YieldInst, is AddressToPointerInst:
101109
return escapingAddressUse(of: operand)
102110

103111
case is StructElementAddrInst, is TupleElementAddrInst,
@@ -113,16 +121,18 @@ extension AddressUseVisitor {
113121
let svi = operand.instruction as! SingleValueInstruction
114122
return projectedAddressUse(of: operand, into: svi)
115123

116-
case is ReturnInst, is ThrowInst, is YieldInst, is TryApplyInst,
117-
is SwitchEnumAddrInst, is CheckedCastAddrBranchInst,
124+
case let apply as FullApplySite:
125+
return appliedAddressUse(of: operand, by: apply)
126+
127+
case is SwitchEnumAddrInst, is CheckedCastAddrBranchInst,
118128
is SelectEnumAddrInst, is InjectEnumAddrInst,
119129
is StoreInst, is StoreUnownedInst, is StoreWeakInst,
120130
is AssignInst, is AssignByWrapperInst, is AssignOrInitInst,
121131
is TupleAddrConstructorInst, is InitBlockStorageHeaderInst,
122132
is RetainValueAddrInst, is ReleaseValueAddrInst,
123133
is DestroyAddrInst, is DeallocStackInst,
124134
is DeinitExistentialAddrInst,
125-
is EndApplyInst, is IsUniqueInst, is MarkFunctionEscapeInst,
135+
is IsUniqueInst, is MarkFunctionEscapeInst,
126136
is PackElementSetInst:
127137
return leafAddressUse(of: operand)
128138

0 commit comments

Comments
 (0)