Skip to content

[6.2] Improve LocalVariableUtils.gatherKnownLifetimeUses; dead ends #82797

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 1 commit into from
Jul 7, 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 @@ -648,12 +648,10 @@ extension AddressOwnershipLiveRange {
let addressOwnershipLiveRangeTest = FunctionTest("address_ownership_live_range") {
function, arguments, context in
let address = arguments.takeValue()
let begin = arguments.takeInstruction()
print("Address: \(address)")
print("Base: \(address.accessBase)")
let begin = address.definingInstructionOrTerminator ?? {
assert(address is FunctionArgument)
return function.instructions.first!
}()
print("Begin: \(begin)")
let localReachabilityCache = LocalVariableReachabilityCache()
guard var ownershipRange = AddressOwnershipLiveRange.compute(for: address, at: begin,
localReachabilityCache, context) else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -975,6 +975,8 @@ extension LifetimeDependenceDefUseWalker {
return yieldedDependence(result: localAccess.operand!)
case .incomingArgument:
fatalError("Incoming arguments are never reachable")
case .deadEnd:
return .continueWalk
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ struct LocalVariableAccess: CustomStringConvertible {
case storeBorrow // scoped initialization of temporaries
case apply // indirect arguments
case escape // alloc_box captures
case deadEnd // unreachable
}
let kind: Kind
// All access have an operand except .incomingArgument and .outgoingArgument.
Expand Down Expand Up @@ -102,7 +103,7 @@ struct LocalVariableAccess: CustomStringConvertible {
case .`init`, .modify:
return true
}
case .load, .dependenceSource, .dependenceDest:
case .load, .dependenceSource, .dependenceDest, .deadEnd:
return false
case .incomingArgument, .outgoingArgument, .store, .storeBorrow, .inoutYield:
return true
Expand Down Expand Up @@ -153,6 +154,8 @@ struct LocalVariableAccess: CustomStringConvertible {
str += "apply"
case .escape:
str += "escape"
case .deadEnd:
str += "deadEnd"
}
if let inst = instruction {
str += "\(inst)"
Expand Down Expand Up @@ -201,7 +204,7 @@ class LocalVariableAccessInfo: CustomStringConvertible {
self.hasEscaped = true
case .inoutYield:
self._isFullyAssigned = false
case .incomingArgument, .outgoingArgument:
case .incomingArgument, .outgoingArgument, .deadEnd:
fatalError("Function arguments are never mapped to LocalVariableAccessInfo")
}
}
Expand Down Expand Up @@ -753,8 +756,8 @@ extension LocalVariableReachableAccess {
///
/// This does not include the destroy or reassignment of the value set by `modifyInst`.
///
/// Returns true if all possible reachable uses were visited. Returns false if any escapes may reach `modifyInst` are
/// reachable from `modifyInst`.
/// Returns true if all possible reachable uses were visited. Returns false if any escapes may reach `modifyInst` or
/// are reachable from `modifyInst`.
///
/// This does not gather the escaping accesses themselves. When escapes are reachable, it also does not guarantee that
/// previously reachable accesses are gathered.
Expand Down Expand Up @@ -855,6 +858,8 @@ extension LocalVariableReachableAccess {
}
if block.terminator.isFunctionExiting {
accessStack.push(LocalVariableAccess(.outgoingArgument, block.terminator))
} else if block.successors.isEmpty {
accessStack.push(LocalVariableAccess(.deadEnd, block.terminator))
} else {
for successor in block.successors { blockList.pushIfNotVisited(successor) }
}
Expand Down
58 changes: 58 additions & 0 deletions test/SILOptimizer/lifetime_dependence/scope_fixup.sil
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,13 @@ class C {

sil @C_read : $@yield_once @convention(method) (@guaranteed C) -> @yields @guaranteed Optional<NCWrapper>

struct S {
let c: C
}
struct MutNE: ~Copyable & ~Escapable {}

sil @getMutNE : $@convention(thin) (@inout S) -> @lifetime(borrow 0) @owned MutNE

// NCContainer.wrapper._read:
// var wrapper: Wrapper {
// _read {
Expand Down Expand Up @@ -791,3 +798,54 @@ bb2:
%42 = tuple ()
return %42
}

// rdar://154406790 (Lifetime-dependent variable 'X' escapes its scope but only if actor/class is final)
//
// The end_access must be extended into the unreachable block past the end_borrow.
//
// CHECK-LABEL: sil hidden [ossa] @testInoutAtModify : $@convention(thin) (@inout S) -> () {
// CHECK: bb0(%0 : $*S):
// CHECK: [[ACCESS:%[0-9]+]] = begin_access [modify] [unknown] %0
// CHECK: apply %{{.*}}([[ACCESS]]) : $@convention(thin) (@inout S) -> @lifetime(borrow 0) @owned MutNE
// CHECK: mark_dependence [unresolved] %{{.*}} on [[ACCESS]]
// CHECK: [[LB:%[0-9]+]] = load_borrow
//
// CHECK: bb1:
// CHECK: end_borrow [[LB]]
// CHECK: end_access [[ACCESS]]
// CHECK: return %16
//
// CHECK: bb2:
// CHECK: end_borrow [[LB]]
// CHECK: end_access [[ACCESS]]
// CHECK: unreachable
// CHECK-LABEL: } // end sil function 'testInoutAtModify'
sil hidden [ossa] @testInoutAtModify : $@convention(thin) (@inout S) -> () {
bb0(%0 : $*S):
%1 = alloc_box ${ let MutNE }, let, name "ne"
%2 = begin_borrow [lexical] [var_decl] %1
%3 = project_box %2, 0
%4 = begin_access [modify] [unknown] %0

%6 = function_ref @getMutNE : $@convention(thin) (@inout S) -> @lifetime(borrow 0) @owned MutNE
%7 = apply %6(%4) : $@convention(thin) (@inout S) -> @lifetime(borrow 0) @owned MutNE
%8 = mark_dependence [unresolved] %7 on %4
store %8 to [init] %3
end_access %4
%11 = mark_unresolved_non_copyable_value [no_consume_or_assign] %3
%12 = load_borrow %11
cond_br undef, bb1, bb2

bb1:
end_borrow %12
end_borrow %2
destroy_value %1
%15 = tuple ()
return %15

bb2:
end_borrow %12
end_borrow %2
dealloc_box [dead_end] %1
unreachable
}
13 changes: 13 additions & 0 deletions test/SILOptimizer/lifetime_dependence/verify_diagnostics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -274,3 +274,16 @@ func testSwitchAddr<T>(holder: inout Holder, t: T) {
mutate(&holder) // expected-note {{conflicting access is here}}
mutableView.modify()
}

// =============================================================================
// Throwing
// =============================================================================

@available(Span 0.1, *)
func mutableSpanMayThrow(_: borrowing MutableSpan<Int>) throws {}

@available(Span 0.1, *)
func testSpanMayThrow(buffer: inout [Int]) {
let bufferSpan = buffer.mutableSpan
try! mutableSpanMayThrow(bufferSpan)
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,33 @@
// RUN: %target-sil-opt -test-runner %s -o /dev/null 2>&1 | %FileCheck %s
// RUN: %target-sil-opt -test-runner \
// RUN: -enable-experimental-feature Lifetimes \
// RUN: %s -o /dev/null 2>&1 | %FileCheck %s

sil_stage canonical
// REQUIRES: swift_feature_Lifetimes

sil_stage raw

import Builtin
import Swift

class C {}
class D {
var object: C
}

struct S {
let c: C
}

struct MutNE: ~Copyable & ~Escapable {}

sil @getMutNE : $@convention(thin) (@inout S) -> @lifetime(borrow 0) @owned MutNE

// An address live range can be extended by a dependent value.
//
// CHECK-LABEL: testMarkDepAddressBase: address_ownership_live_range with: %f0
// CHECK-NEXT: Address: [[F0:%.*]] = ref_element_addr %0 : $D, #D.object
// CHECK-NEXT: Base: class - [[F0]] = ref_element_addr %0 : $D, #D.object
// CHECK-NEXT: borrow: functionArgument(%0 = argument of bb0 : $D
// CHECK: borrow: functionArgument(%0 = argument of bb0 : $D
// CHECK-NEXT: begin: [[F0]] = ref_element_addr %0 : $D, #D.object
// CHECK-NEXT: ends: end_borrow %{{.*}} : $C
// CHECK-NEXT: exits:
Expand All @@ -26,10 +39,50 @@ sil [ossa] @testMarkDepAddressBase : $@convention(thin) (@guaranteed D, @guarant
bb0(%0 : @guaranteed $D, %1 : @guaranteed $D):
%f0 = ref_element_addr %0 : $D, #D.object
%f1 = ref_element_addr %1 : $D, #D.object
specify_test "address_ownership_live_range %f0"
specify_test "address_ownership_live_range %f0 %f0"
%dependence = mark_dependence %f1 on %f0
%load = load_borrow %dependence
end_borrow %load
%99 = tuple()
return %99 : $()
}

// CHECK-LABEL: testInoutAtModify: address_ownership_live_range
// CHECK: Address: %0 = argument of bb0 : $*S
// CHECK: Begin: [[ACCESS:%[0-9]+]] = begin_access [modify] [unknown] %0 : $*S
// CHECK: local: %0 = argument of bb0 : $*S
// CHECK: begin: %{{.*}} = alloc_box ${ let MutNE }, let, name "ne"
// CHECK: ends: unreachable
// CHECK: return
// CHECK: exits:
// CHECK: interiors: end_access
// CHECK-LABEL: testInoutAtModify: address_ownership_live_range
sil hidden [ossa] @testInoutAtModify : $@convention(thin) (@inout S) -> () {
bb0(%0 : $*S):
%1 = alloc_box ${ let MutNE }, let, name "ne"
%2 = begin_borrow [lexical] [var_decl] %1
%3 = project_box %2, 0
%4 = begin_access [modify] [unknown] %0
specify_test "address_ownership_live_range %0 %4"

%6 = function_ref @getMutNE : $@convention(thin) (@inout S) -> @lifetime(borrow 0) @owned MutNE
%7 = apply %6(%4) : $@convention(thin) (@inout S) -> @lifetime(borrow 0) @owned MutNE
store %7 to [init] %3
end_access %4
%10 = mark_unresolved_non_copyable_value [no_consume_or_assign] %3
%11 = load_borrow %10
cond_br undef, bb1, bb2

bb1:
end_borrow %11
end_borrow %2
destroy_value %1
%15 = tuple ()
return %15

bb2:
end_borrow %11
end_borrow %2
dealloc_box [dead_end] %1
unreachable
}