Skip to content

[5.9][move-only] A series of batched commits. #65871

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
May 18, 2023

Conversation

gottesmm
Copy link
Contributor

@gottesmm gottesmm commented May 11, 2023

This PR contains a few batched changes for 5.9. There is at least one change
that just updates tests and another that eliminates a dead field. I didn't create CCC for them.
Here are the CCC for the major changes:

Commit f634acf

• Description: [move-only] Fix a thinko where we are treating inout convention
as a consuming use instead of liveness use. The effect of this change is that
when we were capturing an inout in a closure, we were treating it as if it was
consuming it causing us to give the wrong diagnostic.
• Risk: Low risk. Only affects noncopyable types.
• Original PR: #65864
• Reviewed By: @jckarter
• Testing: I added tests/updated other tests to make sure this did not happen.
• Resolves: rdar://109217216

Commit a10a5db

• Description: [move-only] Ensure that resilient deinits take their self
argument as an address argument instead of an object. The function signature
was already correct, just the logic in SILGen around cerating the SIL argument
assumed that we would always have an object since it was setup originally just
to support classes which even when resilient are still objects.
• Risk: Low risk. Only affects noncopyable types.
• Original PR: #65842
• Reviewed By: @jckarter
• Testing: I added tests/updated other tests to make sure this did not happen.
• Resolves: rdar://109170069

Commit a10a5db

• Description: [move-only] Teach the move only object checker how to handle
concrete resilient guaranteed arguments.

The only difference from normal concrete guaranteed parameter emission is we
have a load_borrow on the argument before the copy_value. That is instead of:

bb0(%0 : @guaranteed $Type):
  %1 = copy_value %0
  %2 = mark_must_check [no_consume_or_assign] %1

we have,

bb0(%0 : $*Type): // in_guaranteed
  %1 = load_borrow %0
  %2 = copy_value %1
  %3 = mark_must_check [no_consume_or_assign] %2

So I just needed to update the checker to recognize that pattern.

This is tested by just making sure that the checker can handle the borrowVal in
the tests without emitting an error.

• Risk: Low risk. Only affects noncopyable types.
• Original PR: #65842
• Reviewed By: @jckarter
• Testing: I added tests/updated other tests to make sure this did not happen.
• Resolves: rdar://109170906

Commit 43fb28f && dddafe8

• Description: [move-only] Spill noncopyable types that are objects using
store_borrow if we call a resilient function that takes it in_guaranteed.

This is a typical update needed around library-evolution. Since we are inside
the module itself, the value is concrete and thus an object, but the API it
uses is resilient, so we have to pass it as an address.

The second commit is just a small update to the first that fixes an issue
where we were not properly handling opaque values with this code path. I just
didn't squash the two commits.

• Risk: Low risk. Only affects noncopyable types.
• Original PR: #65842
• Reviewed By: @jckarter
• Testing: I added tests/updated other tests to make sure this did not happen.
• Resolves: rdar://109171001

Commit d2b7e70
Commit cd3734c
Commit de5737d
Commit a6edf15
Commit 052ecd1

• Description: Originally, we were going to follow the AST in a strict way in
terms of what closures are considered escaping or not from a diagnostics
perspective. Upon further investigation I found that we actually do something
different for inout escaping semantics and by treating the AST as the one
point of truth, we are being inconsistent with the rest of the compiler. As an
example, the following code is considered by the compiler to not be an invalid
escaping use of an inout implying that we do not consider the closure to be
escaping:

func f(_ x: inout Int) {
  let g = {
    _ = x
  }
}

in contrast, a var is always considered to be an escape:

func f(_ x: inout Int) {
  var g = {
    _ = x
  }
}

test2.swift:3:13: error: escaping closure captures 'inout' parameter 'x'
    var g = {
            ^
test2.swift:2:10: note: parameter 'x' is declared 'inout'
func f(_ x: inout Int) {
         ^
test2.swift:4:11: note: captured here
        _ = x
          ^

Of course, if we store the let into memory, we get the error one would expect:

var global: () -> () = {}
func f(_ x: inout Int) {
  let g = {
    _ = x
  }
  global = g
}

test2.swift:4:11: error: escaping closure captures 'inout' parameter 'x'
  let g = {
          ^
test2.swift:3:10: note: parameter 'x' is declared 'inout'
func f(_ x: inout Int) {
         ^
test2.swift:5:7: note: captured here
    _ = x
      ^

By reverting to the old behavior where allocbox to stack ran early,
noncopyable types now have the same sort of semantics: let closures that
capture a noncopyable type that do not on the face of it escape are considered
non-escaping, while if the closure is ever stored into memory (e.x.: store
into a global, into a local var) or escapes, we get the appropriate escaping
diagnostics. E.x.:

public struct E : ~Copyable {}
public func borrowVal(_ e: borrowing E) {}
public func consumeVal(_ e: consuming E) {}

func f1() {
  var e = E()

  // Mutable borrowing use of e. We can consume e as long as we reinit at end
  // of function. We don't here, so we get an error.
  let c1: () -> () = {
    borrowVal(e)
    consumeVal(e)
  }

  // Mutable borrowing use of e. We can consume e as long as we reinit at end
  // of function. We do do that here, so no error.
  let c2: () -> () = {
    borrowVal(e)
    consumeVal(e)
    e = E()
  }
}

Additionally, I did follow on work to ensure that:

  1. In these cases we treat applications of the partial_apply or passing the
    partial_apply off to a function as the liveness requiring uses rather than the
    partial_apply formation and the end lifetime of the partial_apply.

  2. When we do have an escaping partial_apply where we emit an inout capture
    error (and thus bail out of move checking early), we no longer attempt to emit
    "copy of noncopyable type" errors.

• Risk: Low risk. Only affects noncopyable types.
• Original PR: #65911
• Reviewed By: @jckarter
• Testing: I added tests/updated other tests to make sure this did not happen.
• Resolves: rdar://109426644

@gottesmm gottesmm requested a review from a team as a code owner May 11, 2023 20:52
@gottesmm
Copy link
Contributor Author

@swift-ci test

@gottesmm gottesmm force-pushed the release-5.9-more-batched branch from 8b41318 to 5af1ab0 Compare May 15, 2023 21:44
@gottesmm
Copy link
Contributor Author

@swift-ci test

@gottesmm gottesmm force-pushed the release-5.9-more-batched branch from 5af1ab0 to 907e506 Compare May 16, 2023 17:40
@gottesmm
Copy link
Contributor Author

@swift-ci test

gottesmm added 15 commits May 16, 2023 15:04
…consuming use instead of liveness use.

rdar://109217216
(cherry picked from commit a4fcdd8)
… an address argument instead of an object.

The signature was already correct, just the logic in the deinit code assumed
that we would always have an object.

rdar://109170069
(cherry picked from commit 12e1db4)
… resilient guaranteed arguments.

The only difference from normal concrete guaranteed parameter emission is we
have a load_borrow on the argument before the copy_value. That is instead of:

```
bb0(%0 : @guaranteed $Type):
  %1 = copy_value %0
  %2 = mark_must_check [no_consume_or_assign] %1
```

we have,

```
bb0(%0 : $*Type): // in_guaranteed
  %1 = load_borrow %0
  %2 = copy_value %1
  %3 = mark_must_check [no_consume_or_assign] %2
```

So I just needed to update the checker to recognize that pattern.

This is tested by just making sure that the checker can handle the borrowVal in
the tests without emitting an error.

rdar://109170906
(cherry picked from commit 7d7c929)
…ow if we call a resilient function that takes it in_guaranteed.

This ensures that given a class that contains a noncopyable type that contains
another noncopyable type:

```
@_moveOnly struct S2 {}
@_moveOnly struct S { var s2: S2 }
class C { var s: S }
```

if we call a resilient function that takes C.S.S2:

```
borrowVal(c.s.s2)
```

we properly spill s2 onto the stack using a store_borrow.

Why Do This?
------------

Currently SILGenLValue treats ref_element_addr as a base that it needs to load
from for both copyable and non-copyable types. We keep a separation of concerns
and require emission of resilient functions to handle these loaded values. For
copyable types this means copying the value and storing it into a temporary
stack allocation. For noncopyable types, we never actually implemented this so
we would hit an error in SILGenApply telling us that our resilient function
expected an address argument, but we are passing an object.

To work around this, I updated how we emit borrowed lvalue arguments to in this
case to spill the value into a temporary allocation using a store_borrow. I also
included a test that validates that we properly have a read exclusivity scope
around the original loaded from memory for the entire call site so even though
we are performing a load_borrow and then spilling it, we still have read
exclusivity to the original memory for the entire region meaning that we still
preserve the semantics.

rdar://109171001
(cherry picked from commit 59fdfad)
I hit a weird bug where for some reason it wasn't working. I can't reproduce it
now, so it makes sense to use the correct syntax.

(cherry picked from commit e76f3f9)
…e_borrow if we are using lowered addresses.

When opaque values are enabled, we do not need this store_borrow. This was
caught by the following tests:

  Swift(macosx-x86_64) :: SILGen/opaque_values_silgen.swift
  Swift(macosx-x86_64) :: SILGen/opaque_values_silgen_resilient.swift

(cherry picked from commit 81a09b3)
…guments as such even if the closure doesn't actually escape"

This reverts commit 224674c.

Originally, I made this change since we were going to follow the AST in a strict
way in terms of what closures are considered escaping or not from a diagnostics
perspective. Upon further investigation I found that we actually do something
different for inout escaping semantics and by treating the AST as the one point
of truth, we are being inconsistent with the rest of the compiler. As an
example, the following code is considered by the compiler to not be an invalid
escaping use of an inout implying that we do not consider the closure to be
escaping:

```
func f(_ x: inout Int) {
  let g = {
    _ = x
  }
}
```

in contrast, a var is always considered to be an escape:

```
func f(_ x: inout Int) {
  var g = {
    _ = x
  }
}

test2.swift:3:13: error: escaping closure captures 'inout' parameter 'x'
    var g = {
            ^
test2.swift:2:10: note: parameter 'x' is declared 'inout'
func f(_ x: inout Int) {
         ^
test2.swift:4:11: note: captured here
        _ = x
          ^
```

Of course, if we store the let into memory, we get the error one would expect:

```
var global: () -> () = {}
func f(_ x: inout Int) {
  let g = {
    _ = x
  }
  global = g
}

test2.swift:4:11: error: escaping closure captures 'inout' parameter 'x'
  let g = {
          ^
test2.swift:3:10: note: parameter 'x' is declared 'inout'
func f(_ x: inout Int) {
         ^
test2.swift:5:7: note: captured here
    _ = x
      ^
```

By reverting to the old behavior where allocbox to stack ran early, noncopyable
types now have the same sort of semantics: let closures that capture a
noncopyable type that do not on the face of it escape are considered
non-escaping, while if the closure is ever stored into memory (e.x.: store into
a global, into a local var) or escapes, we get the appropriate escaping
diagnostics. E.x.:

```
public struct E : ~Copyable {}
public func borrowVal(_ e: borrowing E) {}
public func consumeVal(_ e: consuming E) {}

func f1() {
  var e = E()

  // Mutable borrowing use of e. We can consume e as long as we reinit at end
  // of function. We don't here, so we get an error.
  let c1: () -> () = {
    borrowVal(e)
    consumeVal(e)
  }

  // Mutable borrowing use of e. We can consume e as long as we reinit at end
  // of function. We do do that here, so no error.
  let c2: () -> () = {
    borrowVal(e)
    consumeVal(e)
    e = E()
  }
}

```

(cherry picked from commit 3ccda34)
… consumable_and_assignable, not assignable_but_not_consumable.

Otherwise, since the compiler views assignable_but_not_consumable in this
position as a sign of an escaping closure, we emit escaping closure errors
instead of nonescaping closure errors.

(cherry picked from commit 9c9fe09)
…ping partial apply and use that to emit diagnostics.

The previous commits in this series of changes reverted the allocbox to stack
change. Even with that though, we were treating the forming of the partial apply
and the destroys of the partial apply as the liveness requiring uses. This is
not the correct semantics for the non-escaping let closures. Instead, we want to
treat the passing off of the partial apply to another function or the invocation
of the partial apply as the liveness requiring uses. This makes sense since when
we capture a noncopyable type in the closure, we capture them as
inout_aliasable, that is as a pointer, so we are not going to actually use the
value until we call the partial_apply.

(cherry picked from commit 7d86bf0)
I also extended the tests to handle more interesting cases.

NOTE: There are a few cases where we introduced some new do not understand
errors. I am going to fix that in the next commit. I just wanted to completely
update the tests for the manner in which the allocbox to stack change affects
them.

(cherry picked from commit 9ff33c5)
…ey have a partial_apply that was identified by an earlier diagnostic as escaping.

Previously, we would emit a "compiler doesn't understand error" since we would
detect the escape and fail. That is the correct behavior here if the
partial_apply is not already identified as escaping by an earlier pass. But in
the case where we see that the partial_apply's callee was marked with
semantics::NO_MOVEONLY_DIAGNOSTICS, then we:

1. Suppress the "compiler doesn't understand error" for this specific
   mark_must_check.

2. Suppress function wide the "copy of noncopyable type" error. Since we stopped
   processing the mark_must_check that was passed to the partial_apply, we may
   have left copies of noncopyable types on that mark_must_check value. This is
   ok since the user will get the error, will recompile, and if any further show
   up after they fix the inout escaping issue, they will be emitted
   appropriately.

(cherry picked from commit 07979f0)
Originally this was a method to determine if we had emitted a diagnostic when
running our gather visitor. It was pretty footgunny since one could easily
forget to set it. Instead of doing that, we now maintain a counter in the
diagnostic emitter that counts how many diagnostics we have emitted and use that
to determine if during the walk if we emitted any additional diagnostics.

(cherry picked from commit d252902)
@gottesmm gottesmm force-pushed the release-5.9-more-batched branch from 907e506 to 5023b93 Compare May 16, 2023 22:05
@gottesmm
Copy link
Contributor Author

@swift-ci test

// the compiler doesn't understand error.
if (fArg->getArgumentConvention().isInoutConvention() &&
pas->getCalleeFunction()->hasSemanticsAttr(
semantics::NO_MOVEONLY_DIAGNOSTICS)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE: This isn't actually a bug. I just by mistake left this in this specific commit. I filled in the if statement body here: de5737d#diff-9c8ec5474745801be2148dbcd9e1178e6aaead4739668d88d551b4137047cb8b

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specifically in 052ecd1

@gottesmm gottesmm merged commit 2089d99 into swiftlang:release/5.9 May 18, 2023
@gottesmm gottesmm deleted the release-5.9-more-batched branch May 18, 2023 17:07
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.

3 participants