Skip to content

Commit 00fa647

Browse files
committed
LifetimeDependenceDiagnostics pass
Prototype diagnostic pass to bootstrap support for ~Escapable types and other lifetime dependence constructs.
1 parent ed1fd33 commit 00fa647

File tree

11 files changed

+503
-2
lines changed

11 files changed

+503
-2
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.scope.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) })

include/swift/AST/DiagnosticsSIL.def

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

935935
ERROR(lifetime_value_outside_scope, none,
936936
"lifetime-dependent value escapes its scope", ())
937+
937938
ERROR(vector_capacity_not_constant, none,
938939
"vector capacity needs to be constant", ())
939940

940941
ERROR(fixed_arrays_not_available, none,
941942
"fixed arrays are only available with -enable-experimental-feature FixedArrays", ())
942943

944+
ERROR(lifetime_variable_outside_scope, none,
945+
"lifetime-dependent variable '%0' escapes its scope", (Identifier))
946+
NOTE(lifetime_outside_scope_parent, none,
947+
"it depends on the lifetime of this parent value", ())
948+
NOTE(lifetime_outside_scope_use, none,
949+
"this use of the lifetime-dependent value is out of scope", ())
950+
NOTE(lifetime_outside_scope_escape, none,
951+
"this use causes the lifetime-dependent value to escape", ())
952+
943953
#define UNDEFINE_DIAGNOSTIC_MACROS
944954
#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: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ llvm::cl::opt<bool>
6969
EnableDeinitDevirtualizer("enable-deinit-devirtualizer", llvm::cl::init(false),
7070
llvm::cl::desc("Enable the DestroyHoisting pass."));
7171

72+
llvm::cl::opt<bool>
73+
DisableLifetimeDependenceDiagnostics(
74+
"disable-lifetime-dependence-diagnostics", llvm::cl::init(false),
75+
llvm::cl::desc("Disable lifetime dependence diagnostics."));
7276

7377
//===----------------------------------------------------------------------===//
7478
// Diagnostic Pass Pipeline
@@ -163,6 +167,10 @@ static void addMandatoryDiagnosticOptPipeline(SILPassPipelinePlan &P) {
163167
// Check noImplicitCopy and move only types for objects and addresses.
164168
P.addMoveOnlyChecker();
165169

170+
// Check ~Escapable.
171+
if (!DisableLifetimeDependenceDiagnostics) {
172+
P.addLifetimeDependenceDiagnostics();
173+
}
166174
if (EnableDeinitDevirtualizer)
167175
P.addDeinitDevirtualizer();
168176

test/SIL/explicit_lifetime_dependence_specifiers.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
// RUN: %target-swift-frontend %s -emit-sil -enable-builtin-module -enable-experimental-feature NonescapableTypes -disable-experimental-parser-round-trip -enable-experimental-feature NoncopyableGenerics | %FileCheck %s
1+
// RUN: %target-swift-frontend %s -emit-sil -enable-builtin-module \
2+
// RUN: -Xllvm -disable-lifetime-dependence-diagnostics \
3+
// RUN: -enable-experimental-feature NonescapableTypes \
4+
// RUN: -disable-experimental-parser-round-trip \
5+
// RUN: -enable-experimental-feature NoncopyableGenerics \
6+
// RUN: | %FileCheck %s
7+
28
// REQUIRES: asserts
39

410
import Builtin

test/SIL/implicit_lifetime_dependence.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
// RUN: %target-swift-frontend %s -emit-sil -enable-builtin-module -enable-experimental-feature NonescapableTypes -disable-experimental-parser-round-trip -enable-experimental-feature NoncopyableGenerics | %FileCheck %s
1+
// RUN: %target-swift-frontend %s -emit-sil -enable-builtin-module \
2+
// RUN: -Xllvm -disable-lifetime-dependence-diagnostics \
3+
// RUN: -enable-experimental-feature NonescapableTypes \
4+
// RUN: -disable-experimental-parser-round-trip \
5+
// RUN: -enable-experimental-feature NoncopyableGenerics \
6+
// RUN: | %FileCheck %s
7+
28
// REQUIRES: asserts
39

410
import Builtin

0 commit comments

Comments
 (0)