[Pitch] Move Function + "Use After Move" Diagnostic

Ok. That’s exactly what I expected. In which case I don’t think move is a great word for what’s happening here. It seems more analogous to moving all the funds from a bank account and closing it down. It’s not the moving that’s salient here, it’s the shutting the account down.

Possibilities:

let y=close(x)
let y=retire(x)
let y=end(x)
3 Likes

This code is perfectly valid if you replace the redeclaration of x with an assignment. Not that moving a small trivial type like Double is particularly useful.

var x = 42.56
let y = move(x)
x = 42

EDIT: Fixed the declaration of x.

I'm trying to verify what is an expected behavior as opposed to an unhandled edge case.


The more of these "that would produce an error" situations crop up, the more I am convinced that it doesn't make sense to model move as a function. The signature of move is given as:

func move<T>(_ t: __owned T) -> T

but based on the discussion here it doesn't seem to me that move behaves similarly enough to a Swift function. The signature is at best misleading, at worst downright wrong—move doesn't accept arbitrary values as input (only 'bindings'), and can't be applied to trivial types.

Could you elaborate on why this is being modeled as a function rather than a move keyword or similar?

6 Likes

I was initially going to argue against adding more keywords, but you make a good point. If it is modeled as a function, it shouldn’t get special-case diagnostics. I can’t think of any precedent for that.

On the other hand, introducing new keywords is a really big deal. If the concept can be represented using existing grammar, it should be.

1 Like

It wouldn't be the first function in the Swift standard library that can't actually be implemented as a function in Swift. There's already type(of:). The best alternative to a move pseudo-function I can think of would be to have it as a contextual keyword e.g. move x. I'm not sure if that could cause any problems, but I'd imagine at worst you'd need to wrap it in parentheses occasionally.

5 Likes

There is a precedent for functions like that. Heck, if you know what you’re doing, you can theoretically implement pretty much anything in Swift. You don’t even need the “unsafe” prefix unless there’s a code path leading to undefined behavior.

The thing that’s different here is the compile-time error for a valid function call. I believe that’s unprecedented. In other words, the function signature is wrong.

2 Likes

Right, it wouldn't be the first (and I believe withExtendedLifetime is similarly unimplementable in a robust way), but I'd appreciate a bit of justification for why this should be a function. Both type(of:) and withExtendedLifetime, AFAICT, at least have the basic semantics of Swift functions—they accept an (arbitrary) value of the proper type, and produce a value.

But move doesn't do this in the same way. The T it accepts and the T it returns should, as far as I understand, be semantic copies under Swift's current rules, but move exists specifically to not copy the value around. Then there's the further issue of the fact that it also operates on 'bindings' rather than values, making it an error to pass, say, the result of a function even though it produces a value of the proper type.

4 Likes

The behavior of Swift.withExtendedLifetime(_:_:) doesn’t technically violate any normal rules either: when you pass an argument to a function, it will remain alive until it is last referenced. The caller can’t make any assumptions about when in the function that is.

Without withExtendedLifetime(_:_:), you’d have to figure out a way to maintain that reference without actually doing anything with it, and without compiler optimizations recognizing a no-op and killing it early anyway.

2 Likes

At the risk of making things way too complicated, would it be possible to use key path literals for this somehow? They seem like a better representation of a binding to me.

I say “literals” because I am well-aware that there is currently no key path type that works with local bindings. But maybe there could be, even if it’s only usable for this purpose right now?

4 Likes

Right, both type(of:) and withExtendedLifetime are not as concerning to me because they basically act like Swift functions. Yes, type(of:) has a bit of special treatment to hook up the argument and result types in ways that aren't possible for normal Swift functions, and withExtendedLifetime uses Builtin.fixLifetime to (I assume) basically make an optimization-proof no-op, but it seems to me that move exists specifically to get around the standard semantics of Swift functions so I'm curious to know why we want it to 'be' a function at all.

ETA: and, of course, we could still have move look like a function, syntax wise (i.e., we would still write move(x), let y = move(x)) without giving it a full signature in the standard library.

Hmmm… so you can reassign a let. Not sure how I feel about that…

2 Likes

I assume @Nobody1707 meant to write var x.

Yeah, that was a mistake. I copied and pasted his example, but forgot to change the declaration of x when I removed the redeclaration. I meant var x.

2 Likes

Here’s a question: if you explicitly move a binding, why wouldn’t you be allowed to make a new one that reuses its former identifier? It’s not ambiguous: the old one is gone.

// Nothing bound to `x`, no issue
let x = 42

// Moves binding from `x` to nothing
_ = move(x)

// Nothing bound to `x`, no issue
let x = 8*6

Is that prohibited purely due to the potential for confusion?

2 Likes

From pure correctness perspective, there's no reason not to allow the redeclaration. But the language doesn't normally allow shadowing in the same scope, and it's a small enough edge case that I can't see it being worthwhile to add code to the compiler to allow it as a special case.

3 Likes

As far as the compiler is concerned, those would be two completely unrelated bindings.

Yes, that's what shadowing means. The compiler currently explicitly prevents you from shadowing any bindiing unless you're in a new scope. I don't think it's worth adding in a new special case to allow shadowing after a move from a let when the problem could just as easily be solved by declaring the let as a var.

1 Like

No, shadowing specifically refers to allowing a binding in an inner scope to have the same identifier as a pre-existing binding in an outer scope, thus making the outer binding inaccessible in favor of the inner binding, while potentially maintaining both.

That’s not what I’m talking about. In this case, there isn’t another binding with the same identifier by the time that code is reached. It was killed earlier.

Without a function like Swift.move(_:), the scenario is impossible: using the identifier again would stop the compiler from killing the binding earlier in the first place, and it would fail with a redeclaration error.

I think it's worth considering "move y" syntax even if we know that eventually we'll have normal functions with consuming parameters: once we cross that bridge we can revise it again. other alternative could be: var x <- y

To me lack of aesthetics outweighs idiomaticy here. I'll prefer drop x or drop(x), etc.

1 Like

I think that there is some confusion here that I would like to explain in more detail.

The binding is not being killed per say. What is instead happening is that it is a normal variable that must obey a special data flow rule since it has been passed to move. The data flow rule is in a sense laid on top of the current swift semantics. It just so happens that this data flow rule prevents later uses after the move. Every other rule in the language is absolutely the same except for that one special data flow rule. So the code would still be invalid.

2 Likes