Skip to content

Commit 4c78ece

Browse files
committed
LifetimeDependence: clarify diagnostics for many unusual cases.
Ensure that we always issue a diagnostic on error, but avoid emitting any notes that don't have source locations. With implicit accessors and thunks, report the correct line number and indicate which accessor generates the error. Always check for debug_value users. Consistently handle access scopes across diagnostic analysis and diagnostic messages. (cherry picked from commit ec51286)
1 parent 9b9a405 commit 4c78ece

File tree

7 files changed

+210
-82
lines changed

7 files changed

+210
-82
lines changed

SwiftCompilerSources/Sources/Optimizer/FunctionPasses/LifetimeDependenceDiagnostics.swift

Lines changed: 72 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -238,37 +238,51 @@ private struct DiagnoseDependence {
238238

239239
// Identify the escaping variable.
240240
let escapingVar = LifetimeVariable(dependent: operand.value, context)
241-
let varName = escapingVar.name
242-
if let varName {
243-
diagnose(escapingVar.sourceLoc, .lifetime_variable_outside_scope,
244-
varName)
241+
if let varDecl = escapingVar.varDecl {
242+
// Use the variable location, not the access location.
243+
diagnose(varDecl.nameLoc, .lifetime_variable_outside_scope, escapingVar.name ?? "")
244+
} else if let sourceLoc = escapingVar.sourceLoc {
245+
diagnose(sourceLoc, .lifetime_value_outside_scope)
245246
} else {
246-
diagnose(escapingVar.sourceLoc, .lifetime_value_outside_scope)
247+
// Always raise an error even if we can't find a source location.
248+
let sourceLoc = function.location.sourceLoc
249+
if let accessorKind = escapingVar.accessorKind {
250+
diagnose(sourceLoc, .lifetime_value_outside_accessor, accessorKind)
251+
} else {
252+
// Thunks do not have a source location, but we try to use the function location anyway.
253+
let thunkSelect = dependence.function.thunkKind == .noThunk ? 0 : 1
254+
diagnose(sourceLoc, .lifetime_value_outside_thunk, thunkSelect, function.name)
255+
}
247256
}
248257
reportScope()
249258
// Identify the use point.
250-
let userSourceLoc = operand.instruction.location.sourceLoc
251-
diagnose(userSourceLoc, diagID)
259+
if let userSourceLoc = operand.instruction.location.sourceLoc {
260+
diagnose(userSourceLoc, diagID)
261+
}
252262
}
253263

254-
// Identify the dependence scope.
264+
// Identify the dependence scope. If no source location is found, bypass this diagnostic.
255265
func reportScope() {
256-
if case let .access(beginAccess) = dependence.scope {
257-
let parentVar = LifetimeVariable(dependent: beginAccess, context)
258-
if let sourceLoc = beginAccess.location.sourceLoc ?? parentVar.sourceLoc {
259-
diagnose(sourceLoc, .lifetime_outside_scope_access,
260-
parentVar.name ?? "")
261-
}
266+
let parentVar = LifetimeVariable(dependent: dependence.parentValue, context)
267+
// First check if the dependency is limited to an access scope. If the access has no source location then
268+
// fall-through to report possible dependence on an argument.
269+
if parentVar.isAccessScope, let accessLoc = parentVar.sourceLoc {
270+
diagnose(accessLoc, .lifetime_outside_scope_access, parentVar.name ?? "")
262271
return
263272
}
264-
if let arg = dependence.parentValue as? Argument,
265-
let varDecl = arg.varDecl,
266-
let sourceLoc = arg.sourceLoc {
267-
diagnose(sourceLoc, .lifetime_outside_scope_argument,
268-
varDecl.userFacingName)
273+
// If the argument does not have a source location (e.g. a synthesized accessor), report the function location. The
274+
// function's source location is sufficient for argument diagnostics, but if the function has no location, don't
275+
// report any scope.
276+
if parentVar.isArgument, let argLoc = parentVar.sourceLoc ?? function.location.sourceLoc {
277+
if let parentName = parentVar.name {
278+
diagnose(argLoc, .lifetime_outside_scope_argument, parentName)
279+
} else {
280+
diagnose(argLoc, .lifetime_outside_scope_synthesized_argument, parentVar.accessorKind ?? function.name)
281+
}
269282
return
270283
}
271-
let parentVar = LifetimeVariable(dependent: dependence.parentValue, context)
284+
// Now diagnose dependencies on regular variable and value scopes.
285+
// Thunks do not have a function location, so any scopes inside the thunk will be ignored.
272286
if let parentLoc = parentVar.sourceLoc {
273287
if let parentName = parentVar.name {
274288
diagnose(parentLoc, .lifetime_outside_scope_variable, parentName)
@@ -282,24 +296,34 @@ private struct DiagnoseDependence {
282296
// Identify a best-effort variable declaration based on a defining SIL
283297
// value or any lifetime dependent use of that SIL value.
284298
private struct LifetimeVariable {
285-
var varDecl: VarDecl?
286-
var sourceLoc: SourceLoc?
299+
var varDecl: VarDecl? = nil
300+
var sourceLoc: SourceLoc? = nil
301+
var isArgument: Bool = false
302+
var isAccessScope: Bool = false
303+
var accessorKind: String?
304+
var thunkKind: Function.ThunkKind = .noThunk
287305

288306
var name: StringRef? {
289307
return varDecl?.userFacingName
290308
}
291309

292310
init(dependent value: Value, _ context: some Context) {
293-
if value.type.isAddress {
294-
self = Self(accessBase: value.accessBase, context)
311+
guard let introducer = getFirstVariableIntroducer(of: value, context) else {
295312
return
296313
}
297-
if let firstIntroducer = getFirstVariableIntroducer(of: value, context) {
298-
self = Self(introducer: firstIntroducer)
314+
if introducer.type.isAddress {
315+
if let beginAccess = introducer as? BeginAccessInst {
316+
// Recurse through beginAccess to find the variable introducer rather than the variable access.
317+
self = .init(dependent: beginAccess.address, context)
318+
self.isAccessScope = true
319+
// However, remember source location of the innermost access.
320+
self.sourceLoc = beginAccess.location.sourceLoc ?? self.sourceLoc
321+
return
322+
}
323+
self = .init(accessBase: introducer.accessBase, context)
299324
return
300325
}
301-
self.varDecl = nil
302-
self.sourceLoc = nil
326+
self = Self(introducer: introducer, context)
303327
}
304328

305329
private func getFirstVariableIntroducer(of value: Value, _ context: some Context) -> Value? {
@@ -313,15 +337,21 @@ private struct LifetimeVariable {
313337
return introducer
314338
}
315339

316-
private init(introducer: Value) {
340+
private init(introducer: Value, _ context: some Context) {
317341
if let arg = introducer as? Argument {
318342
self.varDecl = arg.varDecl
319-
} else {
320-
self.sourceLoc = introducer.definingInstruction?.location.sourceLoc
321-
self.varDecl = introducer.definingInstruction?.findVarDecl()
343+
self.sourceLoc = arg.sourceLoc
344+
self.isArgument = true
345+
return
322346
}
323-
if let varDecl {
324-
sourceLoc = varDecl.nameLoc
347+
if let varDecl = introducer.definingInstruction?.findVarDecl() {
348+
self.varDecl = varDecl
349+
self.sourceLoc = varDecl.nameLoc
350+
} else if let sourceLoc = introducer.definingInstruction?.location.sourceLoc {
351+
self.sourceLoc = sourceLoc
352+
} else {
353+
self.accessorKind = introducer.parentFunction.accessorKindName
354+
self.thunkKind = introducer.parentFunction.thunkKind
325355
}
326356
}
327357

@@ -335,32 +365,27 @@ private struct LifetimeVariable {
335365
// never be produced by one of these, except when it is redundant with the `alloc_box` VarDecl. It does not seem
336366
// possible for a box to be moved/borrowed directly into another variable's box. Reassignment always loads/stores
337367
// the value.
338-
self = Self(introducer: projectBox.box.referenceRoot)
368+
self = .init(introducer: projectBox.box.referenceRoot, context)
339369
case .stack(let allocStack):
340-
self = Self(introducer: allocStack)
370+
self = .init(introducer: allocStack, context)
341371
case .global(let globalVar):
342372
self.varDecl = globalVar.varDecl
343373
self.sourceLoc = varDecl?.nameLoc
344374
case .class(let refAddr):
345-
self.varDecl = refAddr.varDecl
346-
self.sourceLoc = refAddr.location.sourceLoc
375+
self = .init(introducer: refAddr, context)
347376
case .tail(let refTail):
348-
self = Self(introducer: refTail.instance)
377+
self = .init(introducer: refTail.instance, context)
349378
case .argument(let arg):
350-
self.varDecl = arg.varDecl
351-
self.sourceLoc = arg.sourceLoc
379+
self = .init(introducer: arg, context)
352380
case .yield(let result):
353381
// TODO: bridge VarDecl for FunctionConvention.Yields
354-
self.varDecl = nil
355-
self.sourceLoc = result.parentInstruction.location.sourceLoc
382+
self = .init(introducer: result, context)
356383
case .storeBorrow(let sb):
357384
self = .init(dependent: sb.source, context)
358385
case .pointer(let ptrToAddr):
359-
self.varDecl = nil
360-
self.sourceLoc = ptrToAddr.location.sourceLoc
386+
self = .init(introducer: ptrToAddr, context)
361387
case .index, .unidentified:
362-
self.varDecl = nil
363-
self.sourceLoc = nil
388+
break
364389
}
365390
}
366391
}

SwiftCompilerSources/Sources/Optimizer/FunctionPasses/LifetimeDependenceInsertion.swift

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -300,8 +300,8 @@ private func insertMarkDependencies(value: Value, initializer: Instruction?,
300300
}
301301
}
302302

303-
/// Walk up the value dependence chain to find the best-effort variable declaration. Typically called while diagnosing
304-
/// an error.
303+
/// Walk up the value dependence chain to find the best-effort variable declaration. Used to find the source of a borrow
304+
/// dependence or to print the source variable in a diagnostic message.
305305
///
306306
/// Returns an array with at least one introducer value.
307307
///
@@ -440,11 +440,12 @@ struct VariableIntroducerUseDefWalker : LifetimeDependenceUseDefValueWalker, Lif
440440

441441
/// Override to check for on-stack variables before following an initializer.
442442
mutating func walkUp(address: Value, access: AccessBaseAndScopes) -> WalkResult {
443-
// Check for stack locations that correspond to an lvalue.
444-
if case let .stack(allocStack) = access.base {
445-
if allocStack.varDecl != nil {
446-
// Report this variable's innermmost access scope.
447-
return addressIntroducer(access.enclosingAccess.address ?? address, access: access)
443+
// Check for stack locations that correspond to an lvalue if there isn't any nested access scope.
444+
if access.innermostAccess == nil {
445+
if case let .stack(allocStack) = access.base {
446+
if allocStack.varDecl != nil {
447+
return addressIntroducer(allocStack, access: access)
448+
}
448449
}
449450
}
450451
return walkUpDefault(address: address, access: access)

SwiftCompilerSources/Sources/Optimizer/Utilities/LifetimeDependenceUtils.swift

Lines changed: 19 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1221,6 +1221,24 @@ extension LifetimeDependenceUseDefAddressWalker {
12211221
}
12221222

12231223
mutating func walkUpDefault(address: Value, access: AccessBaseAndScopes) -> WalkResult {
1224+
if let beginAccess = access.innermostAccess {
1225+
// Skip the access scope for unsafe[Mutable]Address. Treat it like a projection of 'self' rather than a separate
1226+
// variable access.
1227+
if let addressorSelf = beginAccess.unsafeAddressorSelf {
1228+
return walkUp(newLifetime: addressorSelf)
1229+
}
1230+
// Ignore the acces scope for trivial values regardless of whether it is singly-initialized. Trivial values do not
1231+
// need to be kept alive in memory and can be safely be overwritten in the same scope. Lifetime dependence only
1232+
// cares that the loaded value is within the lexical scope of the trivial value's variable declaration. Rather
1233+
// than skipping all access scopes, call 'walkUp' on each nested access in case one of them needs to redirect the
1234+
// walk, as required for 'access.unsafeAddressorSelf' or in case the implementation overrides certain accesses.
1235+
if isTrivialScope {
1236+
return walkUp(newLifetime: beginAccess.address)
1237+
}
1238+
// Generally assume an access scope introduces a variable borrow scope. And generally ignore address forwarding
1239+
// mark_dependence.
1240+
return addressIntroducer(beginAccess, access: access)
1241+
}
12241242
// Continue walking for some kinds of access base.
12251243
switch access.base {
12261244
case .box, .global, .class, .tail, .pointer, .index, .unidentified:
@@ -1229,14 +1247,9 @@ extension LifetimeDependenceUseDefAddressWalker {
12291247
// Ignore stack locations. Their access scopes do not affect lifetime dependence.
12301248
return walkUp(stackInitializer: allocStack, at: address, access: access)
12311249
case let .argument(arg):
1232-
// Ignore access scopes for @in or @in_guaranteed arguments when all scopes are reads. Do not ignore a [read]
1233-
// access of an inout argument or outer [modify]. Mutation later with the outer scope could invalidate the
1234-
// borrowed state in this narrow scope. Do not ignore any mark_depedence on the address.
1235-
if arg.convention.isIndirectIn && access.isOnlyReadAccess {
1250+
if arg.convention.isExclusiveIndirect {
12361251
return addressIntroducer(arg, access: access)
12371252
}
1238-
// @inout arguments may be singly initialized (when no modification exists in this function), but this is not
1239-
// relevant here because they require nested access scopes which can never be ignored.
12401253
case let .yield(yieldedAddress):
12411254
// Ignore access scopes for @in or @in_guaranteed yields when all scopes are reads.
12421255
let apply = yieldedAddress.definingInstruction as! FullApplySite
@@ -1250,27 +1263,6 @@ extension LifetimeDependenceUseDefAddressWalker {
12501263
return walkUp(newLifetime: sb.source)
12511264
}
12521265
}
1253-
// Skip the access scope for unsafe[Mutable]Address. Treat it like a projection of 'self' rather than a separate
1254-
// variable access.
1255-
if case let .access(innerAccess) = access.scopes.first,
1256-
let addressorSelf = innerAccess.unsafeAddressorSelf {
1257-
return walkUp(newLifetime: addressorSelf)
1258-
}
1259-
// Ignore the acces scope for trivial values regardless of whether it is singly-initialized. Trivial values do not
1260-
// need to be kept alive in memory and can be safely be overwritten in the same scope. Lifetime dependence only
1261-
// cares that the loaded value is within the lexical scope of the trivial value's variable declaration. Rather than
1262-
// skipping all access scopes, call 'walkUp' on each nested access in case one of them needs to redirect the walk,
1263-
// as required for 'access.unsafeAddressorSelf'.
1264-
if isTrivialScope {
1265-
switch access.scopes.first {
1266-
case .none, .base:
1267-
break
1268-
case let .access(beginAccess):
1269-
return walkUp(newLifetime: beginAccess.address)
1270-
case let .dependence(markDep):
1271-
return walkUp(newLifetime: markDep.value)
1272-
}
1273-
}
12741266
return addressIntroducer(access.enclosingAccess.address ?? address, access: access)
12751267
}
12761268

include/swift/AST/DiagnosticsSIL.def

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1157,8 +1157,14 @@ ERROR(lifetime_variable_outside_scope, none,
11571157
"lifetime-dependent variable '%0' escapes its scope", (Identifier))
11581158
ERROR(lifetime_value_outside_scope, none,
11591159
"lifetime-dependent value escapes its scope", ())
1160+
ERROR(lifetime_value_outside_accessor, none,
1161+
"lifetime-dependent value returned by generated accessor '%0'", (StringRef))
1162+
ERROR(lifetime_value_outside_thunk, none,
1163+
"lifetime-dependent value returned by generated %select{function|thunk}0 '%1'", (bool, StringRef))
11601164
NOTE(lifetime_outside_scope_argument, none,
11611165
"it depends on the lifetime of argument '%0'", (Identifier))
1166+
NOTE(lifetime_outside_scope_synthesized_argument, none,
1167+
"it depends on the lifetime of an argument of '%0'", (Identifier))
11621168
NOTE(lifetime_outside_scope_access, none,
11631169
"it depends on this scoped access to variable '%0'", (Identifier))
11641170
NOTE(lifetime_outside_scope_variable, none,

test/SILOptimizer/lifetime_dependence/verify_diagnostics.sil

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ bb0(%0 : @guaranteed $NE):
176176
%2 = function_ref @reborrowNE : $@convention(thin) (@guaranteed NE) -> @lifetime(borrow 0) @owned NE
177177
%3 = apply %2(%0) : $@convention(thin) (@guaranteed NE) -> @lifetime(borrow 0) @owned NE
178178
// expected-error @-1{{lifetime-dependent value escapes its scope}}
179+
// expected-note @-6{{it depends on the lifetime of an argument of 'testReborrow'}}
179180
%4 = mark_dependence [unresolved] %3 on %0
180181
return %4 // expected-note {{this use causes the lifetime-dependent value to escape}}
181182
}
@@ -185,6 +186,7 @@ bb0(%0 : @guaranteed $NE):
185186
%2 = function_ref @reborrowNE : $@convention(thin) (@guaranteed NE) -> @lifetime(borrow 0) @owned NE
186187
%3 = apply %2(%0) : $@convention(thin) (@guaranteed NE) -> @lifetime(borrow 0) @owned NE
187188
// expected-error @-1{{lifetime-dependent value escapes its scope}}
189+
// expected-note @-5{{it depends on the lifetime of an argument of 'testBorrowValue'}}
188190
%4 = mark_dependence [unresolved] %3 on %0
189191
return %4 // expected-note {{this use causes the lifetime-dependent value to escape}}
190192
}
@@ -229,6 +231,7 @@ bb0(%0 : $*NE, %1 : $*Holder):
229231
%5 = alloc_stack $Holder
230232
%6 = store_borrow %4 to %5
231233
%7 = alloc_stack $NE // expected-error {{lifetime-dependent value escapes its scope}}
234+
// expected-note @-8{{it depends on the lifetime of an argument of 'testCopyInit'}}
232235

233236
%8 = function_ref @getNEIndirect : $@convention(thin) (@in_guaranteed Holder) -> @lifetime(borrow address_for_deps 0) @out NE
234237
%9 = apply %8(%7, %6) : $@convention(thin) (@in_guaranteed Holder) -> @lifetime(borrow address_for_deps 0) @out NE

test/SILOptimizer/lifetime_dependence/verify_diagnostics.swift

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,15 @@ struct A {}
3636

3737
func useA(_:A){}
3838

39-
struct NE : ~Escapable {}
39+
public struct NE : ~Escapable {}
40+
41+
public struct NEImmortal: ~Escapable {
42+
@lifetime(immortal)
43+
public init() {}
44+
}
4045

4146
class C {}
47+
4248
struct Holder {
4349
var c: C? = nil
4450
}
@@ -91,3 +97,30 @@ public func testGenericDep<T: ~Escapable>(type: T.Type) -> T {
9197
return result
9298
// expected-note @-1{{this use causes the lifetime-dependent value to escape}}
9399
}
100+
101+
// Test diagnostics on implicit accessors.
102+
public struct ImplicitAccessors {
103+
let c: C
104+
105+
public var neComputed: NEImmortal {
106+
get {
107+
NEImmortal()
108+
}
109+
set {
110+
}
111+
}
112+
}
113+
114+
public struct NoncopyableImplicitAccessors : ~Copyable & ~Escapable {
115+
public var ne: NE
116+
117+
public var neComputedBorrow: NE {
118+
@lifetime(borrow self)
119+
get { ne }
120+
121+
@lifetime(&self)
122+
set {
123+
ne = newValue
124+
}
125+
}
126+
}

0 commit comments

Comments
 (0)