Skip to content

Conversation

@nate-chandler
Copy link
Contributor

@nate-chandler nate-chandler commented Aug 8, 2025

Without complete OSSA lifetimes, a value may have an "incomplete lifetime"--it may not be destroyed on paths into dead-end regions. When a value lacks a destroy on a path into a dead-end region, it is effectively destroyed at its availability boundary (e.g. an unreachable instruction). Indeed, lifetime completion amounts to making such implicit destroys explicit (with correct nesting).

SemanticARCOpts' performGuaranteedCopyValueOptimization attempts to eliminate a copy_value of a guaranteed value. To do so, it computes the live range (OwnershipLiveRange) of the copy_value. Among other things, it checks that all destroys of the copy_value are within the scope of all guaranteed bases of the copied value. If any destroys are not within any of those scopes, the copy cannot be eliminated (because the copy would have been originally live beyond the range which it would be live in after copy elimination).

A value with an incomplete lifetime is implicitly destroyed at its availability boundary. So those implicit destroys along the availability boundary must also be within the scopes of those guaranteed bases.

Previously, implicit destroys along availability boundaries were not considered. This resulted in invalid shortening of lifetimes--before the transformation a value would be unconsumed up to its availability boundary; after the transformation it would be consumed somewhere before that. For example:

sil [ossa] @f : $@convention(thin) (@owned Outer) -> () {
entry(%o : $*Outer):
  %o_borrow = begin_borrow %o : $Outer
  %i = struct_extract %o_borrow : $Outer, #Outer.inner
  %i_copy = copy_value %i : $Inner
  end_borrow %o_borrow : $Outer
  destroy_value %o : $Outer
  apply @g(%i_copy) : $@convention(thin) (@guaranteed Inner) -> ()
  unreachable // %i_copy implicitly destroyed
}

Here, %i_copy is implicitly destroyed at unreachable but %i is consumed at the scope end of %o_borrow. That means that an attempt to RAUW %i_copy with %i would be invalid because uses of %i_copy may be outside the live range of %i.

(Note that this happens not to occur in the above example because there's a bailout in the case that there are no destroys, but perturbing the example to include a cond_br on one branch of which the value is destroyed results in the miscompile described above.)

Here, this is fixed by adding the instructions on the availability boundary at which the value is implicitly destroyed to the live range. Do this by adding the instructions on the availability boundary of each def from which the live range is generated to the live range.

One wrinkle is that the OwnershipLiveRange utility is used in one place by a utility when SIL is in an invalid state (phi uses have been replaced with undef so values leaks on those paths). Account for this by manually fixing up the liveness instance passed to the lifetime completion utility.

rdar://157291161

Put it alongsidde OperandToValue and friends.
For historical reasons, there was an API to check whether operands were
within the boundary which just checked whether those operands' users
were within the buondary.  Make a copy of the method deal in
instructions and factor the original API through it.
For historical reasons, the existing function
(`areUsesWithinExtendedScope`) trafficked in operands rather than
instructions.  Now that PrunedLiveness has an API that deals with
instructions, add a function (`areWithinExtendedScope`) which does as
well.  Factor the existing function through this new function.
The type is a union of an Operand (a real use) and a SILInstruction (an
implicit use).  Such a type is needed to reflect the fact that with
incomplete lifetimes, values can be implicitly destroyed at the
terminators of blocks in dead end regions (along the vaule's
availability boundary).
Instead of tracking Operands, track UsePoints, i.e.
`Either<Operand, SILInstruction>`s.  For now, all use points wil remain
operands.  This change is necessary to allow tracking terminators in
dead-end region as implicit uses.
It will be fixed in a subsequent commit.
With incomplete lifetimes, a value may have an "incomplete lifetime"--it
may not be destroyed on paths into dead-end regions.  When a value lacks
a destroy on a path into a dead-end region, it is effectively destroyed
at its availability boundary (e.g. an `unreachable` instruction).
Indeed, lifetime completion amounts to making such implicit destroys
explicit (with correct nesting).

`SemanticARCOpts`' `performGuaranteedCopyValueOptimization` attempts to
eliminate a `copy_value` of a guaranteed value.  To do so, it computes
the live range (`OwnershipLiveRange`) of the copy_value.  Among other
things, it checks that all destroys of the copy_value are within the
scope of all guaranteed bases of the copied value.  If any destroys are
_not_ within any of those scopes, the copy cannot be eliminated (because
the copy would have been originally live beyond the range which it would
be live in after copy elimination).

A value with an incomplete lifetime is implicitly destroyed at its
availability boundary.  So those implicit destroys along the
availability boundary must _also_ be within the scopes of those
guaranteed bases.

Previously, implicit destroys along availability boundaries were not
considered.  This resulted in invalid shortening of lifetimes--before
the transformation a value would be unconsumed up to its availability
boundary; after the transformation it would be consumed somewhere before
that.  For example:

```
sil [ossa] @f : $@convention(thin) (@owned Outer) -> () {
entry(%o : $*Outer):
  %o_borrow = begin_borrow %o : $Outer
  %i = struct_extract %o_borrow : $Outer, #Outer.inner
  %i_copy = copy_value %i : $Inner
  end_borrow %o_borrow : $Outer
  destroy_value %o : $Outer
  apply @g(%i_copy) : $@convention(thin) (@guaranteed Inner) -> ()
  unreachable // %i_copy implicitly destroyed
}
```

Here, `%i_copy` is implicitly destroyed at `unreachable` but `%i` is
consumed at the scope end of `%o_borrow`.  That means that an attempt to
RAUW `%i_copy` with `%i` would be invalid because uses of `%i_copy` may
be outside the live range of `%i`.

(Note that this happens not to occur in the above example because
there's a bailout in the case that there are no destroys, but perturbing
the example to include a cond_br on one branch of which the value is
destroyed results in the miscompile described above.)

Here, this is fixed by adding the instructions on the availability
boundary at which the value is implicitly destroyed to the live range.
Do this by adding the instructions on the availability boundary of each
def from which the live range is generated to the live range.

One wrinkle is that the OwnershipLiveRange utility is used in one place
by a utility when SIL is in an invalid state (phi uses have been
replaced with undef so values leaks on those paths).  Account for this
by manually fixing up the liveness instance passed to the lifetime
completion utility.

rdar://157291161
@nate-chandler
Copy link
Contributor Author

@swift-ci please test

@nate-chandler
Copy link
Contributor Author

@swift-ci please apple silicon benchmark

@nate-chandler
Copy link
Contributor Author

@swift-ci please test source compatibility

@nate-chandler nate-chandler marked this pull request as ready for review August 9, 2025 16:09
@nate-chandler nate-chandler requested a review from jckarter as a code owner August 9, 2025 16:09
@nate-chandler nate-chandler removed the request for review from jckarter August 9, 2025 16:09
@nate-chandler nate-chandler merged commit 335c551 into swiftlang:main Aug 10, 2025
8 checks passed
@nate-chandler nate-chandler deleted the rdar157291161 branch August 10, 2025 18:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant