Skip to content

LifetimeDependenceDiagnostics pass #71209

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 12 commits into from
Jan 30, 2024
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ swift_compiler_sources(Optimizer
DeinitDevirtualizer.swift
InitializeStaticGlobals.swift
LetPropertyLowering.swift
LifetimeDependenceDiagnostics.swift
ObjectOutliner.swift
ObjCBridgingOptimization.swift
MergeCondFails.swift
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
//===--- LifetimeDependenceDiagnostics.swift - Lifetime dependence --------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2023 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 SIL

private let verbose = true

private func log(_ message: @autoclosure () -> String) {
if verbose {
print("### \(message())")
}
}

/// Diagnostic pass.
///
/// Find the roots of all non-escapable values in this function. All
/// non-escapable values either depend on a NonEscapingScope, or they
/// are produced by a LifetimeDependentInstruction that has no
/// dependence on a parent value (@_unsafeNonEscapableResult).
let lifetimeDependenceDiagnosticsPass = FunctionPass(
name: "lifetime-dependence-diagnostics")
{ (function: Function, context: FunctionPassContext) in
if !context.options.hasFeature(.NonescapableTypes) {
return
}
log("Diagnosing lifetime dependence in \(function.name)")
log("\(function)")

for argument in function.arguments where !argument.type.isEscapable {
// Indirect results are not checked here. Type checking ensures
// that they have a lifetime dependence.
if let lifetimeDep = LifetimeDependence(argument, context) {
analyze(dependence: lifetimeDep, context)
}
}
for instruction in function.instructions {
guard let markDep = instruction as? MarkDependenceInst else { continue }
if let lifetimeDep = LifetimeDependence(markDep, context) {
analyze(dependence: lifetimeDep, context)
}
}
}

/// Analyze a single Lifetime dependence and trigger diagnostics.
///
/// 1. Compute the LifetimeDependence scope.
///
/// 2. Walk down all dependent values checking that they are within range.
private func analyze(dependence: LifetimeDependence,
_ context: FunctionPassContext) {
log("Dependence scope:\n\(dependence)")

// Compute this dependence scope.
var range = dependence.computeRange(context)
defer { range?.deinitialize() }

let diagnostics =
DiagnoseDependence(dependence: dependence, range: range, context: context)

// Check each lifetime-dependent use via a def-use visitor
var walker = DiagnoseDependenceWalker(diagnostics, context)
defer { walker.deinitialize() }
_ = walker.walkDown(root: dependence.parentValue)
}

/// Analyze and diagnose a single LifetimeDependence.
private struct DiagnoseDependence {
let dependence: LifetimeDependence
let range: InstructionRange?
let context: FunctionPassContext

var function: Function { dependence.function }

/// Check that this use is inside the dependence scope.
func checkInScope(operand: Operand) -> WalkResult {
if let range, !range.inclusiveRangeContains(operand.instruction) {
log(" out-of-range: \(operand.instruction)")
reportError(operand: operand, diagID: .lifetime_outside_scope_use)
return .abortWalk
}
log(" contains: \(operand.instruction)")
return .continueWalk
}

func reportEscaping(operand: Operand) {
log(" escaping: \(operand.instruction)")
reportError(operand: operand, diagID: .lifetime_outside_scope_escape)
}

func reportUnknown(operand: Operand) {
standardError.write("Unknown use: \(operand)\n\(function)")
reportEscaping(operand: operand)
}

func checkFunctionResult(operand: Operand) -> WalkResult {
// TODO: Get the argument dependence for this result. Check that it is the
// same as the current dependence scope

if function.hasUnsafeNonEscapableResult {
return .continueWalk
}
// TODO: Take ResultInfo as an argument and provide better
// diagnostics for missing lifetime dependencies.
reportEscaping(operand: operand)
return .abortWalk
}

func reportError(operand: Operand, diagID: DiagID) {
// Identify the escaping variable.
let escapingVar = LifetimeVariable(dependent: operand.value, context)
let varName = escapingVar.name
if let varName {
context.diagnosticEngine.diagnose(escapingVar.sourceLoc,
.lifetime_variable_outside_scope,
varName)
} else {
context.diagnosticEngine.diagnose(escapingVar.sourceLoc,
.lifetime_value_outside_scope)
}
// Identify the dependence scope.
//
// TODO: add bridging for function argument locations
// [SILArgument.getDecl().getLoc()]
//
// TODO: For clear diagnostics: switch on dependence.scope.
// For an access, report both the accessed variable, and the access.
if let parentSourceLoc =
dependence.parentValue.definingInstruction?.location.sourceLoc {
context.diagnosticEngine.diagnose(parentSourceLoc,
.lifetime_outside_scope_parent)
}
// Identify the use point.
let userSourceLoc = operand.instruction.location.sourceLoc
context.diagnosticEngine.diagnose(userSourceLoc, diagID)
}
}

private extension Instruction {
func findVarDecl() -> VarDecl? {
if let varDeclInst = self as? VarDeclInstruction {
return varDeclInst.varDecl
}
for result in results {
for use in result.uses {
if let debugVal = use.instruction as? DebugValueInst {
return debugVal.varDecl
}
}
}
return nil
}
}

// Identify a best-effort variable declaration based on a defining SIL
// value or any lifetime dependent use of that SIL value.
private struct LifetimeVariable {
var varDecl: VarDecl?
var sourceLoc: SourceLoc?

var name: String? {
return varDecl?.userFacingName
}

init(introducer: Value) {
if introducer.type.isAddress {
switch introducer.enclosingAccessScope {
case let .scope(beginAccess):
// TODO: report both the access point and original variable.
self = LifetimeVariable(introducer: beginAccess.operand.value)
return
case .base(_):
// TODO: use an address walker to get the allocation point.
break
}
}
if let arg = introducer as? Argument {
self.varDecl = arg.varDecl
} else {
self.sourceLoc = introducer.definingInstruction?.location.sourceLoc
self.varDecl = introducer.definingInstruction?.findVarDecl()
}
if let varDecl {
sourceLoc = varDecl.sourceLoc
}
}

init(dependent value: Value, _ context: Context) {
// TODO: consider diagnosing multiple variable introducers. It's
// unclear how more than one can happen.
var introducers = Stack<Value>(context)
gatherBorrowIntroducers(for: value, in: &introducers, context)
if let firstIntroducer = introducers.pop() {
self = LifetimeVariable(introducer: firstIntroducer)
return
}
self.varDecl = nil
self.sourceLoc = nil
}
}

/// Walk down lifetime depenence uses. For each check that all dependent
/// leaf uses are non-escaping and within the dependence scope. The walk
/// starts with add address for .access dependencies. The walk can
/// transition from an address to a value at a load. The walk can
/// transition from a value to an address as follows:
///
/// %dependent_addr = mark_dependence [nonescaping] %base_addr on %value
///
/// TODO: handle stores to singly initialized temporaries like copies using a standard reaching-def analysis.
private struct DiagnoseDependenceWalker {
let diagnostics: DiagnoseDependence
let context: Context
var visitedValues: ValueSet

var function: Function { diagnostics.function }

init(_ diagnostics: DiagnoseDependence, _ context: Context) {
self.diagnostics = diagnostics
self.context = context
self.visitedValues = ValueSet(context)
}

mutating func deinitialize() {
visitedValues.deinitialize()
}
}

extension DiagnoseDependenceWalker : LifetimeDependenceDefUseWalker {
mutating func needWalk(for value: Value) -> Bool {
visitedValues.insert(value)
}

mutating func leafUse(of operand: Operand) -> WalkResult {
return diagnostics.checkInScope(operand: operand)
}

mutating func deadValue(_ value: Value, using operand: Operand?)
-> WalkResult {
// Ignore a dead root value. It never escapes.
if let operand {
return diagnostics.checkInScope(operand: operand)
}
return .continueWalk
}

mutating func escapingDependence(on operand: Operand) -> WalkResult {
diagnostics.reportEscaping(operand: operand)
return .abortWalk
}

mutating func returnedDependence(result: Operand) -> WalkResult {
return diagnostics.checkFunctionResult(operand: result)
}

mutating func returnedDependence(address: FunctionArgument,
using operand: Operand) -> WalkResult {
return diagnostics.checkFunctionResult(operand: operand)
}

// Override AddressUseVisitor here because LifetimeDependenceDefUseWalker
// returns .abortWalk, and we want a more useful crash report.
mutating func unknownAddressUse(of operand: Operand) -> WalkResult {
diagnostics.reportUnknown(operand: operand)
return .continueWalk
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ private func registerSwiftPasses() {
registerPass(redundantLoadElimination, { redundantLoadElimination.run($0) })
registerPass(earlyRedundantLoadElimination, { earlyRedundantLoadElimination.run($0) })
registerPass(deinitDevirtualizer, { deinitDevirtualizer.run($0) })
registerPass(lifetimeDependenceDiagnosticsPass, { lifetimeDependenceDiagnosticsPass.run($0) })

// Instruction passes
registerForSILCombine(BeginCOWMutationInst.self, { run(BeginCOWMutationInst.self, $0) })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,19 @@ protocol AddressUseVisitor {
/// An access scope: begin_access, begin_apply, load_borrow.
mutating func scopedAddressUse(of operand: Operand) -> WalkResult

/// end_access, end_apply, abort_apply, end_borrow.
mutating func scopeEndingAddressUse(of operand: Operand) -> WalkResult

/// A address leaf use cannot propagate the address bits beyond the
/// instruction.
///
/// An apply or builtin propagates an address into the callee, but
/// it is considered a leaf use as long as the argument does not escape.
/// StoringInstructions are leaf uses.
mutating func leafAddressUse(of operand: Operand) -> WalkResult

/// An address used by an apply.
mutating func appliedAddressUse(of operand: Operand, by apply: FullApplySite)
-> WalkResult

/// A loaded address use propagates the value at the address.
mutating func loadedAddressUse(of operand: Operand, into value: Value)
-> WalkResult
Expand All @@ -67,10 +73,12 @@ extension AddressUseVisitor {
/// protocol methods above.
mutating func classifyAddress(operand: Operand) -> WalkResult {
switch operand.instruction {
case is BeginAccessInst, is BeginApplyInst, is LoadBorrowInst,
is StoreBorrowInst:
case is BeginAccessInst, is LoadBorrowInst, is StoreBorrowInst:
return scopedAddressUse(of: operand)

case is EndAccessInst, is EndApplyInst, is AbortApplyInst, is EndBorrowInst:
return scopeEndingAddressUse(of: operand)

case let markDep as MarkDependenceInst:
if markDep.valueOperand == operand {
return projectedAddressUse(of: operand, into: markDep)
Expand All @@ -81,7 +89,7 @@ extension AddressUseVisitor {
if markDep.type.isAddress {
return projectedAddressUse(of: operand, into: markDep)
}
if LifetimeDependence(markDependence: markDep, context) != nil {
if LifetimeDependence(markDep, context) != nil {
// This is unreachable from InteriorUseVisitor because the
// base address of a `mark_dependence [nonescaping]` must be a
// `begin_access`, and interior liveness does not check uses of
Expand All @@ -97,7 +105,7 @@ extension AddressUseVisitor {
case let pai as PartialApplyInst where !pai.isOnStack:
return escapingAddressUse(of: operand)

case is AddressToPointerInst:
case is ReturnInst, is ThrowInst, is YieldInst, is AddressToPointerInst:
return escapingAddressUse(of: operand)

case is StructElementAddrInst, is TupleElementAddrInst,
Expand All @@ -113,16 +121,18 @@ extension AddressUseVisitor {
let svi = operand.instruction as! SingleValueInstruction
return projectedAddressUse(of: operand, into: svi)

case is ReturnInst, is ThrowInst, is YieldInst, is TryApplyInst,
is SwitchEnumAddrInst, is CheckedCastAddrBranchInst,
case let apply as FullApplySite:
return appliedAddressUse(of: operand, by: apply)

case is SwitchEnumAddrInst, is CheckedCastAddrBranchInst,
is SelectEnumAddrInst, is InjectEnumAddrInst,
is StoreInst, is StoreUnownedInst, is StoreWeakInst,
is AssignInst, is AssignByWrapperInst, is AssignOrInitInst,
is TupleAddrConstructorInst, is InitBlockStorageHeaderInst,
is RetainValueAddrInst, is ReleaseValueAddrInst,
is DestroyAddrInst, is DeallocStackInst,
is DeinitExistentialAddrInst,
is EndApplyInst, is IsUniqueInst, is MarkFunctionEscapeInst,
is IsUniqueInst, is MarkFunctionEscapeInst,
is PackElementSetInst:
return leafAddressUse(of: operand)

Expand Down
Loading