Merge pull request #85533 from eeckstein/fix-access-simplification

SILOptimizer: don't remove empty conflicting access scopes
This commit is contained in:
eeckstein
2025-12-01 09:35:48 +01:00
committed by GitHub
16 changed files with 801 additions and 67 deletions

View File

@@ -17,6 +17,7 @@ swift_compiler_sources(Optimizer
ComputeEscapeEffects.swift
ComputeSideEffects.swift
CopyToBorrowOptimization.swift
DeadAccessScopeElimination.swift
DeadStoreElimination.swift
DeinitDevirtualizer.swift
DestroyHoisting.swift

View File

@@ -0,0 +1,247 @@
//===--- DeadAccessScopeElimination.swift ----------------------------------==//
//
// 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 SIL
/// Eliminates dead access scopes if they are not conflicting with other scopes.
///
/// Removes:
/// ```
/// %2 = begin_access [modify] [dynamic] %1
/// ... // no uses of %2
/// end_access %2
/// ```
///
/// However, dead _conflicting_ access scopes are not removed.
/// If a conflicting scope becomes dead because an optimization e.g. removed a load, it is still
/// important to get an access violation at runtime.
/// Even a propagated value of a redundant load from a conflicting scope is undefined.
///
/// ```
/// %2 = begin_access [modify] [dynamic] %1
/// store %x to %2
/// %3 = begin_access [read] [dynamic] %1 // conflicting with %2!
/// %y = load %3
/// end_access %3
/// end_access %2
/// use(%y)
/// ```
/// After redundant-load-elimination:
/// ```
/// %2 = begin_access [modify] [dynamic] %1
/// store %x to %2
/// %3 = begin_access [read] [dynamic] %1 // now dead, but still conflicting with %2
/// end_access %3
/// end_access %2
/// use(%x) // propagated from the store, but undefined here!
/// ```
/// In this case the scope `%3` is not removed because it's important to get an access violation
/// error at runtime before the undefined value `%x` is used.
///
/// This pass considers potential conflicting access scopes in called functions.
/// But it does not consider potential conflicting access in callers (because it can't!).
/// However, optimizations, like redundant-load-elimination, can only do such transformations if
/// the outer access scope is within the function, e.g.
///
/// ```
/// bb0(%0 : $*T): // an inout from a conflicting scope in the caller
/// store %x to %0
/// %3 = begin_access [read] [dynamic] %1
/// %y = load %3 // cannot be propagated because it cannot be proved that %1 is the same address as %0
/// end_access %3
/// ```
///
/// All those checks are only done for dynamic access scopes, because they matter for runtime
/// exclusivity checking. Dead static scopes are removed unconditionally.
///
let deadAccessScopeElimination = FunctionPass(name: "dead-access-scope-elimination") {
(function: Function, context: FunctionPassContext) in
// Add all dead scopes here and then remove the ones which turn out to be conflicting.
var removableScopes = SpecificIterableInstructionSet<BeginAccessInst>(context)
defer { removableScopes.deinitialize() }
var scopeTree = ScopeTree(context)
// The payload is the recent access instruction at the block begin, e.g.
// ```
// %1 = begin_acces %0
// br bb2
// bb2: // recent access instruction at begin of bb2 is: `%1 = begin_acces %0`
// ```
// It's nil if the block is not within an access scope.
//
var blockWorklist = BasicBlockWorklistWithPayload<Instruction?>(context)
defer { blockWorklist.deinitialize() }
blockWorklist.pushIfNotVisited(function.entryBlock, with: nil)
// Walk through the control flow in depth-first order. Note that we don't need to do any kind
// of state merging at merge-points, because access scopes must be consistent on all paths.
while let (block, recentAccessInstAtBlockBegin) = blockWorklist.pop() {
// The last seen `begin_access` (or `end_access` in case of not perfectly nested scopes; see ScopeTree.backlinks)
var recentAccessInst = recentAccessInstAtBlockBegin
for inst in block.instructions {
process(instruction: inst, updating: &recentAccessInst, &scopeTree, &removableScopes)
}
blockWorklist.pushIfNotVisited(contentsOf: block.successors, with: recentAccessInst)
}
for deadBeginAccess in removableScopes {
context.erase(instructionIncludingAllUsers: deadBeginAccess)
}
}
private func process(instruction: Instruction,
updating recentAccessInst: inout Instruction?,
_ scopeTree: inout ScopeTree,
_ removableScopes: inout SpecificIterableInstructionSet<BeginAccessInst>)
{
switch instruction {
case let beginAccess as BeginAccessInst:
if beginAccess.isDead {
// Might be removed again later if it turns out to be in a conflicting scope.
removableScopes.insert(beginAccess)
}
if beginAccess.enforcement != .dynamic {
// We unconditionally remove dead _static_ scopes, because they don't have any impact at runtime.
// Usually static scopes are already removed in the optimization pipeline. However optimizations
// might turn dynamic into static scopes. So let's handle them.
break
}
scopeTree.visitEnclosingScopes(of: recentAccessInst) { enclosingBeginAccess in
if beginAccess.accessKind.conflicts(with: enclosingBeginAccess.accessKind),
// Avoid computing alias info if both scopes are not removable anyway.
removableScopes.contains(beginAccess) || removableScopes.contains(enclosingBeginAccess),
scopeTree.context.aliasAnalysis.mayAlias(beginAccess.address, enclosingBeginAccess.address)
{
// Conflicting enclosing scopes are not removable.
removableScopes.erase(enclosingBeginAccess)
// ... as well as the inner scope (which conflicts with the enclosing scope).
removableScopes.erase(beginAccess)
}
}
scopeTree.update(recent: &recentAccessInst, with: beginAccess)
case let endAccess as EndAccessInst where endAccess.beginAccess.enforcement == .dynamic:
scopeTree.update(recent: &recentAccessInst, with: endAccess)
default:
if instruction.mayCallFunction {
// Check for potential conflicting scopes in called functions.
scopeTree.visitEnclosingScopes(of: recentAccessInst) { enclosingBeginAccess in
if removableScopes.contains(enclosingBeginAccess),
instruction.mayHaveAccessScopeWhichConflicts(with: enclosingBeginAccess, scopeTree.context)
{
removableScopes.erase(enclosingBeginAccess)
}
}
}
}
}
/// Represents the tree of access scopes in a function.
/// Note that if the scopes are not nested perfectly, it's strictly speaking not a tree.
private struct ScopeTree {
// Links `begin_access` and `end_access` instructions in backward control flow direction.
// This is used to visit all enclosing scopes of a `begin_access`.
// As an optimization, `end_access`es are ignored for scopes which are perfectly nested - which is
// by far the most common case. In this case the backlinks simply are the parent links in the scope tree.
//
// Example of not perfectly nested scopes:
// ```
// %1 = begin_access <------------------+
// ... |
// %2 = begin_access <--------------+ -+
// ... |
// end_access %1 <---------+ -+
// ... |
// %3 = begin_access <-----+ -+
// ... |
// end_access %2 <-+ -+
// ... |
// end_access %3 -+
// ```
//
// Perfectly nested scopes:
// ```
// %1 = begin_access <-+ <-+
// ... | |
// %2 = begin_access -+ |
// end_access %2 | <- ignored
// ... |
// %3 = begin_access -------+
// end_access %3 <- ignored
// ...
// end_access %1 <- ignored
// ```
private var backlinks = Dictionary<Instruction, Instruction>()
let context: FunctionPassContext
init(_ context: FunctionPassContext) { self.context = context }
mutating func update(recent: inout Instruction?, with beginAccess: BeginAccessInst) {
backlinks[beginAccess] = recent
recent = beginAccess
}
mutating func update(recent: inout Instruction?, with endAccess: EndAccessInst) {
if endAccess.beginAccess == recent {
// The scope is perfectly nested. Ignore it and directly backlink to the parent of the `begin_access`
recent = backlinks[endAccess.beginAccess]
} else {
backlinks[endAccess] = recent
recent = endAccess
}
}
func visitEnclosingScopes(of accessInstruction: Instruction?, closure: (BeginAccessInst) -> ()) {
// Ignore scopes which are already closed
var ignore = SpecificInstructionSet<BeginAccessInst>(context)
defer { ignore.deinitialize() }
var enclosingScope = accessInstruction
while let parent = enclosingScope {
switch parent {
case let parentBeginAccess as BeginAccessInst where !ignore.contains(parentBeginAccess):
closure(parentBeginAccess)
case let parentEndAccess as EndAccessInst:
// Seeing an `end_access` in the backlink chain means that this scope is already closed.
ignore.insert(parentEndAccess.beginAccess)
default:
break
}
enclosingScope = backlinks[parent]
}
}
}
private extension Instruction {
func mayHaveAccessScopeWhichConflicts(with beginAccess: BeginAccessInst, _ context: FunctionPassContext) -> Bool {
if beginAccess.accessKind == .read {
return mayWrite(toAddress: beginAccess.address, context.aliasAnalysis)
} else {
return mayReadOrWrite(address: beginAccess.address, context.aliasAnalysis)
}
}
}
private extension BeginAccessInst {
var isDead: Bool { users.allSatisfy({ $0.isIncidentalUse }) }
}

View File

@@ -94,6 +94,7 @@ private func registerSwiftPasses() {
registerPass(cleanupDebugStepsPass, { cleanupDebugStepsPass.run($0) })
registerPass(namedReturnValueOptimization, { namedReturnValueOptimization.run($0) })
registerPass(stripObjectHeadersPass, { stripObjectHeadersPass.run($0) })
registerPass(deadAccessScopeElimination, { deadAccessScopeElimination.run($0) })
registerPass(deadStoreElimination, { deadStoreElimination.run($0) })
registerPass(redundantLoadElimination, { redundantLoadElimination.run($0) })
registerPass(mandatoryRedundantLoadElimination, { mandatoryRedundantLoadElimination.run($0) })

View File

@@ -287,3 +287,66 @@ extension IntrusiveSet {
insert(contentsOf: source)
}
}
/// A set which supports iterating over its elements.
/// The iteration order is deterministic. All set operations are O(1).
public struct IterableSet<Set: IntrusiveSet> : CollectionLikeSequence {
public typealias Element = Set.Element
private var set: Set
// Erased elements do not get removed from `list`, because this would be O(n).
private var list: Stack<Element>
// Elements which are in `list. This is different from `set` if elements get erased.
private var inList: Set
public init(_ context: some Context) {
self.set = Set(context)
self.list = Stack(context)
self.inList = Set(context)
}
public mutating func deinitialize() {
inList.deinitialize()
list.deinitialize()
set.deinitialize()
}
public func contains(_ element: Element) -> Bool {
set.contains(element)
}
/// Returns true if `element` was not contained in the set before inserting.
@discardableResult
public mutating func insert(_ element: Element) -> Bool {
if inList.insert(element) {
list.append(element)
}
return set.insert(element)
}
public mutating func erase(_ element: Element) {
set.erase(element)
}
public func makeIterator() -> Iterator { Iterator(base: list.makeIterator(), set: set) }
public struct Iterator: IteratorProtocol {
var base: Stack<Element>.Iterator
let set: Set
public mutating func next() -> Element? {
while let n = base.next() {
if set.contains(n) {
return n
}
}
return nil
}
}
}
public typealias IterableInstructionSet = IterableSet<InstructionSet>
public typealias SpecificIterableInstructionSet<InstType: Instruction> = IterableSet<SpecificInstructionSet<InstType>>
public typealias IterableBasicBlockSet = IterableSet<BasicBlockSet>

View File

@@ -70,7 +70,54 @@ public struct Worklist<Set: IntrusiveSet> : CustomStringConvertible, NoReflectio
}
}
/// Like `Worklist` but can store an additional arbitrary payload per element.
public struct WorklistWithPayload<Set: IntrusiveSet, Payload> : CustomStringConvertible, NoReflectionChildren {
public typealias Element = Set.Element
private var worklist: Stack<(Element, Payload)>
private var pushedElements: Set
public init(_ context: some Context) {
self.worklist = Stack(context)
self.pushedElements = Set(context)
}
public mutating func pop() -> (Element, Payload)? { return worklist.pop() }
public mutating func pushIfNotVisited(_ element: Element, with payload: Payload) {
if pushedElements.insert(element) {
worklist.append((element, payload))
}
}
public mutating func pushIfNotVisited<S: Sequence>(contentsOf other: S, with payload: Payload)
where S.Element == Element
{
for element in other {
pushIfNotVisited(element, with: payload)
}
}
/// Returns true if \p element was pushed to the worklist, regardless if it's already popped or not.
public func hasBeenPushed(_ element: Element) -> Bool { pushedElements.contains(element) }
public var isEmpty: Bool { worklist.isEmpty }
public var description: String {
"""
worklist: \(worklist)
pushed: \(pushedElements)
"""
}
/// TODO: once we have move-only types, make this a real deinit.
public mutating func deinitialize() {
pushedElements.deinitialize()
worklist.deinitialize()
}
}
public typealias BasicBlockWorklist = Worklist<BasicBlockSet>
public typealias BasicBlockWorklistWithPayload<Payload> = WorklistWithPayload<BasicBlockSet, Payload>
public typealias InstructionWorklist = Worklist<InstructionSet>
public typealias SpecificInstructionWorklist<InstType: Instruction> = Worklist<SpecificInstructionSet<InstType>>
public typealias ValueWorklist = Worklist<ValueSet>

View File

@@ -143,6 +143,11 @@ public class Instruction : CustomStringConvertible, Hashable {
return bridged.mayAccessPointer()
}
/// True if arbitrary functions may be called by this instruction.
/// This can be either directly, e.g. by an `apply` instruction, or indirectly by destroying a value which
/// might have a deinitializer which can call functions.
public var mayCallFunction: Bool { false }
public final var mayLoadWeakOrUnowned: Bool {
return bridged.mayLoadWeakOrUnowned()
}
@@ -331,6 +336,8 @@ final public class StoreInst : Instruction, StoringInstruction {
public var storeOwnership: StoreOwnership {
StoreOwnership(rawValue: bridged.StoreInst_getStoreOwnership())!
}
public override var mayCallFunction: Bool { storeOwnership == .assign }
}
final public class StoreWeakInst : Instruction, StoringInstruction { }
@@ -345,6 +352,15 @@ final public class AssignInst : Instruction, StoringInstruction {
public var assignOwnership: AssignOwnership {
AssignOwnership(rawValue: bridged.AssignInst_getAssignOwnership())!
}
public override var mayCallFunction: Bool {
switch assignOwnership {
case .unknown, .reassign, .reinitialize:
true
case .initialize:
false
}
}
}
final public class AssignOrInitInst : Instruction, StoringInstruction {}
@@ -382,6 +398,7 @@ final public class CopyAddrInst : Instruction, SourceDestAddrInstruction {
bridged.CopyAddrInst_setIsInitializationOfDest(isInitializationOfDestination)
context.notifyInstructionChanged(self)
}
public override var mayCallFunction: Bool { !isInitializationOfDestination }
}
final public class ExplicitCopyAddrInst : Instruction, SourceDestAddrInstruction {
@@ -394,6 +411,7 @@ final public class ExplicitCopyAddrInst : Instruction, SourceDestAddrInstruction
public var isInitializationOfDestination: Bool {
bridged.ExplicitCopyAddrInst_isInitializationOfDest()
}
public override var mayCallFunction: Bool { !isInitializationOfDestination }
}
final public class MarkUninitializedInst : SingleValueInstruction, UnaryInstruction {
@@ -639,10 +657,12 @@ final public class RetainValueAddrInst : RefCountingInst {
}
final public class ReleaseValueAddrInst : RefCountingInst {
public override var mayCallFunction: Bool { true }
}
final public class StrongReleaseInst : RefCountingInst {
public var instance: Value { operand.value }
public override var mayCallFunction: Bool { true }
}
final public class UnownedReleaseInst : RefCountingInst {
@@ -651,10 +671,12 @@ final public class UnownedReleaseInst : RefCountingInst {
final public class ReleaseValueInst : RefCountingInst {
public var value: Value { return operand.value }
public override var mayCallFunction: Bool { true }
}
final public class UnmanagedReleaseValueInst : RefCountingInst {
public var value: Value { return operand.value }
public override var mayCallFunction: Bool { true }
}
final public class AutoreleaseValueInst : RefCountingInst {}
@@ -667,10 +689,14 @@ final public class DestroyValueInst : Instruction, UnaryInstruction {
/// end the lifetime of its operand.
/// Such `destroy_value` instructions are lowered to no-ops.
public var isDeadEnd: Bool { bridged.DestroyValueInst_isDeadEnd() }
public override var mayCallFunction: Bool { true }
}
final public class DestroyAddrInst : Instruction, UnaryInstruction {
public var destroyedAddress: Value { operand.value }
public override var mayCallFunction: Bool { true }
}
final public class EndLifetimeInst : Instruction, UnaryInstruction {}
@@ -762,6 +788,15 @@ final public class BuiltinInst : SingleValueInstruction {
public var arguments: LazyMapSequence<OperandArray, Value> {
operands.values
}
public override var mayCallFunction: Bool {
switch id {
case .Once, .OnFastPath, .Destroy, .DestroyArray, .DestroyTaskGroup, .DestroyDefaultActor, .Release:
true
default:
false
}
}
}
final public class UpcastInst : SingleValueInstruction, UnaryInstruction {
@@ -1371,6 +1406,8 @@ final public class ApplyInst : SingleValueInstruction, FullApplySite {
public typealias SpecializationInfo = BridgedGenericSpecializationInformation
public var specializationInfo: SpecializationInfo { bridged.ApplyInst_getSpecializationInfo() }
public override var mayCallFunction: Bool { true }
}
final public class FunctionExtractIsolationInst : SingleValueInstruction {}
@@ -1648,7 +1685,12 @@ final public class BeginAccessInst : SingleValueInstruction, UnaryInstruction {
case read = 1
case modify = 2
case `deinit` = 3
public func conflicts(with other: AccessKind) -> Bool {
return self != .read || other != .read
}
}
public var accessKind: AccessKind {
AccessKind(rawValue: bridged.BeginAccessInst_getAccessKind())!
}
@@ -1730,11 +1772,15 @@ final public class BeginApplyInst : MultipleValueInstruction, FullApplySite {
public var isNonThrowing: Bool { bridged.BeginApplyInst_getNonThrowing() }
public var isNonAsync: Bool { bridged.BeginApplyInst_getNonAsync() }
public override var mayCallFunction: Bool { true }
}
final public class EndApplyInst : SingleValueInstruction, UnaryInstruction {
public var token: MultipleValueInstructionResult { operand.value as! MultipleValueInstructionResult }
public var beginApply: BeginApplyInst { token.parentInstruction as! BeginApplyInst }
public override var mayCallFunction: Bool { true }
}
final public class AbortApplyInst : Instruction, UnaryInstruction {
@@ -1931,6 +1977,8 @@ final public class TryApplyInst : TermInst, FullApplySite {
public var isNonAsync: Bool { bridged.TryApplyInst_getNonAsync() }
public var specializationInfo: ApplyInst.SpecializationInfo { bridged.TryApplyInst_getSpecializationInfo() }
public override var mayCallFunction: Bool { true }
}
final public class BranchInst : TermInst {

View File

@@ -101,6 +101,8 @@ PASS(EarlyRedundantLoadElimination, "early-redundant-load-elimination",
"Early Redundant Load Elimination")
PASS(RedundantLoadElimination, "redundant-load-elimination",
"Redundant Load Elimination")
PASS(DeadAccessScopeElimination, "dead-access-scope-elimination",
"Removes dead, non-conflicting access scopes")
PASS(DeadStoreElimination, "dead-store-elimination",
"Dead Store Elimination")
PASS(LifetimeDependenceDiagnostics,

View File

@@ -64,7 +64,6 @@ namespace {
SILValue
visitUncheckedTrivialBitCastInst(UncheckedTrivialBitCastInst *UTBCI);
SILValue visitEndCOWMutationInst(EndCOWMutationInst *ECM);
SILValue visitBeginAccessInst(BeginAccessInst *BAI);
SILValue visitMetatypeInst(MetatypeInst *MTI);
SILValue visitConvertFunctionInst(ConvertFunctionInst *cfi);
@@ -419,16 +418,6 @@ visitUncheckedBitwiseCastInst(UncheckedBitwiseCastInst *UBCI) {
return SILValue();
}
SILValue InstSimplifier::visitBeginAccessInst(BeginAccessInst *BAI) {
// Remove "dead" begin_access.
if (llvm::all_of(BAI->getUses(), [](Operand *operand) -> bool {
return isIncidentalUse(operand->getUser());
})) {
return BAI->getOperand();
}
return SILValue();
}
SILValue InstSimplifier::visitConvertFunctionInst(ConvertFunctionInst *cfi) {
// Eliminate round trip convert_function. Non round-trip is performed in
// SILCombine.

View File

@@ -599,6 +599,7 @@ void addFunctionPasses(SILPassPipelinePlan &P,
P.addRedundantPhiElimination();
P.addCSE();
P.addDCE();
P.addDeadAccessScopeElimination();
// Perform retain/release code motion and run the first ARC optimizer.
P.addEarlyCodeMotion();

View File

@@ -54,7 +54,7 @@ static bool seemsUseful(SILInstruction *I) {
// Even though begin_access/destroy_value/copy_value/end_lifetime have
// side-effects, they can be DCE'ed if they do not have useful
// dependencies/reverse dependencies
if (isa<BeginAccessInst>(I) || isa<CopyValueInst>(I) ||
if (isa<CopyValueInst>(I) ||
isa<DestroyValueInst>(I) || isa<EndLifetimeInst>(I) ||
isa<EndBorrowInst>(I))
return false;
@@ -361,12 +361,6 @@ void DCE::markLive() {
}
break;
}
case SILInstructionKind::EndAccessInst: {
// An end_access is live only if it's begin_access is also live.
auto *beginAccess = cast<EndAccessInst>(&I)->getBeginAccess();
addReverseDependency(beginAccess, &I);
break;
}
case SILInstructionKind::DestroyValueInst: {
auto phi = PhiValue(I.getOperand(0));
// Disable DCE of phis which are lexical or may have a pointer escape.

View File

@@ -0,0 +1,309 @@
// RUN: %target-sil-opt %s -dead-access-scope-elimination | %FileCheck %s
sil_stage canonical
import Swift
import Builtin
sil_global @g1: $Int
sil_global @g2: $Int
sil @unknown : $@convention(thin) () -> ()
sil @readonly : $@convention(thin) () -> () {
[global: read]
}
sil @pure : $@convention(thin) () -> () {
[global: ]
}
// CHECK-LABEL: sil [ossa] @simple_dead :
// CHECK-NOT: begin_access
// CHECK-NOT: end_access
// CHECK: } // end sil function 'simple_dead'
sil [ossa] @simple_dead : $@convention(thin) () -> () {
bb0:
%0 = global_addr @g1 : $*Int
%2 = begin_access [modify] [dynamic] %0
fix_lifetime %2
debug_value %2 : $*Int, name "g1"
end_access %2
%r = tuple ()
return %r
}
// CHECK-LABEL: sil [ossa] @simple_alive :
// CHECK: [[A:%.*]] = begin_access
// CHECK: [[L:%.*]] = load [trivial] [[A]]
// CHECK: return [[L]]
// CHECK: } // end sil function 'simple_alive'
sil [ossa] @simple_alive : $@convention(thin) () -> Int {
bb0:
%0 = global_addr @g1 : $*Int
%2 = begin_access [modify] [dynamic] %0
%3 = load [trivial] %2
end_access %2
return %3
}
// CHECK-LABEL: sil [ossa] @conflicting :
// CHECK: %1 = begin_access [modify]
// CHECK: %2 = begin_access [read]
// CHECK: end_access %2
// CHECK: end_access %1
// CHECK: } // end sil function 'conflicting'
sil [ossa] @conflicting : $@convention(thin) () -> () {
bb0:
%0 = global_addr @g1 : $*Int
%1 = begin_access [modify] [dynamic] %0
%2 = begin_access [read] [dynamic] %0
end_access %2
end_access %1
%r = tuple ()
return %r
}
// CHECK-LABEL: sil [ossa] @not_conflicting :
// CHECK-NOT: begin_access
// CHECK-NOT: end_access
// CHECK: } // end sil function 'not_conflicting'
sil [ossa] @not_conflicting : $@convention(thin) () -> () {
bb0:
%0 = global_addr @g1 : $*Int
%1 = begin_access [read] [dynamic] %0
%2 = begin_access [read] [dynamic] %0
end_access %2
end_access %1
%r = tuple ()
return %r
}
// CHECK-LABEL: sil [ossa] @not_aliasing :
// CHECK-NOT: begin_access
// CHECK-NOT: end_access
// CHECK: } // end sil function 'not_aliasing'
sil [ossa] @not_aliasing : $@convention(thin) () -> () {
bb0:
%0 = global_addr @g1 : $*Int
%1 = global_addr @g2 : $*Int
%2 = begin_access [modify] [dynamic] %0
%3 = begin_access [read] [dynamic] %1
end_access %3
end_access %2
%r = tuple ()
return %r
}
// CHECK-LABEL: sil [ossa] @call_with_potential_access :
// CHECK: %2 = begin_access [read]
// CHECK: %3 = begin_access [read]
// CHECK: end_access %3
// CHECK: end_access %2
// CHECK: } // end sil function 'call_with_potential_access'
sil [ossa] @call_with_potential_access : $@convention(thin) () -> () {
bb0:
%0 = global_addr @g1 : $*Int
%1 = global_addr @g2 : $*Int
%2 = begin_access [read] [dynamic] %0
%3 = begin_access [read] [dynamic] %1
%4 = function_ref @unknown : $@convention(thin) () -> ()
%5 = apply %4() : $@convention(thin) () -> ()
end_access %3
end_access %2
%r = tuple ()
return %r
}
// CHECK-LABEL: sil [ossa] @call_pure :
// CHECK-NOT: begin_access
// CHECK-NOT: end_access
// CHECK: } // end sil function 'call_pure'
sil [ossa] @call_pure : $@convention(thin) () -> () {
bb0:
%0 = global_addr @g1 : $*Int
%1 = global_addr @g2 : $*Int
%2 = begin_access [read] [dynamic] %0
%3 = begin_access [read] [dynamic] %1
%4 = function_ref @pure : $@convention(thin) () -> ()
%5 = apply %4() : $@convention(thin) () -> ()
end_access %3
end_access %2
%r = tuple ()
return %r
}
// CHECK-LABEL: sil [ossa] @call_readonly :
// CHECK: %3 = begin_access
// CHECK-NEXT: apply
// CHECK-NEXT: end_access %3
// CHECK-NEXT: apply
// CHECK-NEXT: tuple
// CHECK: } // end sil function 'call_readonly'
sil [ossa] @call_readonly : $@convention(thin) () -> () {
bb0:
%0 = global_addr @g1 : $*Int
%1 = global_addr @g2 : $*Int
%2 = function_ref @readonly : $@convention(thin) () -> ()
%3 = begin_access [modify] [dynamic] %0
%4 = apply %2() : $@convention(thin) () -> ()
end_access %3
%6 = begin_access [read] [dynamic] %0
%7 = apply %2() : $@convention(thin) () -> ()
end_access %6
%r = tuple ()
return %r
}
// CHECK-LABEL: sil [ossa] @unknown_call_via_destructor :
// CHECK: %2 = begin_access [read]
// CHECK: end_access %2
// CHECK: } // end sil function 'unknown_call_via_destructor'
sil [ossa] @unknown_call_via_destructor : $@convention(thin) (@owned AnyObject) -> () {
bb0(%0: @owned $AnyObject):
%1 = global_addr @g1 : $*Int
%2 = begin_access [read] [dynamic] %1
destroy_value %0
end_access %2
%r = tuple ()
return %r
}
// CHECK-LABEL: sil [ossa] @two_inner_accesses :
// CHECK: %1 = begin_access [read]
// CHECK-NEXT: %2 = begin_access [modify]
// CHECK-NEXT: end_access %2
// CHECK-NEXT: end_access %1
// CHECK: } // end sil function 'two_inner_accesses'
sil [ossa] @two_inner_accesses : $@convention(thin) () -> () {
bb0:
%0 = global_addr @g1 : $*Int
%1 = begin_access [read] [dynamic] %0
%2 = begin_access [modify] [dynamic] %0
end_access %2
%4 = begin_access [read] [dynamic] %0
end_access %4
end_access %1
%r = tuple ()
return %r
}
// CHECK-LABEL: sil [ossa] @partially_overlapping_accesses :
// CHECK: %2 = begin_access [read] [dynamic] %1
// CHECK: apply
// CHECK-NEXT: end_access %2
// CHECK-NEXT: tuple
// CHECK: } // end sil function 'partially_overlapping_accesses'
sil [ossa] @partially_overlapping_accesses : $@convention(thin) () -> () {
bb0:
%0 = global_addr @g1 : $*Int
%1 = global_addr @g2 : $*Int
%2 = begin_access [read] [dynamic] %0
%3 = begin_access [read] [dynamic] %1
end_access %2
%4 = function_ref @unknown : $@convention(thin) () -> ()
%5 = apply %4() : $@convention(thin) () -> ()
%6 = begin_access [read] [dynamic] %0
end_access %3
end_access %6
%r = tuple ()
return %r
}
// CHECK-LABEL: sil [ossa] @multi_block1 :
// CHECK: bb0:
// CHECK: %1 = begin_access
// CHECK: bb1:
// CHECK: %3 = begin_access
// CHECK: bb2:
// CHECK: %6 = begin_access
// CHECK: bb3:
// CHECK: %9 = begin_access
// CHECK: } // end sil function 'multi_block1'
sil [ossa] @multi_block1 : $@convention(thin) () -> () {
bb0:
%0 = global_addr @g1 : $*Int
%1 = begin_access [modify] [dynamic] %0
cond_br undef, bb1, bb2
bb1:
%3 = begin_access [read] [dynamic] %0
end_access %3
br bb3
bb2:
%6 = begin_access [read] [dynamic] %0
end_access %6
br bb3
bb3:
%9 = begin_access [read] [dynamic] %0
end_access %9
end_access %1
%r = tuple ()
return %r
}
// CHECK-LABEL: sil [ossa] @multi_block2 :
// CHECK: bb0:
// CHECK: %1 = begin_access
// CHECK: bb1:
// CHECK: %3 = begin_access
// CHECK: bb2:
// CHECK: end_access %1
// CHECK-NEXT: br bb3
// CHECK: bb3:
// CHECK-NEXT: tuple
// CHECK: } // end sil function 'multi_block2'
sil [ossa] @multi_block2 : $@convention(thin) () -> () {
bb0:
%0 = global_addr @g1 : $*Int
%1 = begin_access [modify] [dynamic] %0
cond_br undef, bb1, bb2
bb1:
%3 = begin_access [read] [dynamic] %0
end_access %3
end_access %1
br bb3
bb2:
end_access %1
%8 = begin_access [read] [dynamic] %0
end_access %8
br bb3
bb3:
%11 = begin_access [read] [dynamic] %0
end_access %11
%r = tuple ()
return %r
}
// CHECK-LABEL: sil [ossa] @nested_static :
// CHECK-NOT: begin_access [modify] [static]
// CHECK: } // end sil function 'nested_static'
sil [ossa] @nested_static : $@convention(thin) () -> () {
bb0:
%0 = global_addr @g1 : $*Int
%1 = begin_access [modify] [dynamic] %0
%2 = begin_access [modify] [static] %1
end_access %2
end_access %1
%r = tuple ()
return %r
}
// CHECK-LABEL: sil [ossa] @non_dead_inside_dead :
// CHECK: begin_access [modify]
// CHECK: begin_access [read]
// CHECK: } // end sil function 'non_dead_inside_dead'
sil [ossa] @non_dead_inside_dead : $@convention(thin) () -> Int {
bb0:
%0 = global_addr @g1 : $*Int
%1 = begin_access [modify] [dynamic] %0
%2 = begin_access [read] [dynamic] %0
%3 = load [trivial] %2
end_access %2
end_access %1
return %3
}

View File

@@ -253,11 +253,10 @@ bb3:
br bb1
}
// Check that DCE eliminates dead access instructions.
// Dead accesses must not be removed
// CHECK-LABEL: sil @dead_access
// CHECK: bb0
// CHECK-NEXT: tuple
// CHECK-NEXT: return
// CHECK: begin_access
// CHECK: begin_access
// CHECK-LABEL: end sil function 'dead_access'
sil @dead_access : $@convention(thin) (@in Container) -> () {
bb0(%0 : $*Container):

View File

@@ -290,11 +290,10 @@ bb3:
br bb1
}
// Check that DCE eliminates dead access instructions.
// Dead accesses must not be removed
// CHECK-LABEL: sil [ossa] @dead_access :
// CHECK: bb0
// CHECK-NEXT: tuple
// CHECK-NEXT: return
// CHECK: begin_access
// CHECK: begin_access
// CHECK-LABEL: } // end sil function 'dead_access'
sil [ossa] @dead_access : $@convention(thin) (@in Container) -> () {
bb0(%0 : $*Container):

View File

@@ -0,0 +1,67 @@
// RUN: %target-run-simple-swift(-Onone)
// RUN: %target-run-simple-swift(-O)
// REQUIRES: executable_test
import StdlibUnittest
var tests = TestSuite("exclusivity checking")
struct NC: ~Copyable {
var i: Int = 1
mutating func add(_ other: borrowing Self) {
i += other.i
i += other.i
print(self.i, other.i)
}
}
class C1 {
var nc = NC()
func foo() {
nc.add(nc)
}
}
struct S {
var i: Int = 1
mutating func add(_ c: C2) {
let other = c.getS()
i += other.i
i += other.i
print(self.i, other.i)
}
}
final class C2 {
var s = S()
@inline(never)
func getS() -> S { s }
func foo() {
s.add(self)
}
}
tests.test("non-copyable type")
.crashOutputMatches("Simultaneous accesses")
.code {
expectCrashLater()
C1().foo()
}
tests.test("copyable type")
.crashOutputMatches("Simultaneous accesses")
.code {
expectCrashLater()
C2().foo()
}
runAllTests()

View File

@@ -325,34 +325,18 @@ public class B {
deinit
}
// simplify access markers
sil @simplify_accesssil : $@convention(method) (@guaranteed A) -> () {
// CHECK-LABEL: sil @dont_remove_empty_access_scopes : $@convention(method) (@guaranteed A) -> () {
// CHECK: begin_access
// CHECK-LABEL: } // end sil function 'dont_remove_empty_access_scopes'
sil @dont_remove_empty_access_scopes : $@convention(method) (@guaranteed A) -> () {
bb0(%0 : $A):
%badr = ref_element_addr %0 : $A, #A.b
%ba1 = begin_access [read] [dynamic] %badr : $*B
end_access %ba1 : $*B
%ba2 = begin_access [read] [dynamic] %badr : $*B
fix_lifetime %ba2 : $*B
end_access %ba2 : $*B
%ba3 = begin_access [read] [dynamic] %badr : $*B
%b = load %ba3 : $*B
end_access %ba3 : $*B
%xadr = ref_element_addr %b : $B, #B.x
%xaccess = begin_access [read] [dynamic] %xadr : $*Int
end_access %xaccess : $*Int
%r = tuple ()
return %r : $()
}
// CHECK-LABEL: sil @simplify_accesssil : $@convention(method) (@guaranteed A) -> () {
// CHECK: bb0(%0 : $A):
// CHECK-NEXT: [[ADR:%.*]] = ref_element_addr %0 : $A, #A.b
// CHECK-NEXT: fix_lifetime [[ADR]] : $*B
// CHECK-NEXT: %{{.*}} = tuple ()
// CHECK-NEXT: return %{{.*}} : $()
// CHECK-LABEL: } // end sil function 'simplify_accesssil'
// Test replacement of metatype instructions with metatype arguments.
protocol SomeP {}

View File

@@ -370,31 +370,14 @@ public class B {
// simplify access markers
// CHECK-LABEL: sil [ossa] @simplify_accesssil : $@convention(method) (@guaranteed A) -> () {
// CHECK: bb0(%0 : @guaranteed $A):
// CHECK-NEXT: [[ADR:%.*]] = ref_element_addr %0 : $A, #A.b
// CHECK-NEXT: fix_lifetime [[ADR]] : $*B
// CHECK-NEXT: %{{.*}} = tuple ()
// CHECK-NEXT: return %{{.*}} : $()
// CHECK: } // end sil function 'simplify_accesssil'
sil [ossa] @simplify_accesssil : $@convention(method) (@guaranteed A) -> () {
// CHECK-LABEL: sil [ossa] @dont_remove_empty_access_scopes : $@convention(method) (@guaranteed A) -> () {
// CHECK: begin_access
// CHECK: } // end sil function 'dont_remove_empty_access_scopes'
sil [ossa] @dont_remove_empty_access_scopes : $@convention(method) (@guaranteed A) -> () {
bb0(%0 : @guaranteed $A):
%badr = ref_element_addr %0 : $A, #A.b
%ba1 = begin_access [read] [dynamic] %badr : $*B
end_access %ba1 : $*B
%ba2 = begin_access [read] [dynamic] %badr : $*B
fix_lifetime %ba2 : $*B
end_access %ba2 : $*B
%ba3 = begin_access [read] [dynamic] %badr : $*B
%b = load [copy] %ba3 : $*B
end_access %ba3 : $*B
%bb = begin_borrow %b : $B
%xadr = ref_element_addr %bb : $B, #B.x
%xaccess = begin_access [read] [dynamic] %xadr : $*Int
end_access %xaccess : $*Int
end_borrow %bb : $B
destroy_value %b : $B
%r = tuple ()
return %r : $()
}