Skip to content

Commit c1e1ef6

Browse files
authored
Merge pull request #84641 from atrick/lifedep-tempaddress-unreachable
LifetimeDependenceDiagnostics: extend temp alloc to unreachable.
2 parents d06d126 + bed80ee commit c1e1ef6

File tree

4 files changed

+127
-3
lines changed

4 files changed

+127
-3
lines changed

SwiftCompilerSources/Sources/Optimizer/FunctionPasses/LifetimeDependenceScopeFixup.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -744,10 +744,9 @@ extension ScopeExtension {
744744

745745
// Append each scope that needs extension to scopesToExtend from the inner to the outer scope.
746746
for extScope in scopes.reversed() {
747-
// An outer scope might not originally cover one of its inner scopes. Therefore, extend 'extendedUseRange' to to
748-
// cover this scope's end instructions. The extended scope must at least cover the original scopes because the
749-
// original scopes may protect other operations.
750747
var mustExtend = false
748+
// Iterating over scopeEndInst ignores unreachable paths which may not include the dealloc_stack. This is fine
749+
// because the stack allocation effectively covers the entire unreachable path.
751750
for scopeEndInst in extScope.endInstructions {
752751
switch extendedUseRange.overlaps(pathBegin: extScope.firstInstruction, pathEnd: scopeEndInst, context) {
753752
case .containsPath, .containsEnd, .disjoint:
@@ -757,6 +756,10 @@ extension ScopeExtension {
757756
break
758757
case .containsBegin, .overlappedByPath:
759758
// containsBegin can occur when the extendable scope has the same begin as the use range.
759+
//
760+
// An outer scope might not originally cover one of its inner scopes. Therefore, extend 'extendedUseRange' to
761+
// to cover this scope's end instructions. The extended scope must at least cover the original scopes because
762+
// the original scopes may protect other operations.
760763
extendedUseRange.insert(scopeEndInst)
761764
break
762765
}

SwiftCompilerSources/Sources/Optimizer/Utilities/LifetimeDependenceUtils.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,10 @@ extension LifetimeDependence.Scope {
428428
///
429429
/// Returns nil if the dependence scope covers the entire function. Returns an empty range for an unknown scope.
430430
///
431+
/// When this Scope is live on unreachable paths, the returned range may include blocks that are not dominated by the
432+
/// scope introducer. Even though 'range.isValid == false' for such a range, it is still valid for checking that
433+
/// dependencies are in scope since we already know that the Scope introducer dominates all dependent uses.
434+
///
431435
/// Ignore the lifetime of temporary trivial values (with .initialized and .unknown scopes). Temporaries have an
432436
/// unknown Scope, which means that LifetimeDependence.Scope did not recognize a VariableScopeInstruction. This is
433437
/// important to promote mark_dependence instructions emitted by SILGen to [nonescaping] (e.g. unsafeAddressor). It
@@ -558,10 +562,32 @@ extension LifetimeDependence.Scope {
558562
}
559563
}
560564
guard isAddressable, !deallocInsts.isEmpty else {
565+
// Valid on all paths to function exit.
561566
return nil
562567
}
563568
var range = InstructionRange(begin: initializingStore, context)
564569
range.insert(contentsOf: deallocInsts)
570+
571+
// Insert unreachable paths with no dealloc_stack.
572+
var forwardUnreachableWalk = BasicBlockWorklist(context)
573+
defer { forwardUnreachableWalk.deinitialize() }
574+
575+
// TODO: ensure complete dealloc_stack on all paths in SIL verification, then assert exitBlock.isEmpty.
576+
for exitBlock in range.exitBlocks {
577+
forwardUnreachableWalk.pushIfNotVisited(exitBlock)
578+
}
579+
while let b = forwardUnreachableWalk.pop() {
580+
if let unreachableInst = b.terminator as? UnreachableInst {
581+
// Note: 'unreachableInst' is not necessarilly dominated by 'initializingStore'. This marks the range invalid,
582+
// but leaves it in a usable state that includes all blocks covered by the temporary allocation. The extra
583+
// blocks (backward up to the function entry) are irrelevant becase we already know that 'initializingStore'
584+
// dominates dependent uses.
585+
range.insert(unreachableInst)
586+
}
587+
for succBlock in b.successors {
588+
forwardUnreachableWalk.pushIfNotVisited(succBlock)
589+
}
590+
}
565591
return range
566592
}
567593
}

test/SILOptimizer/lifetime_dependence/verify_diagnostics.sil

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ sil @addressInt : $@convention(thin) (@in_guaranteed InlineInt) -> @lifetime(bor
7070
sil @addressOfInt : $@convention(thin) (@in_guaranteed Int) -> @lifetime(borrow address 0) @owned Span<Int>
7171
sil @noAddressInt : $@convention(thin) (@in_guaranteed Int) -> @lifetime(borrow 0) @owned Span<Int>
7272
sil @useSpan : $@convention(thin) (@guaranteed Span<Int>) -> ()
73+
sil @useRawSpan : $@convention(thin) (@guaranteed RawSpan) -> @error any Error
74+
75+
sil @getInlineSpan : $@convention(thin) (@in_guaranteed InlineInt) -> @lifetime(borrow address 0) @owned RawSpan
7376

7477
// Test returning a owned dependence on a trivial value
7578
sil [ossa] @return_trivial_dependence : $@convention(thin) (@guaranteed C) -> @lifetime(borrow 0) @owned NE {
@@ -352,3 +355,68 @@ bb0(%0: $Int):
352355
%18 = tuple ()
353356
return %18
354357
}
358+
359+
// Test dependence on the temporary stack address of a trivial value. computeAddressableRange must extend the lifetime
360+
// of %tempAddr into the unreachable.
361+
sil hidden [ossa] @testTempAddressUnreachable : $@convention(thin) (@in_guaranteed InlineInt) -> () {
362+
bb0(%0 : $*InlineInt):
363+
%loadArg = load [trivial] %0
364+
%tempAddr = alloc_stack $InlineInt
365+
store %loadArg to [trivial] %tempAddr
366+
367+
%f1 = function_ref @getInlineSpan : $@convention(thin) (@in_guaranteed InlineInt) -> @lifetime(borrow address 0) @owned RawSpan
368+
%call = apply %f1(%tempAddr) : $@convention(thin) (@in_guaranteed InlineInt) -> @lifetime(borrow address 0) @owned RawSpan
369+
%md = mark_dependence [unresolved] %call on %tempAddr
370+
371+
%f2 = function_ref @useRawSpan : $@convention(thin) (@guaranteed RawSpan) -> @error any Error
372+
try_apply %f2(%md) : $@convention(thin) (@guaranteed RawSpan) -> @error any Error, normal bb1, error bb2
373+
374+
bb1(%void : $()):
375+
destroy_value %md
376+
dealloc_stack %tempAddr
377+
%99 = tuple ()
378+
return %99
379+
380+
bb2(%error : @owned $any Error):
381+
destroy_value [dead_end] %md
382+
unreachable
383+
}
384+
385+
// Test dependence on the temporary stack address of a trivial value. computeAddressableRange must extend the lifetime
386+
// of %tempAddr into the unreachable.
387+
//
388+
// Note that the computed instruction range is marked Invalid because it does not have a single dominating
389+
// block. Nonetheless, the range still include all blocks in which the stack allocation is live, which is all we care
390+
// about.
391+
sil hidden [ossa] @testTempAddressNondominatedUnreachable : $@convention(thin) (@in_guaranteed InlineInt) -> () {
392+
bb0(%0 : $*InlineInt):
393+
cond_br undef, bb1, bb2
394+
395+
bb1:
396+
br bb5
397+
398+
bb2:
399+
%loadArg = load [trivial] %0
400+
%tempAddr = alloc_stack $InlineInt
401+
store %loadArg to [trivial] %tempAddr
402+
403+
%f1 = function_ref @getInlineSpan : $@convention(thin) (@in_guaranteed InlineInt) -> @lifetime(borrow address 0) @owned RawSpan
404+
%call = apply %f1(%tempAddr) : $@convention(thin) (@in_guaranteed InlineInt) -> @lifetime(borrow address 0) @owned RawSpan
405+
%md = mark_dependence [unresolved] %call on %tempAddr
406+
407+
%f2 = function_ref @useRawSpan : $@convention(thin) (@guaranteed RawSpan) -> @error any Error
408+
try_apply %f2(%md) : $@convention(thin) (@guaranteed RawSpan) -> @error any Error, normal bb3, error bb4
409+
410+
bb3(%void : $()):
411+
destroy_value %md
412+
dealloc_stack %tempAddr
413+
%99 = tuple ()
414+
return %99
415+
416+
bb4(%error : @owned $any Error):
417+
destroy_value [dead_end] %md
418+
br bb5
419+
420+
bb5:
421+
unreachable
422+
}

test/SILOptimizer/lifetime_dependence/verify_diagnostics.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,19 @@ func testClosureCapture1(_ a: HasMethods) {
208208
*/
209209
}
210210

211+
public struct InlineInt: BitwiseCopyable {
212+
var val: UInt64
213+
214+
var span: RawSpan {
215+
@_addressableSelf
216+
@_lifetime(borrow self) borrowing get {
217+
let buf = UnsafeRawBufferPointer(start: UnsafeRawPointer(Builtin.addressOfBorrow(val)), count: 1)
218+
let span = RawSpan(_unsafeBytes: buf)
219+
return unsafe _overrideLifetime(span, borrowing: val)
220+
}
221+
}
222+
}
223+
211224
// =============================================================================
212225
// Indirect ~Escapable results
213226
// =============================================================================
@@ -382,3 +395,17 @@ func returnTempBorrow() -> Borrow<Int> {
382395
// expected-note@-1{{it depends on the lifetime of this parent value}}
383396
return span // expected-note{{this use causes the lifetime-dependent value to escape}}
384397
}
398+
399+
// Test dependence on the temporary stack address of a trivial value. computeAddressableRange must extend the lifetime
400+
// of 'inline' into the unreachable.
401+
//
402+
// This test requires InlineInt to be a trivial value defined in this module so that inline.span generates:
403+
//
404+
// %temp = alloc_stack
405+
// store %arg to [trivial] temp
406+
// apply %get_span(%temp)
407+
//
408+
// If we use InlineArray instead, we get a store_borrow, which is a completely different situation.
409+
func test(inline: InlineInt) {
410+
inline.span.withUnsafeBytes { _ = $0 }
411+
}

0 commit comments

Comments
 (0)