~Copyable and functions

Most of Swift is magical. inout is only explicit because experience with reference parameters in C++ proved that it was too easy to get logic errors because a variable was unexpectedly passed by l-value reference and modified.

This isn’t quite correct either. Most function parameters are implicitly borrowed by default; only arguments to setters and initializers are consuming by default, as a guess for best performance. So bar(arg) does not consume arg either.

(As noted elsewhere this ā€œdefaultā€ is not quite the same since it doesn’t impose the no-implicit-copies restriction, but in terms of ā€œcan you use the thing againā€ it’s definitely the case.)

3 Likes

That's what I thought before this thread started, but if consuming parameters default to being borrowed when passed to functions with default argument conventions then the lifetime on c shouldn't have ended when he called copy on it.

If bar(arg) isn't translated as bar(consume arg), then it must be doing:

bar(borrow arg)
_ = consume arg

Unless this is all just a bug in the implementation of explicitly consuming parameters, and

func copy<T>(_ value: T) -> T { value }

func foo(a: inout Int, b: consuming Int) {
    a = copy(b)
    a = b
}

is supposed to work.

EDIT: Wait, this does work! It only errors if copy isn't generic! This must be a bug. Changing copy from a function that takes an Int into a generic function that takes a T shouldn't change whether b is consumed when you call it.

EDIT 2: Bug reported, Issue 70659.

Int is trivially-copyable too, which could be interfering with diagnostics (but shouldn’t). String might be a better test in general. But yes, a consuming parameter is consumed when the function exits if it hasn’t been consumed already.

…before optimizations, anyway. Optimizations are permitted to do a handful of things, including turning a copy+consume into a forward. So looking at generated SIL or LLVM IR or assembly may not reflect the formal semantics of the language, just like how assembly doesn’t have struct types.

(No longer at Apple, but I’ll still say thank you for filing the bug!)

3 Likes

i thought they were __owned by default?

That’s a spelling that currently exists for opting into the ā€œimplicitly-consumingā€ behavior, yes. I try not to use undocumented, ambiguously-supported features to describe documented ones. (For example, compiler crashes with __owned aren’t nearly as high-priority as those with consuming or with plain initializers or setters.)

1 Like

The SE-0377 acceptance post also says that initializer parameters default to consuming.

but there’s an important difference in mutating an __owned parameter will always copy the buffer, whereas we’re ā€œsupposedā€œ to be able to mutate a consuming parameter if there are no external references to it.

as far as i’m aware, it is not safe (from a performance/memory standpoint) to modify a bare parameter to an init.

I don’t think that’s contractual either way, just that the parameter is live until the end of the function. But we’re moving into the realm of documentation, and I think the documentation will probably say ā€œif you care about minimizing copies, use borrowing or consuming explicitlyā€.

but consuming is way too blunt, as it enforces that the parameter can only be moved once, whereas i’m generally more interested in ensuring that the parameter can be moved before the end of the function.

That’s something to take up in a separate thread in the Evolution section of the forum.

The differences between consuming and __owned are:

  • __owned always affects symbol names for public ABI when used, whereas consuming only affects the symbol name if it changes the default convention.
  • __owned parameters are locally implicitly copyable, whereas consuming parameters locally require explicit copy x anywhere they may potentially require a copy.
  • __owned parameters are locally immutable, whereas consuming parameters are locally mutable. In neither case are local mutations exposed in the caller.

There should be no reason to continue using __owned unless you're stuck with __owned affecting your symbol names in your ABI. If (the compiler thinks) you need to copy a consuming parameter, and its type is Copyable, you can still explicitly copy it.

The compiler hasn't historically tried to do any lifetime tracking at all with trivial types so there are definitely some known issues with using these modifiers with types like Int in Swift 5.9. Nightly compilers might be a little better.

9 Likes

if i’m understanding correctly, does that mean the guidance here is no longer valid?

if so, i would consider that a massive improvement over what is specified in the ownership vision document. but it contradicts much of what we’ve been told about __owned so far.

tangentially, there is also an unattended compiler bug that causes very hard-to-debug ARC drift on 5.9 toolchains when using consuming.

1 Like

From the caller's perspective, working with copyable values, the use of __owned vs. consuming by the callee should make no difference whatsoever. You do still have to use consume x to explicitly end a copyable value's lifetime if you want to forward ownership of it at a specific point.

3 Likes

i was not aware that consume could be applied to __owned parameters, but lo and behold it does compile.

struct S
{
    var y:[Int]

    init(x:[Int])
    {
        var y:[Int] = consume x
        y.append(1)
        self.y = y
    }

neat!

for curiosity’s sake, i wondered if you could also get away with consume-ing a __shared parameter:

    mutating
    func f(x:[Int])
    {
        var y:[Int] = consume x
        y.append(1)
        self.y = y
    }

the compiler gets confused by that, but then again it should not have compiled in the first place.

$ swift test.swift 
test.swift:16:23: error: 
'consume' applied to value that the compiler does not support. 
This is a compiler bug. Please file a bug with a small example of the bug
        var y:[Int] = consume x
1 Like

Yeah, the internal diagnostic looks like a bug, but you should get a diagnostic trying to consume something whose lifetime can't actually be shortened in practice, like a global, escaped local, or __shared/borrowing parameter.

4 Likes

I think you just misread @jrose's comment on shortened lifetimes. He wasn't saying that you can't shorten them yourself, he was saying that the compiler will no longer do so automatically. In particular, I think he was referring to the move to lexical lifetimes.

1 Like

yes, i was under the false impression that consume is related to consuming, because i had seen the ā€œthis is a compiler bugā€ error many times on parameters without the keyword and thought it was expecting the keyword to be present.

1 Like