Skip to content

Fix LifetimeDependenceDiagnostics: scoped dependence on a copy #81105

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Apr 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ let lifetimeDependenceDiagnosticsPass = FunctionPass(
}
}
for instruction in function.instructions {
if let markDep = instruction as? MarkDependenceInst, markDep.isUnresolved {
if let markDep = instruction as? MarkDependenceInstruction, markDep.isUnresolved {
if let lifetimeDep = LifetimeDependence(markDep, context) {
if analyze(dependence: lifetimeDep, context) {
// Note: This promotes the mark_dependence flag but does not invalidate analyses; preserving analyses is good,
Expand Down Expand Up @@ -202,12 +202,29 @@ private struct DiagnoseDependence {
// Check that the parameter dependence for this result is the same
// as the current dependence scope.
if let arg = dependence.scope.parentValue as? FunctionArgument,
function.argumentConventions[resultDependsOn: arg.index] != nil {
// The returned value depends on a lifetime that is inherited or
// borrowed in the caller. The lifetime of the argument value
// itself is irrelevant here.
log(" has dependent function result")
return .continueWalk
let argDep = function.argumentConventions[resultDependsOn: arg.index] {
switch argDep {
case .inherit:
if dependence.markDepInst != nil {
// A mark_dependence represents a "borrow" scope. A local borrow scope cannot inherit the caller's dependence
// because the borrow scope depends on the argument value itself, while the caller allows the result to depend
// on a value that the argument was copied from.
break
}
fallthrough
case .scope:
// The returned value depends on a lifetime that is inherited or
// borrowed in the caller. The lifetime of the argument value
// itself is irrelevant here.
log(" has dependent function result")
return .continueWalk
}
// Briefly (April 2025), RawSpan._extracting, Span._extracting, and UTF8Span.span returned a borrowed value that
// depended on a copied argument. Continue to support those interfaces. The implementations were correct but
// needed an explicit _overrideLifetime.
if let sourceFileKind = dependence.function.sourceFileKind, sourceFileKind == .interface {
return .continueWalk
}
}
return .abortWalk
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,10 @@ let lifetimeDependenceScopeFixupPass = FunctionPass(
// Recursively sink enclosing end_access, end_borrow, end_apply, and destroy_value. If the scope can be extended
// into the caller, return the function arguments that are the dependency sources.
var scopeExtension = ScopeExtension(localReachabilityCache, context)
let args = scopeExtension.extendScopes(dependence: newLifetimeDep)
guard scopeExtension.extendScopes(dependence: newLifetimeDep) else {
continue
}
let args = scopeExtension.findArgumentDependencies()

// Redirect the dependence base to the function arguments. This may create additional mark_dependence instructions.
markDep.redirectFunctionReturn(to: args, context)
Expand Down Expand Up @@ -246,28 +249,25 @@ private struct ScopeExtension {
// `mark_dependence` to the outer access `%0`. This ensures that exclusivity diagnostics correctly reports the
// violation, and that subsequent optimizations do not shrink the inner access `%a1`.
extension ScopeExtension {
mutating func extendScopes(dependence: LifetimeDependence) -> SingleInlineArray<FunctionArgument> {
mutating func extendScopes(dependence: LifetimeDependence) -> Bool {
log("Scope fixup for lifetime dependent instructions: \(dependence)")

gatherExtensions(dependence: dependence)

let noCallerScope = SingleInlineArray<FunctionArgument>()

// computeDependentUseRange initializes scopeExtension.dependsOnCaller.
guard var useRange = computeDependentUseRange(of: dependence) else {
return noCallerScope
return false
}
// tryExtendScopes deinitializes 'useRange'
var scopesToExtend = SingleInlineArray<ExtendableScope>()
guard canExtendScopes(over: &useRange, scopesToExtend: &scopesToExtend) else {
useRange.deinitialize()
return noCallerScope
return false
}
// extend(over:) must receive the original unmodified `useRange`, without intermediate scope ending instructions.
// This deinitializes `useRange` before erasing instructions.
extend(scopesToExtend: scopesToExtend, over: &useRange, context)

return dependsOnArgs
return true
}
}

Expand Down Expand Up @@ -448,10 +448,15 @@ extension ScopeExtension {
}

extension ScopeExtension {
/// Return all scope owners as long as they are all function arguments and all nested accesses are compatible with
/// their argument convention. Then, if all nested accesses were extended to the return statement, it is valid to
/// logically combine them into a single access for the purpose of diagnostic lifetime dependence.
var dependsOnArgs: SingleInlineArray<FunctionArgument> {
/// Check if the dependent value depends only on function arguments and can therefore be returned to caller. If so,
/// return the list of arguments that it depends on. If this returns an empty list, then the dependent value cannot be
/// returned.
///
/// The conditions for returning a dependent value are:
/// - The dependent value is returned from this function.
/// - All nested scopes are access scopes that are redundant with the caller's exclusive access scope.
/// - All scope owners are function arguments.
func findArgumentDependencies() -> SingleInlineArray<FunctionArgument> {
let noCallerScope = SingleInlineArray<FunctionArgument>()
// Check that the dependent value is returned by this function.
if !dependsOnCaller! {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ extension LifetimeDependence {

// Construct a LifetimeDependence from a return value. This only
// constructs a dependence for ~Escapable results that do not have a
// lifetime dependence (@_unsafeNonescapableResult).
// lifetime dependence (@lifetime(immortal), @_unsafeNonescapableResult).
//
// This is necessary because inserting a mark_dependence placeholder for such an unsafe dependence would illegally
// have the same base and value operand.
Expand Down
3 changes: 2 additions & 1 deletion lib/AST/LifetimeDependence.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -790,7 +790,8 @@ class LifetimeDependenceChecker {
auto immortalParam =
std::find_if(afd->getParameters()->begin(),
afd->getParameters()->end(), [](ParamDecl *param) {
return strcmp(param->getName().get(), "immortal") == 0;
return param->getName().nonempty()
&& strcmp(param->getName().get(), "immortal") == 0;
});
if (immortalParam != afd->getParameters()->end()) {
diagnose(*immortalParam,
Expand Down
6 changes: 4 additions & 2 deletions stdlib/public/core/Span/RawSpan.swift
Original file line number Diff line number Diff line change
Expand Up @@ -711,7 +711,8 @@ extension RawSpan {
public func _extracting(first maxLength: Int) -> Self {
_precondition(maxLength >= 0, "Can't have a prefix of negative length")
let newCount = min(maxLength, byteCount)
return unsafe Self(_unchecked: _pointer, byteCount: newCount)
let newSpan = unsafe Self(_unchecked: _pointer, byteCount: newCount)
return unsafe _overrideLifetime(newSpan, copying: self)
}

/// Returns a span over all but the given number of trailing bytes.
Expand All @@ -734,7 +735,8 @@ extension RawSpan {
_precondition(k >= 0, "Can't drop a negative number of bytes")
let droppedCount = min(k, byteCount)
let count = byteCount &- droppedCount
return unsafe Self(_unchecked: _pointer, byteCount: count)
let newSpan = unsafe Self(_unchecked: _pointer, byteCount: count)
return unsafe _overrideLifetime(newSpan, copying: self)
}

/// Returns a span containing the trailing bytes of the span,
Expand Down
6 changes: 4 additions & 2 deletions stdlib/public/core/Span/Span.swift
Original file line number Diff line number Diff line change
Expand Up @@ -764,7 +764,8 @@ extension Span where Element: ~Copyable {
public func _extracting(first maxLength: Int) -> Self {
_precondition(maxLength >= 0, "Can't have a prefix of negative length")
let newCount = min(maxLength, count)
return unsafe Self(_unchecked: _pointer, count: newCount)
let newSpan = unsafe Self(_unchecked: _pointer, count: newCount)
return unsafe _overrideLifetime(newSpan, copying: self)
}

/// Returns a span over all but the given number of trailing elements.
Expand All @@ -786,7 +787,8 @@ extension Span where Element: ~Copyable {
public func _extracting(droppingLast k: Int) -> Self {
_precondition(k >= 0, "Can't drop a negative number of elements")
let droppedCount = min(k, count)
return unsafe Self(_unchecked: _pointer, count: count &- droppedCount)
let newSpan = unsafe Self(_unchecked: _pointer, count: count &- droppedCount)
return unsafe _overrideLifetime(newSpan, copying: self)
}

/// Returns a span containing the final elements of the span,
Expand Down
3 changes: 2 additions & 1 deletion stdlib/public/core/UTF8Span.swift
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,8 @@ extension UTF8Span {
public var span: Span<UInt8> {
@lifetime(copy self)
get {
unsafe Span(_unchecked: _unsafeBaseAddress, count: self.count)
let newSpan = unsafe Span<UInt8>(_unchecked: _unsafeBaseAddress, count: self.count)
return unsafe _overrideLifetime(newSpan, copying: self)
}
}

Expand Down
3 changes: 2 additions & 1 deletion test/SIL/explicit_lifetime_dependence_specifiers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ func deriveThisOrThat1(_ this: borrowing BufferView, _ that: borrowing BufferVie
if (Int.random(in: 1..<100) == 0) {
return BufferView(independent: this.ptr)
}
return BufferView(independent: that.ptr)
let newThat = BufferView(independent: that.ptr)
return _overrideLifetime(newThat, copying: that)
}

// CHECK-LABEL: sil hidden @$s39explicit_lifetime_dependence_specifiers17deriveThisOrThat2yAA10BufferViewVAD_ADntF : $@convention(thin) (@guaranteed BufferView, @owned BufferView) -> @lifetime(copy 1, borrow 0) @owned BufferView {
Expand Down
10 changes: 7 additions & 3 deletions test/SIL/implicit_lifetime_dependence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,14 @@ func testBasic() {
// CHECK-LABEL: sil hidden @$s28implicit_lifetime_dependence6deriveyAA10BufferViewVADF : $@convention(thin) (@guaranteed BufferView) -> @lifetime(copy 0) @owned BufferView {
@lifetime(copy x)
func derive(_ x: borrowing BufferView) -> BufferView {
return BufferView(x.ptr, x.c)
let newBV = BufferView(x.ptr, x.c)
return _overrideLifetime(newBV, copying: x)
}

@lifetime(copy x)
func derive(_ unused: Int, _ x: borrowing BufferView) -> BufferView {
return BufferView(independent: x.ptr, x.c)
let newBV = BufferView(independent: x.ptr, x.c)
return _overrideLifetime(newBV, copying: x)
}

// CHECK-LABEL: sil hidden @$s28implicit_lifetime_dependence16consumeAndCreateyAA10BufferViewVADnF : $@convention(thin) (@owned BufferView) -> @lifetime(copy 0) @owned BufferView {
Expand Down Expand Up @@ -212,7 +214,9 @@ struct GenericBufferView<Element> : ~Escapable {
// CHECK-LABEL: sil hidden @$s28implicit_lifetime_dependence23tupleLifetimeDependenceyAA10BufferViewV_ADtADF : $@convention(thin) (@guaranteed BufferView) -> @lifetime(copy 0) (@owned BufferView, @owned BufferView) {
@lifetime(copy x)
func tupleLifetimeDependence(_ x: borrowing BufferView) -> (BufferView, BufferView) {
return (BufferView(x.ptr, x.c), BufferView(x.ptr, x.c))
let newX1 = BufferView(x.ptr, x.c)
let newX2 = BufferView(x.ptr, x.c)
return (_overrideLifetime(newX1, copying: x), _overrideLifetime(newX2, copying: x))
}

public struct OuterNE: ~Escapable {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// swift-interface-format-version: 1.0
// swift-module-flags: -module-name lifetime_depend_diagnose -enable-experimental-feature LifetimeDependence -swift-version 5 -enable-library-evolution
import Swift
import _Concurrency
import _StringProcessing
import _SwiftConcurrencyShims

#if $LifetimeDependence
public struct NE : ~Swift.Escapable {
@usableFromInline
internal let _pointer: Swift.UnsafeRawPointer?

@lifetime(borrow pointer)
public init(pointer: Swift.UnsafeRawPointer?) {
self._pointer = pointer
}
}

extension NE {
// This is illegal at the source level because NE.init is implicitly @lifetime(borrow),
// so we can't return it as dependent on @lifetime(copy self).
@lifetime(copy self)
@unsafe @_alwaysEmitIntoClient public func forward() -> NE {
return NE(pointer: _pointer)
}
}
#endif
13 changes: 13 additions & 0 deletions test/SILOptimizer/lifetime_dependence/diagnose_interface.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// RUN: %target-swift-frontend %s -emit-sil \
// RUN: -o /dev/null \
// RUN: -I %S/Inputs \
// RUN: -verify \
// RUN: -enable-experimental-feature LifetimeDependence

// REQUIRES: swift_in_compiler
// REQUIRES: swift_feature_LifetimeDependence

// Test that lifetime dependence diagnostics continues to older (early
// 2025) .swiftinterface files. Source-level diagnostics are stricter.

import lifetime_depend_diagnose
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@
// REQUIRES: swift_in_compiler
// REQUIRES: swift_feature_LifetimeDependence

@_unsafeNonescapableResult
@lifetime(copy source)
internal func _overrideLifetime<
T: ~Copyable & ~Escapable, U: ~Copyable & ~Escapable
>(
_ dependent: consuming T, copying source: borrowing U
) -> T {
dependent
}

// Some container-ish thing.
struct CN: ~Copyable {
let p: UnsafeRawPointer
Expand Down Expand Up @@ -47,7 +57,8 @@ struct MBV : ~Escapable, ~Copyable {
// Requires a borrow.
@lifetime(copy self)
borrowing func getBV() -> BV {
BV(p, i)
let bv = BV(p, i)
return _overrideLifetime(bv, copying: self)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,10 @@ struct MutableSpan : ~Escapable, ~Copyable {

var iterator: Iter {
@lifetime(copy self)
get { Iter(base: base, count: count) }
get {
let newIter = Iter(base: base, count: count)
return _overrideLifetime(newIter, copying: self)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@
// REQUIRES: swift_in_compiler
// REQUIRES: swift_feature_LifetimeDependence

/// Unsafely discard any lifetime dependency on the `dependent` argument. Return
/// a value identical to `dependent` that inherits all lifetime dependencies from
/// the `source` argument.
@_unsafeNonescapableResult
@_transparent
@lifetime(copy source)
internal func _overrideLifetime<
T: ~Copyable & ~Escapable, U: ~Copyable & ~Escapable
>(
_ dependent: consuming T, copying source: borrowing U
) -> T {
dependent
}

struct NCContainer : ~Copyable {
let ptr: UnsafeRawBufferPointer
let c: Int
Expand Down Expand Up @@ -233,7 +247,9 @@ func test9() {

@lifetime(copy x)
func getViewTuple(_ x: borrowing View) -> (View, View) {
return (View(x.ptr, x.c), View(x.ptr, x.c))
let x1 = View(x.ptr, x.c)
let x2 = View(x.ptr, x.c)
return (_overrideLifetime(x1, copying: x), _overrideLifetime(x2, copying: x))
}

public func test10() {
Expand Down
Loading