How is __shared keyword supposed to work in Swift 5.0?

I'm a normal Swift user trying to understand the semantics of this code--does the code compile, and what value is printed? What happens on the caller or callee side at run time is neither part of the language semantics nor relevant to me.

I don't think that's true. What happens on the caller side is all about language semantics. Part of our goal here AIUI is that, by default, users don't have to think about borrowing or copies in Swift unless they use language features that require these be made explicit.

To me, the normal user, the program semantics boil down to: does the code compile and what will be printed. I honestly don't understand what you're saying.

The possiblities are:

  1. The shared argument is an immutable borrow and the code is illegal.
  2. The shared argument has pass-by-reference semantics.
  3. shared does not affect program semantics for any code that I can write today.

#2 is an implementation detail.

No, it is not. It determines whether the program prints "5" or "6". I can't imagine anyone would think otherwise:

#1 is separable into two claims—"the shared argument is an immutable borrow" is true today; "the code is illegal" is not true, because the language semantics allow for implicit copies to be introduced to make borrows legal.

The first claim is has no semantic meaning if a copy will be introduced. The copy changes the behavior of the program, so of course the user needs to know about it.

1 Like

What determines whether the program prints "5" or "6" is whether a caller passed the same variable as arguments to sh and io. It is illegal to do so because io requires exclusive access, so updates to io within f cannot modify anything other than that argument, and likewise, sh requires shared access to its argument, which implies that neither f nor any other code in the program can modify the argument during the call to f. Therefore, for an address-independent type like Int, it does not matter whether f takes sh by reference or by value, or whether it takes io by reference or by value-result. If the code were instead:

func f(sh: __shared Int, sh2: __shared Int) { ... }

var x = 5
f(sh: x, sh2: x)

then it wouldn't matter whether the arguments were passed by value or reference, or where zero, one, or two copies happened inside the caller.

The copy is necessary to form the call to f in the first place, given the constraints on its parameters.

It's true that "owned", "shared access", and "exclusive access" can be roughly corresponded to "by value", "by const reference", and "by mutable reference" for normal value types (and pretty much all Swift types today), but the distinction will become more important when we have move-only types for more interesting things like concurrency primitives.

1 Like

Part of our goal here AIUI is that, by default, users don't have to think about borrowing or copies in Swift unless they use language features that require these be made explicit.

If the promise of progressive disclosure is to be met, then it cannot be that the program prints either “5” or “6” contingently. It should print exactly what the following prints:

func f(sh: Int, io: inout Int) {
  io += 1
  print(sh)
}
var x = 5
f(sh: x, io: &x)
1 Like

Yes, that's what I'm saying—either it behaves exactly as today with copyable types, or is an error with move-only types. There would be no circumstance under which sh and io can legally mutably alias regardless of how sh is annotated.

Andy and I met in person and discussed this, it turns out we're in agreement on everything being discussed here. Our messaging around this feature is currently doing a poor job of keeping the intended "layers" of disclosure distinct which is leading to the confusion.

6 Likes

Thanks @Joe_Groff. That's right, there's no debate about how we want the code to behave. The debate is about how people will expect the code to behave based on the name __shared and how it was described earlier in this thread and in the current documentation. I'll try restating my position again in case it wasn't clear (yes, I think it's pretty important).

I want to unambiguously communicate the argument passing semantics for a trivial function application. I don't expect users to pick apart separate complex caller/callee semantics and try to infer the simple argument passing semantics from that.

Tweaking the original example:

func f(sh: __shared Int, f: () -> ()) {
  f()
  print(sh)
}

var x = 5
let f = { x += 1 }
f(sh: x, f: f)

By messaging that __shared is a "shared value" convention, we encourage people to interpret it as Rust's 'immutable borrow'. The only other possible interpretation, given that we've messaged it as a change to argument passing semantics would be C++ 'const &'. Both are wrong.

The counter argument is that users should know that it's impossible to pass a mutable variable of copyable type as __shared, and therefore they should expect the compiler to create a new invisible variable that can then be "shared" with the callee. Even if I could accept this logic at face value, it is totally counter intuitive. Swift could easily have made it legal to pass a mutable variable of copyable type as __shared with exclusivity enforced. The fact that it doesn't work this way then just looks like an implementation quirk that no one would expect.

In my mind, when the user writes f(sh: x, f: f), at the highest level of semantics it should mean that x is the "variable" that will be bound to the sh argument. It's unnecessarily confusing to think about this as the caller implicitly passing a new invisible variable to sh. In fact, I would say that, by creating that temporary phantom "variable", the compiler contradicts any reasonable interpretation of __shared.

Instead, I would progressively explain Swift's conventions this way:

Level 1: Argument Passing

Value types have value semantics, reference types have reference semantics. Swift's argument passing semantics are always pass-by-value for value types where inout means copy-in, copy-out. End of story. Whether a callee declares ownership of the argument does not affect these semantics.

Level 2: Lifetime

For lifetime-managed values (references or move-only types) the user can override whether the caller or callee takes ownership of the argument for the purpose of ending its lifetime. This can reduce the copies required at the implementation level. There are no semantics here. The compiler is free to destroy things out of order.

Level 3: Move-only semantics (straw man)

Now the declaration of ownership can affect semantics simply because variables of move-only types are semantically different depending on whether the variable has ownership. So it's really the implicit ownership conversions that have semantics, not the argument passing itself. For arguments of move-only type:

  • owned to owned: implicit move

  • owned to unowned: implicit borrow (exclusivity enforced)

  • unowned to owned: illegal

For mutable variables with copyable types, all of the above implicitly copy, as pass-by-value semantics dictate. The only one left is:

  • unowned to unowned (any type): no implicit operations

Passing an unowned variable to an unowned argument is the only situation that could reasonably be called "shared".

In the owned to unowned case, even though copyable types will be semantically copied, as an optimization and ABI convention, the implementation can share references by guaranteeing lifetimes. This ABI is required to implement move-only semantics, but does not by itself change program behavior!

Level 4: Explicit borrow and move (straw man)

I think it's important for the programmer to be able to achieve the same argument passing semantics for copyable types that move-only types will implicitly adopt. So, to get the behavior of Rust's immutable borrow we could invent syntax like f(sh: borrow(x), f: f). This would give the user a way to declare an expectation of zero abstraction cost, free the implementation to remove physical copies, and, in the running example above, produce a diagnostic that the code is illegal because f will modify x.

7 Likes
Terms of Service

Privacy Policy

Cookie Policy