I'm sending out an updated version of my notes on SIL redesign in support of reasoning about lifetimes, including ownership and borrowing. I sent a very preliminary version of these notes a year ago. Since then, a lot has changed. Work on the SIL representation has been going on "in the background" in several different areas. It's sometimes hard to understand how it all relates to each other.

-Andy

# SIL implementation of ownership and borrowing

SIL representation is going through an evolution in several areas:
Ownership SSA (OSSA), @guaranteed calling convention, lifetime
guarantees, closure conventions, opaque values, borrowing,
exclusivity, and __shared parameter modifiers. These are all loosely
related, but it's important to understand how they fit together. Here
are some notes that I gathered and found useful to write
down. (Coroutines support is also related, but I'm avoiding that topic
for now.) Disclaimer: None of this is authoritative. I hope that any
misunderstandings and misstatements will be corrected by reviewers.

## Borrowed (shared) Swift values

I'm not sure exactly what borrowing will look like in the language,
but conceptually there are two things to support:

- borrow y = x

- foo(borrow x)

Both of these operations imply the absence of a copy and read-only
access to an immutable value within some scope: either the borrowed
variable's scope or the statement's scope. Exclusivity enforcement
guarantees that no writes to the borrowed variable occur within the
scope, thereby ensuring immutability without copying.

For copyable types, the implementation can always introduce a copy
without changing formal program behavior. However, exclusivity still
needs to be enforced to preserve the borrow's original
semantics. Adding a copy would ensure local immutability, but removing
exclusivity would broaden the set of legal programs--it's not a source
compatible change.

Maybe the user doesn't care about formal semantics and only requested
a `borrow` as a performance assurance. Exclusivity should still be
enforced because it is the only way to reliably avoid copying and
provide that assurance.

In short, `borrow => exclusively-immutable and no copy`.

As such, the entire borrow scope will initially need to be
protected by SIL exclusivity markers: `begin_access [read] /
end_access`:

```
%x_address = project_box ...
// initialize %x_address
%x_access = begin_access [read] %x_address
apply @foo(%x_access) : $(@in_guaranteed) -> ()
end_access %x_access
```

If the boxed variable is promoted to a SILValue, the exclusivity
markers can be trivially eliminated. This is valid because promoting
to a SILValue requires static proof of exclusive-immutability. All of
the above instructions can be eliminated except the call itself:

```
apply @foo(%x) : $(@in_guaranteed) -> ()
```

Where `%x` is the value that `%x_address` was initialized to. The only
problem is that this breaks the calling convention. The original SIL
passes an address as the `@in_guaranteed` argument and the optimized SIL 
passes a value. They can't both be right. Knowing which to fix depends
on the type of the borrowed variable.

The `@in_guaranteed` parameter convention passes the value by address,
which is required for address-only types. Types that are opaque due to
generic abstraction or resilience are address-only. Some move-only
types are also address-only.

If the type is *not* address-only (neither borrowed-move-only nor opaque), then
it's possible to pass the value to a function with a direct @guaranteed
parameter convention:

```
apply @foo(%x) : $(@guaranteed AnyObject) -> ()
```

With a direct convention, we cannot pass a SIL address, so the original
SIL would need to be written as:

```
%x_address = project_box ...
%x_access = begin_access [read] %x_address : $*AnyObject
%x_borrow = load_borrow %x_address : $*AnyObject
apply @foo(%x_borrow) : $(@guaranteed AnyObject) -> ()
end_borrow %x_borrow : $AnyObject
end_access %x_access
```

We still need to solve the problem of address-only types. A new
feature called SIL opaque values does that for us. It allows
address-only types to be operated on in SIL just like loadable
types. The only difference is that indirect parameter conventions are
required. With SIL opaque values enabled, the original code will be:

```
%x_address = project_box ...
%x_access = begin_access [read] %x_address : $*T
%x_borrow = load_borrow %x_address : $*T
apply @foo(%x_borrow) : $(@in_guaranteed T) -> ()
end_borrow %x_borrow : $T
end_access %x_access : $*T
```

And the code can now be optimized just like before:

```
apply @foo(%x) : $(@in_guaranteed T) -> ()
```

`load_borrow` is an OSSA feature that allows an addressable formal
memory location to be viewed as an SSA value *without* a separate
lifetime. But it has a secondary role. It also allows the
compiler to view the borrowed memory address as being reserved
for that borrowed value up to the `end_borrow`. `begin_access [read]`
only enforces the language level guarantee the formal memory is
exclusively immutable. `load_borrow` provides a SIL-level guarantee
that physical memory remains exclusively immutable. So while
`begin_access` can be eliminated once its conditions are satisfied,
`load_borrow` must remain until address-only values are lowered to a
direct representation of the ABI, otherwise known "address
lowering". With a normal `load`, the compiler would
still need to bitwise copy the value `%x_borrow` when passing it
`@in_guaranteed`. Not only does that defeat a performance goal, but it
is not possible for some types, such as opaque move-only types, or,
hypothetically, move-only types with "mutable" properties, as required
for atomics.

Ownership SSA (OSSA) form also introduces `begin_borrow / end_borrow`
scopes for SILValues, but those are mostly unrelated to borrowing at
the language level. SILValue borrowing is not necessary to represent
borrowed Swift values (the `load_borrow` is no longer needed once the
Swift value at `%x_address` is promoted to the SILValue
`%x`). `begin_borrow / end_borrow` scopes *are* currently used in
situations that are unrelated to Swift borrowing, as explained in
below in the OSSA section.

> The naming conflict between borrowed Swift values and borrowed
> SILValues should eventually be resolved. For example, language
> review may decide that "shared" is a better name than "borrowed". I
> use the `borrow` here because I don't want to create more confusion
> surrounding our existing (unsupported) `__shared` parameter
> modifier, which does not currently have semantics that I described
> above.

## @guaranteed (aka +0) parameter ABI

In the previous section, we saw that a `@guaranteed` convention is
required to implement borrowed Swift values simply because the
borrowed value cannot be copied or destroyed within the borrow
scope. Beyond that, the calling convention is purely an ABI feature
unrelated to borrowing at the language level.

`@guaranteed` (+0) and `@owned` (+1) conventions determine where
copies are needed but otherwise have no effect on variable
lifetimes. In the `@owned` case, lifetime ends before the return,
and in the `@guaranteed` case it ends after the return. From the point
of view of both the user and the optimizer, this is semantically the
same lifetime. In other words, manually inlining an unadorned pure
Swift function cannot affect formal program behavior.

> SILValues are currently wrapped in `begin_borrow` before being
> passed `@guaranteed`, but there's no reason to do that other than to
> avoid inserting the `begin_borrow` during inlining, and in fact it
> contradicts the purpose of `begin_borrow` as described in the OSSA
> section.

It is up to the compiler to decide which convention to use for
parameters in a given position with a given type. Sometimes `@owned`
is clearly best (initializers). Sometimes `@guaranteed` is clearly
best (self, stdlib protocols, closures). Since those decisions
are about to become ABI, they're being evaluated carefully. Regardless
of the outcome, programmers need a way to override the convention with
a parameter attribute or modifier.

## __shared parameter modifier

The `__shared` parameter modifier simply forces the compiler to
choose the `@guaranteed` (+0) convention. This is unsupported but
important for evaluating alternative ABI decisions and preparing for
ABI compatibility with move-only types.

In the first section, I defined a hypothetical `borrow` caller-side
modifier. To understand the difference between a `__shared` convention
and a `borrowed` value consider this example:

```
func foo<T>(_ t: __shared T, _ f: ()->()) {
  f()
  print(t)
}

func bar<T>(_ t: inout T) { ... }

func test<T>(_ t: T) {
  var t = move(t)
  foo(t) { bar(&t) }
}
```

This is legal code today, but `t` will be copied when passed to
`foo`. This likely defies the user's expectation that they see the
value printed after modification by `bar`.

If `T` is ever a move-only type, this will simply be undefined without
additional exclusivity enforcement. Requiring that a variable be
`borrowed` before being passed `__shared` catches that:

```
func test<T>(_ t: T) {
  var t = move(t)
  foo(borrow t) { bar(&t) }
}
```

Now this is a static compiler error.

Of course, we could introduce an implicit borrow whenever move-only
types are passed `__shared`. But I believe that is too subtle and
misleading of a rule to expose to users.

This becomes much simpler if `@guaranteed` is the default
convention that users can override with a `__consumed` parameter
modifier. The allowable argument and parameter pairings would be:

- non-borrowed value -> __consumed parameter

- borrowed value -> default parameter

- copyable value -> default parameter

## Ownership SSA (OSSA)

Michael Gottesman is working on extensively documenting this
feature. Briefly, a SILValue is a singly defined node in an SSA graph
that represents one instance of a Swift value. With OSSA, each
SILValue now has an independent lifetime--it has "ownership" of its
lifetime. The SILValue's lifetime must be ended by destroying the
value (e.g. decrementing the refcount), or moving the value into
another SILValue (e.g. passing an @owned argument).

A SILValue can be "borrowed" across some program scope via
`begin/end_borrow`. The borrowed value can then be "projected" out into
subobjects or cast to another type. The original value cannot be
destroyed within the borrow scope. This representation allows trivial
lifetime analysis. There's no need to reason about projections, casts
and the like. That's all hidden by the borrow scope.

So, SILValue borrowing isn't required for modeling language level
semantics. Instead, it's a convenient way to verify that SIL
transformations obey the rules of OSSA. In fact, these instructions
are trivially removed as soon as the compiler no longer needs to
verify OSSA.

What do we mean by OSSA rules? Here's a quick summary.

The users of SILValues can be divided into these groups.

Uses independent of ownership:

U1. Use the value instantaneously (`copy_value`, `@guaranteed` argument)

U2. Escape the nontrivial contents of the value (`ref_to_unowned`,
`ref_to_rawpointer`, `unchecked_trivial_bitcast`)

Uses that require an owned value:

O3. Propagate the value without consuming it (`mark_dependence`, `begin_borrow`)

O4. Consume the value immediately (`store`, `destroy`, `@owned` argument)

O5. Consume the value indirectly via a move (`tuple`, `struct`)

Uses that require a borrowed value:

B6. Project the borrowed value (`struct_extract`, `tuple_extract`,
`ref_element_addr`, `open_existential_value`)

`begin_borrow` is only needed to handle uses in (B6). Support for
`struct_extract` and `tuple_extract` was the most compelling need for
`begin_borrow`. It maybe be best though to eliminate those
instructions instead using a more OSSA-friendly `destructure`. Doing
this would enable normal optimization of tuples and struct copies.

`begin_borrow` may still remain useful to make it easier to handle
the other uses that fall into (B6). For example, rather than analyzing
all uses of `ref_element_addr`, the compiler can treat the entire
scope like a single use at `end_borrow`.

```
  %borrow = begin_borrow %0 : $Class
  %addr = ref_element_addr %borrow : $Class, #Class.property
  %value = load %addr
  // Inner Instructions
  end_borrow %borrow : $Class
  // Outer Instructions
  destroy_value %obj
```

Here, the value of `%addr` depends on lifetime of `%borrow`. The
compiler can choose to ignore that dependent lifetime and consider the
`end_borrow` a normal use. Even this simplified view of lifetime
allows hoisting `destroy_value %obj` above "Outer
Instructions". Alternatively, the compiler can analyze the uses of the
dependent value (`%addr`) and see that it's safe to hoist both
the `begin_borrow` and `destroy_value` above "Inner Instructions".

So, uses in (O3 - propagate) can be either analyzed transitively or
skipped to the end of their scope.

Uses in (U2 - escape) cannot be safely analyzed transitively,
requiring some additional mechanism to provide safety, as described in
the section "Dependent Lifetime".

## Dependent Lifetime

A value's lifetime cannot be verified if a use has escaped the
contents that value into a trivial type (U2). In those cases, it is
the user's responsibility to designate the value's lifetime. API's
like `withExtendedLifetime` do this by emitting a `fix_lifetime`
instruction. Without `fix_lifetime`, the compiler would be able to
hoist either a `destroy` or an `end_borrow` above any uses of the
escaped nontrivial value.

`fix_lifetime` is too conservative for performance critical code. It
prevents surrounding code motion and effectively disables dead code
elimination. `mark_dependence` is a more precise instruction that ties
an "owner" value lifetime to an otherwise unrelated (likely trivial)
"dependent" value. If the compiler is able to analyze the uses of the
trivial value, then it can more aggressively optimize the owner's
lifetime.

> I'll send a separate detailed proposal for a adding an
> `withDependentLifetime` API and `end_dependence` instruction.

Here is some SIL code with an explicitly dependent lifetime:

```
bb0(%0 : @owned $Class)
  %unowned = ref_to_unowned %obj : $Class to $@sil_unowned Class
  %dependent = mark_dependence %unowned on %obj
  store %dependent to ...
  // Inner Instructions
  end_dependence %dependent
  // Outer Instructions
  destroy_value %obj
```

In this example, the `%dependent` value itself escapes, so the
compiler knows nothing of its lifetime. However, it can still hoist
`destroy_value` above "Outer Instructions" because the
`end_dependence`

In the next example, the compiler can determine that the dependent
value does not escape:

```
bb0(%0 : @owned $Class)
  %unowned = ref_to_unowned %obj : $Class to $@sil_unowned Class
  %dependent = mark_dependence %unowned on %obj
  load_unowned %dependent
  // Inner Instructions
  end_dependence %dependent
  // Outer Instructions
  destroy_value %obj
```

In that case both `destroy` and `end_dependence` can be hoisted above
"Inner Instructions".

Furthermore, if the `%dependent` value becomes dead, then the entire
`mark_dependence` scope can be eliminated.

Note that mark dependence does not require the compiler to discover
any relationship between the owner and its dependent value. The
instruction makes that relationship explicit (in the example below
`%trivial` depends on `%copy`). This is an important difference
between the lifetime propagation of `begin_borrow` and
`mark_dependence`.

```
bb0(%0 : @owned $Class)
  %objptr = ref_to_raw_pointer %obj : $Class to $Builtin.RawPointer
  store %objptr to %temp : $*Builtin.RawPointer
  %copy = copy_value %obj
  %trivial = load %temp : $*Builtin.RawPointer
  %dependent = mark_dependence %trivial on %copy
  load_unowned %dependent  
  // Inner Instructions
  end_dependence %dependent
  // Outer Instructions
  destroy_value %obj
```

Both `begin_borrow` and `mark_dependence` instructions open a scope for
dependent lifetimes. In both cases, the only dependent values that
affect the owner's lifetime are values that directly derive from the
`begin_borrow` or `mark_dependence` SSA value. The differences are:

- The `begin_borrow` value is simply a borrowed instance of the owner.

- The `mark_dependence` value is any arbitrary trivial or non-trivial value.

- Only non-trivial values derived from `begin_borrow` are considered
  relevant. Casting the borrowed value to a trivial value will require
  a separate `mark_dependence`.

- All values derived from `mark_dependence` are considered, whether
  trivial or non-trivial.

SIL implementation of ownership and borrowing

SIL representation is going through an evolution in several areas: Ownership SSA (OSSA), @guaranteed calling convention, lifetime guarantees, closure conventions, opaque values, borrowing, exclusivity, and __shared parameter modifiers. These are all loosely related, but it's important to understand how they fit together. Here are some notes that I gathered and found useful to write down. (Coroutines support is also related, but I'm avoiding that topic for now.) Disclaimer: None of this is authoritative. I hope that any misunderstandings and misstatements will be corrected by reviewers.

Borrowed (shared) Swift values

I'm not sure exactly what borrowing will look like in the language, but conceptually there are two things to support:

  • borrow y = x

  • foo(borrow x)

Both of these operations imply the absence of a copy and read-only access to an immutable value within some scope: either the borrowed variable's scope or the statement's scope. Exclusivity enforcement guarantees that no writes to the borrowed variable occur within the scope, thereby ensuring immutability without copying.

For copyable types, the implementation can always introduce a copy without changing formal program behavior. However, exclusivity still needs to be enforced to preserve the borrow's original semantics. Adding a copy would ensure local immutability, but removing exclusivity would broaden the set of legal programs--it's not a source compatible change.

Maybe the user doesn't care about formal semantics and only requested a borrow as a performance assurance. Exclusivity should still be enforced because it is the only way to reliably avoid copying and provide that assurance.

In short, borrow => exclusively-immutable and no copy.

As such, the entire borrow scope will initially need to be protected by SIL exclusivity markers: begin_access [read] / end_access:

%x_address = project_box ...
// initialize %x_address
%x_access = begin_access [read] %x_address
apply @foo(%x_access) : $(@in_guaranteed) -> ()
end_access %x_access

If the boxed variable is promoted to a SILValue, the exclusivity markers can be trivially eliminated. This is valid because promoting to a SILValue requires static proof of exclusive-immutability. All of the above instructions can be eliminated except the call itself:

apply @foo(%x) : $(@in_guaranteed) -> ()

Where %x is the value that %x_address was initialized to. The only problem is that this breaks the calling convention. The original SIL passes an address as the @in_guaranteed argument and the optimized SIL passes a value. They can't both be right. Knowing which to fix depends on the type of the borrowed variable.

The @in_guaranteed parameter convention passes the value by address, which is required for address-only types. Types that are opaque due to generic abstraction or resilience are address-only. Some move-only types are also address-only.

If the type is not address-only (neither borrowed-move-only nor opaque), then it's possible to pass the value to a function with a direct @guaranteed parameter convention:

apply @foo(%x) : $(@guaranteed AnyObject) -> ()

With a direct convention, we cannot pass a SIL address, so the original SIL would need to be written as:

%x_address = project_box ...
%x_access = begin_access [read] %x_address : $*AnyObject
%x_borrow = load_borrow %x_address : $*AnyObject
apply @foo(%x_borrow) : $(@guaranteed AnyObject) -> ()
end_borrow %x_borrow : $AnyObject
end_access %x_access

We still need to solve the problem of address-only types. A new feature called SIL opaque values does that for us. It allows address-only types to be operated on in SIL just like loadable types. The only difference is that indirect parameter conventions are required. With SIL opaque values enabled, the original code will be:

%x_address = project_box ...
%x_access = begin_access [read] %x_address : $*T
%x_borrow = load_borrow %x_address : $*T
apply @foo(%x_borrow) : $(@in_guaranteed T) -> ()
end_borrow %x_borrow : $T
end_access %x_access : $*T

And the code can now be optimized just like before:

apply @foo(%x) : $(@in_guaranteed T) -> ()

load_borrow is an OSSA feature that allows an addressable formal memory location to be viewed as an SSA value without a separate lifetime. But it has a secondary role. It also allows the compiler to view the borrowed memory address as being reserved for that borrowed value up to the end_borrowbegin_access [read] only enforces the language level guarantee the formal memory is exclusively immutable. load_borrow provides a SIL-level guarantee that physical memory remains exclusively immutable. So whilebegin_access can be eliminated once its conditions are satisfied, load_borrow must remain until address-only values are lowered to a direct representation of the ABI, otherwise known "address lowering". With a normal load, the compiler would still need to bitwise copy the value %x_borrow when passing it @in_guaranteed. Not only does that defeat a performance goal, but it is not possible for some types, such as opaque move-only types, or, hypothetically, move-only types with "mutable" properties, as required for atomics.

Ownership SSA (OSSA) form also introduces begin_borrow / end_borrow scopes for SILValues, but those are mostly unrelated to borrowing at the language level. SILValue borrowing is not necessary to represent borrowed Swift values (the load_borrow is no longer needed once the Swift value at %x_address is promoted to the SILValue %x). begin_borrow / end_borrow scopes are currently used in situations that are unrelated to Swift borrowing, as explained in below in the OSSA section.

The naming conflict between borrowed Swift values and borrowed SILValues should eventually be resolved. For example, language review may decide that "shared" is a better name than "borrowed". I use the borrow here because I don't want to create more confusion surrounding our existing (unsupported) __shared parameter modifier, which does not currently have semantics that I described above.

@guaranteed (aka +0) parameter ABI

In the previous section, we saw that a @guaranteed convention is required to implement borrowed Swift values simply because the borrowed value cannot be copied or destroyed within the borrow scope. Beyond that, the calling convention is purely an ABI feature unrelated to borrowing at the language level.

@guaranteed (+0) and @owned (+1) conventions determine where copies are needed but otherwise have no effect on variable lifetimes. In the @owned case, lifetime ends before the return, and in the @guaranteed case it ends after the return. From the point of view of both the user and the optimizer, this is semantically the same lifetime. In other words, manually inlining an unadorned pure Swift function cannot affect formal program behavior.

SILValues are currently wrapped in begin_borrow before being passed @guaranteed, but there's no reason to do that other than to avoid inserting the begin_borrow during inlining, and in fact it contradicts the purpose of begin_borrow as described in the OSSA section.

It is up to the compiler to decide which convention to use for parameters in a given position with a given type. Sometimes @owned is clearly best (initializers). Sometimes @guaranteed is clearly best (self, stdlib protocols, closures). Since those decisions are about to become ABI, they're being evaluated carefully. Regardless of the outcome, programmers need a way to override the convention with a parameter attribute or modifier.

__shared parameter modifier

The __shared parameter modifier simply forces the compiler to choose the @guaranteed (+0) convention. This is unsupported but important for evaluating alternative ABI decisions and preparing for ABI compatibility with move-only types.

In the first section, I defined a hypothetical borrow caller-side modifier. To understand the difference between a __sharedconvention and a borrowed value consider this example:

func foo<T>(_ t: __shared T, _ f: ()->()) {
  f()
  print(t)
}

func bar<T>(_ t: inout T) { ... }

func test<T>(_ t: T) {
  var t = move(t)
  foo(t) { bar(&t) }
}

This is legal code today, but t will be copied when passed to foo. This likely defies the user's expectation that they see the value printed after modification by bar.

If T is ever a move-only type, this will simply be undefined without additional exclusivity enforcement. Requiring that a variable beborrowed before being passed __shared catches that:

func test<T>(_ t: T) {
  var t = move(t)
  foo(borrow t) { bar(&t) }
}

Now this is a static compiler error.

Of course, we could introduce an implicit borrow whenever move-only types are passed __shared. But I believe that is too subtle and misleading of a rule to expose to users.

This becomes much simpler if @guaranteed is the default convention that users can override with a __consumed parameter modifier. The allowable argument and parameter pairings would be:

  • non-borrowed value -> __consumed parameter

  • borrowed value -> default parameter

  • copyable value -> default parameter

Ownership SSA (OSSA)

Michael Gottesman is working on extensively documenting this feature. Briefly, a SILValue is a singly defined node in an SSA graph that represents one instance of a Swift value. With OSSA, each SILValue now has an independent lifetime--it has "ownership" of its lifetime. The SILValue's lifetime must be ended by destroying the value (e.g. decrementing the refcount), or moving the value into another SILValue (e.g. passing an @owned argument).

A SILValue can be "borrowed" across some program scope via begin/end_borrow. The borrowed value can then be "projected" out into subobjects or cast to another type. The original value cannot be destroyed within the borrow scope. This representation allows trivial lifetime analysis. There's no need to reason about projections, casts and the like. That's all hidden by the borrow scope.

So, SILValue borrowing isn't required for modeling language level semantics. Instead, it's a convenient way to verify that SIL transformations obey the rules of OSSA. In fact, these instructions are trivially removed as soon as the compiler no longer needs to verify OSSA.

What do we mean by OSSA rules? Here's a quick summary.

The users of SILValues can be divided into these groups.

Uses independent of ownership:

U1. Use the value instantaneously (copy_value@guaranteed argument)

U2. Escape the nontrivial contents of the value (ref_to_unownedref_to_rawpointerunchecked_trivial_bitcast)

Uses that require an owned value:

O3. Propagate the value without consuming it (mark_dependencebegin_borrow)

O4. Consume the value immediately (storedestroy@owned argument)

O5. Consume the value indirectly via a move (tuplestruct)

Uses that require a borrowed value:

B6. Project the borrowed value (struct_extracttuple_extractref_element_addropen_existential_value)

begin_borrow is only needed to handle uses in (B6). Support for struct_extract and tuple_extract was the most compelling need for begin_borrow. It maybe be best though to eliminate those instructions instead using a more OSSA-friendly destructure. Doing this would enable normal optimization of tuples and struct copies.

begin_borrow may still remain useful to make it easier to handle the other uses that fall into (B6). For example, rather than analyzing all uses of ref_element_addr, the compiler can treat the entire scope like a single use at end_borrow.

  %borrow = begin_borrow %0 : $Class
  %addr = ref_element_addr %borrow : $Class, #Class.property
  %value = load %addr
  // Inner Instructions
  end_borrow %borrow : $Class
  // Outer Instructions
  destroy_value %obj

Here, the value of %addr depends on lifetime of %borrow. The compiler can choose to ignore that dependent lifetime and consider the end_borrow a normal use. Even this simplified view of lifetime allows hoisting destroy_value %obj above "Outer Instructions". Alternatively, the compiler can analyze the uses of the dependent value (%addr) and see that it's safe to hoist both the begin_borrow and destroy_value above "Inner Instructions".

So, uses in (O3 - propagate) can be either analyzed transitively or skipped to the end of their scope.

Uses in (U2 - escape) cannot be safely analyzed transitively, requiring some additional mechanism to provide safety, as described in the section "Dependent Lifetime".

Dependent Lifetime

A value's lifetime cannot be verified if a use has escaped the contents that value into a trivial type (U2). In those cases, it is the user's responsibility to designate the value's lifetime. API's like withExtendedLifetime do this by emitting a fix_lifetime instruction. Without fix_lifetime, the compiler would be able to hoist either a destroy or an end_borrow above any uses of the escaped nontrivial value.

fix_lifetime is too conservative for performance critical code. It prevents surrounding code motion and effectively disables dead code elimination. mark_dependence is a more precise instruction that ties an "owner" value lifetime to an otherwise unrelated (likely trivial) "dependent" value. If the compiler is able to analyze the uses of the trivial value, then it can more aggressively optimize the owner's lifetime.

I'll send a separate detailed proposal for a adding an withDependentLifetime API and end_dependence instruction.

Here is some SIL code with an explicitly dependent lifetime:

bb0(%0 : @owned $Class)
  %unowned = ref_to_unowned %obj : $Class to $@sil_unowned Class
  %dependent = mark_dependence %unowned on %obj
  store %dependent to ...
  // Inner Instructions
  end_dependence %dependent
  // Outer Instructions
  destroy_value %obj

In this example, the %dependent value itself escapes, so the compiler knows nothing of its lifetime. However, it can still hoistdestroy_value above "Outer Instructions" because the end_dependence

In the next example, the compiler can determine that the dependent value does not escape:

bb0(%0 : @owned $Class)
  %unowned = ref_to_unowned %obj : $Class to $@sil_unowned Class
  %dependent = mark_dependence %unowned on %obj
  load_unowned %dependent
  // Inner Instructions
  end_dependence %dependent
  // Outer Instructions
  destroy_value %obj

In that case both destroy and end_dependence can be hoisted above "Inner Instructions".

Furthermore, if the %dependent value becomes dead, then the entire mark_dependence scope can be eliminated.

Note that mark dependence does not require the compiler to discover any relationship between the owner and its dependent value. The instruction makes that relationship explicit (in the example below %trivial depends on %copy). This is an important difference between the lifetime propagation of begin_borrow and mark_dependence.

bb0(%0 : @owned $Class)
  %objptr = ref_to_raw_pointer %obj : $Class to $Builtin.RawPointer
  store %objptr to %temp : $*Builtin.RawPointer
  %copy = copy_value %obj
  %trivial = load %temp : $*Builtin.RawPointer
  %dependent = mark_dependence %trivial on %copy
  load_unowned %dependent  
  // Inner Instructions
  end_dependence %dependent
  // Outer Instructions
  destroy_value %obj

Both begin_borrow and mark_dependence instructions open a scope for dependent lifetimes. In both cases, the only dependent values that affect the owner's lifetime are values that directly derive from the begin_borrow or mark_dependence SSA value. The differences are:

  • The begin_borrow value is simply a borrowed instance of the owner.

  • The mark_dependence value is any arbitrary trivial or non-trivial value.

  • Only non-trivial values derived from begin_borrow are considered relevant. Casting the borrowed value to a trivial value will require a separate mark_dependence.

  • All values derived from mark_dependence are considered, whether trivial or non-trivial.


_______________________________________________
swift-dev mailing list
swift-dev@swift.org
https://lists.swift.org/mailman/listinfo/swift-dev

Reply via email to