Skip to content

Commit 0802375

Browse files
authored
Merge pull request #84092 from atrick/62-lifedep-tempalloc
[6.2] LifetimeDependenceDiagnostics: check for on-stack trivial copies
2 parents e9fd280 + d2dea32 commit 0802375

File tree

7 files changed

+587
-171
lines changed

7 files changed

+587
-171
lines changed

SwiftCompilerSources/Sources/Optimizer/FunctionPasses/LifetimeDependenceScopeFixup.swift

Lines changed: 92 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ let lifetimeDependenceScopeFixupPass = FunctionPass(
106106

107107
let localReachabilityCache = LocalVariableReachabilityCache()
108108

109+
var mustFixStackNesting = false
109110
for instruction in function.instructions {
110111
guard let markDep = instruction as? MarkDependenceInstruction else {
111112
continue
@@ -122,6 +123,7 @@ let lifetimeDependenceScopeFixupPass = FunctionPass(
122123
guard scopeExtension.extendScopes(dependence: newLifetimeDep) else {
123124
continue
124125
}
126+
mustFixStackNesting = mustFixStackNesting || scopeExtension.mustFixStackNesting
125127
let args = scopeExtension.findArgumentDependencies()
126128

127129
// If the scope cannot be extended to the caller, this must be the outermost dependency level.
@@ -133,6 +135,9 @@ let lifetimeDependenceScopeFixupPass = FunctionPass(
133135
// Redirect the dependence base to the function arguments. This may create additional mark_dependence instructions.
134136
markDep.redirectFunctionReturn(to: args, context)
135137
}
138+
if mustFixStackNesting {
139+
context.fixStackNesting(in: function)
140+
}
136141
}
137142

138143
private extension Type {
@@ -285,6 +290,9 @@ private struct ScopeExtension {
285290
// Initialized after walking dependent uses. True if the scope can be extended into the caller.
286291
var dependsOnCaller: Bool?
287292

293+
// Does scope extension potentially invalidate stack nesting?
294+
var mustFixStackNesting = false
295+
288296
// Scopes listed in RPO over an upward walk. The outermost scope is first.
289297
var scopes = SingleInlineArray<ExtendableScope>()
290298

@@ -357,42 +365,70 @@ extension ScopeExtension {
357365
private struct ExtendableScope {
358366
enum Introducer {
359367
case scoped(ScopedInstruction)
368+
case stack(Instruction)
360369
case owned(Value)
361370
}
362371

372+
// scope.allocStackInstruction is always valid for Introducer.allocStack and is valid for Introducer.scoped when
373+
// ScopedInstruction is a store_borrow.
363374
let scope: LifetimeDependence.Scope
364375
let introducer: Introducer
365376

366377
var firstInstruction: Instruction {
367378
switch introducer {
368379
case let .scoped(scopedInst):
369380
return scopedInst.instruction
381+
case let .stack(initializingStore):
382+
return initializingStore
370383
case let .owned(value):
371384
if let definingInst = value.definingInstructionOrTerminator {
372385
return definingInst
373386
}
374387
return value.parentBlock.instructions.first!
375388
}
376389
}
390+
377391
var endInstructions: LazyMapSequence<LazyFilterSequence<UseList>, Instruction> {
378392
switch introducer {
379393
case let .scoped(scopedInst):
380394
return scopedInst.endOperands.users
395+
case .stack:
396+
// For alloc_stack without a store-borrow scope, include the deallocs in its scope to ensure that we never shorten
397+
// the original allocation. It's possible that some other use depends on the address.
398+
//
399+
// Same as 'AllocStackInst.deallocations' but as an Instruction list...
400+
return scope.allocStackInstruction!.uses.lazy.filter
401+
{ $0.instruction is DeallocStackInst }.lazy.map { $0.instruction }
402+
381403
case let .owned(value):
382404
return value.uses.endingLifetime.users
383405
}
384406
}
385407

386-
// Allow scope extension as long as `beginInst` is scoped instruction and does not define a variable scope.
408+
var deallocs: LazyMapSequence<LazyFilterSequence<UseList>, DeallocStackInst>? {
409+
guard let allocStack = scope.allocStackInstruction else {
410+
return nil
411+
}
412+
return allocStack.uses.users(ofType: DeallocStackInst.self)
413+
}
414+
415+
// Allow scope extension as long as `beginInst` does not define a variable scope and is either a scoped instruction or
416+
// a store to a singly-initialized temporary.
387417
init?(_ scope: LifetimeDependence.Scope, beginInst: Instruction?) {
388418
self.scope = scope
389419
guard let beginInst = beginInst, VariableScopeInstruction(beginInst) == nil else {
390420
return nil
391421
}
392-
guard let scopedInst = beginInst as? ScopedInstruction else {
393-
return nil
422+
// Check for "scoped" store_borrow extension before checking allocStackInstruction.
423+
if let scopedInst = beginInst as? ScopedInstruction {
424+
self.introducer = .scoped(scopedInst)
425+
return
394426
}
395-
self.introducer = .scoped(scopedInst)
427+
if scope.allocStackInstruction != nil {
428+
self.introducer = .stack(beginInst)
429+
return
430+
}
431+
return nil
396432
}
397433

398434
// Allow extension of owned temporaries that
@@ -459,11 +495,14 @@ extension ScopeExtension {
459495
switch initializer {
460496
case let .store(initializingStore: store, initialAddress: _):
461497
if let sb = store as? StoreBorrowInst {
462-
// Follow the source for nested scopes.
498+
// Follow the stored value since the owner of the borrowed value needs to cover this allocation.
463499
gatherExtensions(valueOrAddress: sb.source)
464-
scopes.push(ExtendableScope(scope, beginInst: sb)!)
500+
}
501+
if scope.allocStackInstruction != nil {
502+
scopes.push(ExtendableScope(scope, beginInst: store)!)
465503
return
466504
}
505+
break
467506
case .argument, .yield:
468507
// TODO: extend indirectly yielded scopes.
469508
break
@@ -742,9 +781,9 @@ extension ScopeExtension {
742781
// Extend the scopes that actually required extension.
743782
//
744783
// Consumes 'useRange'
745-
private func extend(scopesToExtend: SingleInlineArray<ExtendableScope>,
746-
over useRange: inout InstructionRange,
747-
_ context: some MutatingContext) {
784+
private mutating func extend(scopesToExtend: SingleInlineArray<ExtendableScope>,
785+
over useRange: inout InstructionRange,
786+
_ context: some MutatingContext) {
748787
var deadInsts = [Instruction]()
749788
for extScope in scopesToExtend {
750789
// Extend 'useRange' to to cover this scope's end instructions. 'useRange' cannot be extended until the
@@ -772,6 +811,9 @@ extension ScopeExtension {
772811

773812
// Delete original end instructions.
774813
for deadInst in deadInsts {
814+
if deadInst is DeallocStackInst {
815+
mustFixStackNesting = true
816+
}
775817
context.erase(instruction: deadInst)
776818
}
777819
}
@@ -788,13 +830,10 @@ extension ExtendableScope {
788830
switch initializer {
789831
case .argument, .yield:
790832
// A yield is already considered nested within the coroutine.
791-
break
792-
case let .store(initializingStore, _):
793-
if let sb = initializingStore as? StoreBorrowInst {
794-
return canExtend(storeBorrow: sb, over: &range)
795-
}
833+
return true
834+
case .store:
835+
return self.scope.allocStackInstruction != nil
796836
}
797-
return true
798837
default:
799838
// non-yield scopes can always be ended at any point.
800839
return true
@@ -823,27 +862,28 @@ extension ExtendableScope {
823862
return true
824863
}
825864

826-
/// A store borrow is considered to be nested within the scope of its stored values. It is, however, also
827-
/// restricted to the range of its allocation.
828-
///
829-
/// TODO: consider rewriting the dealloc_stack instructions if we ever find that SILGen emits them sooner that
830-
/// we need for lifetime dependencies.
831-
func canExtend(storeBorrow: StoreBorrowInst, over range: inout InstructionRange) -> Bool {
832-
// store_borrow can be extended if all deallocations occur after the use range.
833-
return storeBorrow.allocStack.deallocations.allSatisfy({ !range.contains($0) })
834-
}
835-
836865
/// Extend this scope over the 'range' boundary. Return the old scope ending instructions to be deleted.
837866
func extend(over range: inout InstructionRange, _ context: some MutatingContext) -> [Instruction] {
838867
// Collect the original end instructions and extend the range to to cover them. The resulting access scope
839868
// must cover the original scope because it may protect other memory operations.
840-
let endsToErase = self.endInstructions
841-
var unusedEnds = InstructionSet(context)
842-
for end in endsToErase {
869+
let originalScopeEnds = [Instruction](self.endInstructions)
870+
// Track scope-ending instructions that have not yet been reused as range-ending instructions.
871+
var unreusedEnds = InstructionSet(context)
872+
for end in originalScopeEnds {
843873
assert(range.inclusiveRangeContains(end))
844-
unusedEnds.insert(end)
874+
unreusedEnds.insert(end)
875+
}
876+
defer { unreusedEnds.deinitialize() }
877+
878+
// Never reuse dealloc_stack to avoid running data flow.
879+
var endsToErase = [Instruction]()
880+
if let deallocs = self.deallocs {
881+
endsToErase.append(contentsOf: deallocs.map { $0 })
882+
for dealloc in deallocs {
883+
unreusedEnds.erase(dealloc)
884+
}
845885
}
846-
defer { unusedEnds.deinitialize() }
886+
847887
for end in range.ends {
848888
let location = end.location.autoGenerated
849889
switch end {
@@ -856,60 +896,66 @@ extension ExtendableScope {
856896
// function argument.
857897
let builder = Builder(before: end, location: location, context)
858898
// Insert newEnd so that this scope will be nested in any outer scopes.
859-
range.insert(createEndInstruction(builder, context))
899+
range.insert(contentsOf: createEndInstructions(builder, context))
860900
continue
861901
default:
862902
break
863903
}
864-
if unusedEnds.contains(end) {
865-
unusedEnds.erase(end)
866-
assert(!unusedEnds.contains(end))
904+
// If this range ending instruction was also scope-ending, then mark it as reused by removing it from the set.
905+
if unreusedEnds.contains(end) {
906+
unreusedEnds.erase(end)
907+
assert(!unreusedEnds.contains(end))
867908
continue
868909
}
869910
Builder.insert(after: end, location: location, context) {
870-
range.insert(createEndInstruction($0, context))
911+
range.insert(contentsOf: createEndInstructions($0, context))
871912
}
872913
}
873914
for exitInst in range.exits {
874915
let location = exitInst.location.autoGenerated
875916
let builder = Builder(before: exitInst, location: location, context)
876-
range.insert(createEndInstruction(builder, context))
917+
range.insert(contentsOf: createEndInstructions(builder, context))
877918
}
878-
return endsToErase.filter { unusedEnds.contains($0) }
919+
endsToErase.append(contentsOf: originalScopeEnds.filter { unreusedEnds.contains($0) })
920+
return endsToErase
879921
}
880922

881923
/// Create a scope-ending instruction at 'builder's insertion point.
882-
func createEndInstruction(_ builder: Builder, _ context: some Context) -> Instruction {
924+
func createEndInstructions(_ builder: Builder, _ context: some Context) -> SingleInlineArray<Instruction> {
883925
switch self.scope {
884926
case let .access(beginAccess):
885-
return builder.createEndAccess(beginAccess: beginAccess)
927+
return SingleInlineArray(element: builder.createEndAccess(beginAccess: beginAccess))
886928
case let .borrowed(beginBorrow):
887-
return builder.createEndBorrow(of: beginBorrow.value)
929+
return SingleInlineArray(element: builder.createEndBorrow(of: beginBorrow.value))
888930
case let .yield(yieldedValue):
889931
let beginApply = yieldedValue.definingInstruction as! BeginApplyInst
890932
// createEnd() returns non-nil because beginApply.endReaches() was checked by canExtend()
891-
return beginApply.createEnd(builder, context)!
933+
return SingleInlineArray(element: beginApply.createEnd(builder, context)!)
892934
case let .initialized(initializer):
893935
switch initializer {
894936
case let .store(initializingStore: store, initialAddress: _):
937+
var endInsts = SingleInlineArray<Instruction>()
895938
if let sb = store as? StoreBorrowInst {
896-
// FIXME: we may need to rewrite the dealloc_stack.
897-
return builder.createEndBorrow(of: sb)
939+
endInsts.append(builder.createEndBorrow(of: sb))
940+
}
941+
if let allocStack = self.scope.allocStackInstruction {
942+
endInsts.append(builder.createDeallocStack(allocStack))
943+
return endInsts
898944
}
899945
break
900946
case .argument, .yield:
901947
// TODO: extend indirectly yielded scopes.
902948
break
903949
}
904950
case let .owned(value):
905-
return builder.createDestroyValue(operand: value)
951+
return SingleInlineArray(element: builder.createDestroyValue(operand: value))
906952
case let .local(varInst):
907953
switch varInst {
908954
case let .beginBorrow(beginBorrow):
909955
// FIXME: we may need to rewrite the dealloc_stack.
910-
return builder.createEndBorrow(of: beginBorrow)
956+
return SingleInlineArray(element: builder.createEndBorrow(of: beginBorrow))
911957
case let .moveValue(moveValue):
912-
return builder.createDestroyValue(operand: moveValue)
958+
return SingleInlineArray(element: builder.createDestroyValue(operand: moveValue))
913959
}
914960
default:
915961
break

SwiftCompilerSources/Sources/Optimizer/PassManager/Context.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,10 @@ struct FunctionPassContext : MutatingContext {
444444
notifyInstructionsChanged()
445445
}
446446
}
447+
448+
func fixStackNesting(in function: Function) {
449+
_bridged.fixStackNesting(function.bridged)
450+
}
447451
}
448452

449453
struct SimplifyContext : MutatingContext {

SwiftCompilerSources/Sources/Optimizer/Utilities/LifetimeDependenceUtils.swift

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -381,8 +381,30 @@ extension LifetimeDependence.Scope {
381381
}
382382
}
383383

384+
// Scope helpers.
384385
extension LifetimeDependence.Scope {
385-
/// Ignore "irrelevent" borrow scopes: load_borrow or begin_borrow without [var_decl]
386+
// If the LifetimeDependenceScope is .initialized, then return the alloc_stack.
387+
var allocStackInstruction: AllocStackInst? {
388+
switch self {
389+
case let .initialized(initializer):
390+
switch initializer {
391+
case let .store(initializingStore: store, initialAddress):
392+
if let allocStackInst = initialAddress as? AllocStackInst {
393+
return allocStackInst
394+
}
395+
if let sb = store as? StoreBorrowInst {
396+
return sb.allocStack
397+
}
398+
return nil
399+
default:
400+
return nil
401+
}
402+
default:
403+
return nil
404+
}
405+
}
406+
407+
/// Ignore "irrelevant" borrow scopes: load_borrow or begin_borrow without [var_decl]
386408
func ignoreBorrowScope(_ context: some Context) -> LifetimeDependence.Scope? {
387409
guard case let .borrowed(beginBorrowVal) = self else {
388410
return nil
@@ -456,7 +478,7 @@ extension LifetimeDependence.Scope {
456478
}
457479
let address = initializer.initialAddress
458480
if address.type.objectType.isTrivial(in: address.parentFunction) {
459-
return nil
481+
return LifetimeDependence.Scope.computeAddressableRange(initializer: initializer, context)
460482
}
461483
return LifetimeDependence.Scope.computeInitializedRange(initializer: initializer, context)
462484
case let .unknown(value):
@@ -503,6 +525,46 @@ extension LifetimeDependence.Scope {
503525
}
504526
return range
505527
}
528+
529+
// For trivial values, compute the range in which the value may have its address taken.
530+
// Returns nil unless the address has both an addressable parameter use and a dealloc_stack use.
531+
//
532+
// This extended range is convervative. In theory, it can lead to a "false positive" diagnostic if this scope was
533+
// computed for a apply site that takes this address as *non-addressable*, as opposed to the apply site discovered
534+
// here that takes the alloc_stack as an *addressable* argument. This won't normally happen because addressability
535+
// depends on the value's type (via @_addressableForDepenencies), so all other lifetime-dependent applies should be
536+
// addressable. Furthermore, SILGen only creates temporary stack locations for addressable arguments for a single
537+
// argument.
538+
private static func computeAddressableRange(initializer: AccessBase.Initializer, _ context: Context)
539+
-> InstructionRange? {
540+
switch initializer {
541+
case .argument, .yield:
542+
return nil
543+
case let .store(initializingStore, initialAddr):
544+
guard initialAddr is AllocStackInst else {
545+
return nil
546+
}
547+
var isAddressable = false
548+
var deallocInsts = SingleInlineArray<DeallocStackInst>()
549+
for use in initialAddr.uses {
550+
let inst = use.instruction
551+
switch inst {
552+
case let apply as ApplySite:
553+
isAddressable = isAddressable || apply.isAddressable(operand: use)
554+
case let dealloc as DeallocStackInst:
555+
deallocInsts.append(dealloc)
556+
default:
557+
break
558+
}
559+
}
560+
guard isAddressable, !deallocInsts.isEmpty else {
561+
return nil
562+
}
563+
var range = InstructionRange(begin: initializingStore, context)
564+
range.insert(contentsOf: deallocInsts)
565+
return range
566+
}
567+
}
506568
}
507569

508570
// =============================================================================

0 commit comments

Comments
 (0)