Skip to content

Improve LifetimeDependenceDefUseWalker: consult DestructorAnalysis. #81551

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

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
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 @@ -10,5 +10,6 @@ swift_compiler_sources(Optimizer
AliasAnalysis.swift
CalleeAnalysis.swift
DeadEndBlocksAnalysis.swift
DestructorAnalysis.swift
DominatorTree.swift
PostDominatorTree.swift)
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//===--- DestructorAnalysis.swift - the dead-end blocks analysis ----------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import OptimizerBridging
import SIL

struct DestructorAnalysis {
let bridged: BridgedDestructorAnalysis

// TODO: swift::isDestructorSideEffectFree() also handles other cases, such as Arrays and closures.
func deinitMayHaveEffects(type: Type, in function: Function) -> Bool {
if type.isBox {
return type.getBoxFields(in: function).contains(where: { deinitMayHaveEffects(type: $0, in: function) })
}
return bridged.mayStoreToMemoryOnDestruction(type.bridged)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -449,14 +449,17 @@ extension DependentAddressUseDefWalker: AddressUseDefWalker {
/// TODO: handle stores to singly initialized temporaries like copies using a standard reaching-def analysis.
private struct DiagnoseDependenceWalker {
let context: Context
let destructorAnalysis: DestructorAnalysis

var diagnostics: DiagnoseDependence
let localReachabilityCache = LocalVariableReachabilityCache()
var visitedValues: ValueSet

var function: Function { diagnostics.function }
var function: Function { get { diagnostics.function } }

init(_ diagnostics: DiagnoseDependence, _ context: Context) {
init(_ diagnostics: DiagnoseDependence, _ context: FunctionPassContext) {
self.context = context
self.destructorAnalysis = context.destructorAnalysis
self.diagnostics = diagnostics
self.visitedValues = ValueSet(context)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ private struct ScopeExtension {
// violation, and that subsequent optimizations do not shrink the inner access `%a1`.
extension ScopeExtension {
mutating func extendScopes(dependence: LifetimeDependence) -> Bool {
log("Scope fixup for lifetime dependent instructions: \(dependence)")
log("Scope fixup for lifetime dependent instructions:\n\(dependence)")

gatherExtensions(dependence: dependence)

Expand Down Expand Up @@ -1012,19 +1012,22 @@ private extension BeginApplyInst {
///
/// Set 'dependsOnCaller' if a use escapes the function.
private struct LifetimeDependentUseWalker : LifetimeDependenceDefUseWalker {
let function: Function
let context: Context
let function: Function
let destructorAnalysis: DestructorAnalysis

let visitor: (Operand) -> WalkResult
let localReachabilityCache: LocalVariableReachabilityCache
var visitedValues: ValueSet

/// Set to true if the dependence is returned from the current function.
var dependsOnCaller = false

init(_ function: Function, _ localReachabilityCache: LocalVariableReachabilityCache, _ context: Context,
init(_ function: Function, _ localReachabilityCache: LocalVariableReachabilityCache, _ context: FunctionPassContext,
visitor: @escaping (Operand) -> WalkResult) {
self.function = function
self.context = context
self.function = function
self.destructorAnalysis = context.destructorAnalysis
self.visitor = visitor
self.localReachabilityCache = localReachabilityCache
self.visitedValues = ValueSet(context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,11 @@ struct FunctionPassContext : MutatingContext {
return DeadEndBlocksAnalysis(bridged: bridgeDEA)
}

var destructorAnalysis: DestructorAnalysis {
let bridgeAnalysis = _bridged.getDestructorAnalysis()
return DestructorAnalysis(bridged: bridgeAnalysis)
}

var dominatorTree: DominatorTree {
let bridgedDT = _bridged.getDomTree()
return DominatorTree(bridged: bridgedDT)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,8 @@ protocol LifetimeDependenceDefUseWalker : ForwardingDefUseWalker,
AddressUseVisitor {
var function: Function { get }

var destructorAnalysis: DestructorAnalysis { get }

/// Dependence tracking through local variables.
var localReachabilityCache: LocalVariableReachabilityCache { get }

Expand Down Expand Up @@ -658,14 +660,19 @@ extension LifetimeDependenceDefUseWalker {
// Catch .instantaneousUse operations that are dependence leaf uses.
return leafUse(of: operand)

case is DestroyValueInst, is EndLifetimeInst, is DeallocRefInst,
is DeallocBoxInst, is DeallocExistentialBoxInst,
is BeginCOWMutationInst, is EndCOWMutationInst,
is EndInitLetRefInst, is DeallocPartialRefInst, is BeginDeallocRefInst:
// Catch .destroyingConsume operations that are dependence leaf
// uses.
case is DestroyValueInst:
// Handles dependent value destroys.
//
// Ignore destroys on a type whose members recursively only have default deinits.
if !destructorAnalysis.deinitMayHaveEffects(type: operand.value.type, in: function) {
return .continueWalk
}
return leafUse(of: operand)

case is DeallocBoxInst, is DeallocExistentialBoxInst, is DeallocPartialRefInst, is DeallocRefInst,
is EndLifetimeInst:
return .continueWalk

case let si as StoringInstruction where si.sourceOperand == operand:
return visitStoredUses(of: operand, into: si.destinationOperand.value)

Expand Down Expand Up @@ -1028,12 +1035,15 @@ let lifetimeDependenceScopeTest = FunctionTest("lifetime_dependence_scope") {
private struct LifetimeDependenceUsePrinter : LifetimeDependenceDefUseWalker {
let context: Context
let function: Function
let destructorAnalysis: DestructorAnalysis

let localReachabilityCache = LocalVariableReachabilityCache()
var visitedValues: ValueSet

init(function: Function, _ context: Context) {
init(function: Function, _ context: FunctionPassContext) {
self.context = context
self.function = function
self.destructorAnalysis = context.destructorAnalysis
self.visitedValues = ValueSet(context)
}

Expand Down
8 changes: 8 additions & 0 deletions include/swift/SILOptimizer/OptimizerBridging.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class AliasAnalysis;
class BasicCalleeAnalysis;
class CalleeList;
class DeadEndBlocks;
class DestructorAnalysis;
class DominanceInfo;
class PostDominanceInfo;
class BasicBlockSet;
Expand Down Expand Up @@ -116,6 +117,12 @@ struct BridgedDeadEndBlocksAnalysis {
BRIDGED_INLINE bool isDeadEnd(BridgedBasicBlock block) const;
};

struct BridgedDestructorAnalysis {
swift::DestructorAnalysis * _Nonnull analysis;

BRIDGED_INLINE bool mayStoreToMemoryOnDestruction(BridgedType type) const;
};

struct BridgedDomTree {
swift::DominanceInfo * _Nonnull di;

Expand Down Expand Up @@ -213,6 +220,7 @@ struct BridgedPassContext {
SWIFT_IMPORT_UNSAFE BRIDGED_INLINE BridgedAliasAnalysis getAliasAnalysis() const;
SWIFT_IMPORT_UNSAFE BRIDGED_INLINE BridgedCalleeAnalysis getCalleeAnalysis() const;
SWIFT_IMPORT_UNSAFE BRIDGED_INLINE BridgedDeadEndBlocksAnalysis getDeadEndBlocksAnalysis() const;
SWIFT_IMPORT_UNSAFE BRIDGED_INLINE BridgedDestructorAnalysis getDestructorAnalysis() const;
SWIFT_IMPORT_UNSAFE BRIDGED_INLINE BridgedDomTree getDomTree() const;
SWIFT_IMPORT_UNSAFE BRIDGED_INLINE BridgedPostDomTree getPostDomTree() const;
SWIFT_IMPORT_UNSAFE BRIDGED_INLINE BridgedDeclObj getSwiftArrayDecl() const;
Expand Down
13 changes: 13 additions & 0 deletions include/swift/SILOptimizer/OptimizerBridgingImpl.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include "swift/SILOptimizer/Analysis/AliasAnalysis.h"
#include "swift/SILOptimizer/Analysis/BasicCalleeAnalysis.h"
#include "swift/SILOptimizer/Analysis/DeadEndBlocksAnalysis.h"
#include "swift/SILOptimizer/Analysis/DestructorAnalysis.h"
#include "swift/SILOptimizer/Analysis/DominanceAnalysis.h"
#include "swift/SILOptimizer/OptimizerBridging.h"
#include "swift/SILOptimizer/PassManager/PassManager.h"
Expand Down Expand Up @@ -74,6 +75,14 @@ bool BridgedDeadEndBlocksAnalysis::isDeadEnd(BridgedBasicBlock block) const {
return deb->isDeadEnd(block.unbridged());
}

//===----------------------------------------------------------------------===//
// BridgedDestructorAnalysis
//===----------------------------------------------------------------------===//

bool BridgedDestructorAnalysis::mayStoreToMemoryOnDestruction(BridgedType type) const {
return analysis->mayStoreToMemoryOnDestruction(type.unbridged());
}

//===----------------------------------------------------------------------===//
// BridgedDomTree, BridgedPostDomTree
//===----------------------------------------------------------------------===//
Expand Down Expand Up @@ -205,6 +214,10 @@ BridgedDeadEndBlocksAnalysis BridgedPassContext::getDeadEndBlocksAnalysis() cons
return {dba->get(invocation->getFunction())};
}

BridgedDestructorAnalysis BridgedPassContext::getDestructorAnalysis() const {
return {invocation->getPassManager()->getAnalysis<swift::DestructorAnalysis>()};
}

BridgedDomTree BridgedPassContext::getDomTree() const {
auto *da = invocation->getPassManager()->getAnalysis<swift::DominanceAnalysis>();
return {da->get(invocation->getFunction())};
Expand Down
24 changes: 22 additions & 2 deletions lib/SILOptimizer/Analysis/DestructorAnalysis.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@ bool DestructorAnalysis::isSafeType(CanType Ty) {
return cacheResult(Ty, true);
if (Ty->is<BuiltinFloatType>())
return cacheResult(Ty, true);
if (Ty->is<BuiltinRawPointerType>())
return cacheResult(Ty, true);

if (auto fixedArrTy = dyn_cast<BuiltinFixedArrayType>(Ty)) {
auto subMap = Ty->getContextSubstitutionMap();
Type eltTy = fixedArrTy->getElementType().subst(subMap);
return !isSafeType(eltTy->getCanonicalType());
}

// A struct is safe if
// * either it implements the _DestructorSafeContainer protocol and
Expand All @@ -81,8 +89,10 @@ bool DestructorAnalysis::isSafeType(CanType Ty) {
return cacheResult(Ty, true);

// Check the stored properties.
auto subMap = Ty->getContextSubstitutionMap();
for (auto SP : Struct->getStoredProperties()) {
if (!isSafeType(SP->getInterfaceType()->getCanonicalType()))
Type spTy = SP->getInterfaceType().subst(subMap);
if (!isSafeType(spTy->getCanonicalType()))
return cacheResult(Ty, false);
}
return cacheResult(Ty, true);
Expand All @@ -96,7 +106,17 @@ bool DestructorAnalysis::isSafeType(CanType Ty) {
return cacheResult(Ty, true);
}

// TODO: enum types.
if (auto *enumDecl = Ty->getEnumOrBoundGenericEnum()) {
auto subMap = Ty->getContextSubstitutionMap();
for (auto *enumEltDecl : enumDecl->getAllElements()) {
if (auto payloadInterfaceTy = enumEltDecl->getPayloadInterfaceType()) {
Type payloadTy = payloadInterfaceTy.subst(subMap);
if (!isSafeType(payloadTy->getCanonicalType()))
return cacheResult(Ty, false);
}
}
return cacheResult(Ty, true);
}

return cacheResult(Ty, false);
}
Expand Down
13 changes: 11 additions & 2 deletions test/SILOptimizer/lifetime_dependence/semantics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -357,9 +357,10 @@ func testScopedOfInheritedWithLet(_ arg: [Int] ) {
// TODO: should be // ✅ Safe: 'copySpan' result should be borrowed over `result`
// rdar://128821299 ([nonescaping] extend borrowed arguments that are the source of a scoped dependence)
let result = reborrowSpan(copySpan(span)) // expected-error {{lifetime-dependent variable 'result' escapes its scope}}
// expected-note @-1{{it depends on the lifetime of this parent value}}
// expected-note @-1{{it depends on the lifetime of this parent value}}
// expected-note @-2{{this use of the lifetime-dependent value is out of scope}}
_ = result
} // expected-note {{this use of the lifetime-dependent value is out of scope}}
}

// Test a scoped dependence on an inherited inout argument.
//
Expand Down Expand Up @@ -456,6 +457,14 @@ func testInoutMutableBorrow(a: inout [Int]) -> MutableSpan<Int> {
a.mutableSpan()
}

func testInoutTemporaryBorrow(a: inout [Int]) {
let span = a.span()
parse(span)
// 'read' access ends here even though span is still alive
// because span has no user-defined deinit.
a.append(10)
}

// =============================================================================
// Scoped dependence on property access
// =============================================================================
Expand Down