Open nmsmith opened 7 months ago
It doesn't just clarify that the lifetime of an object is going to end, it does a transfer operation. That is, it transfers the ownership of the object. The object's lifetime did not end, it just is no longer usable in the current scope.
it does a transfer operation
There's no such thing as a transfer operation. No code needs to be executed to transfer ownership. It's a concept that only exists for the purpose of type checking, and to decide whether a destructor needs to be called at some point.
it transfers the ownership of the object
Mojo doesn't really have a notion of "object" right now. It just has "variables" and "values".
When does moveinit get executed?
Moveinit gets executed when the right-hand side of an assignment statement is a variable or field name followed by the ^ sigil. For example y = x^
. (However, such a move can often be eliminated during an optimization pass, so there's no guarantee that it will happen.)
Moveinit doesn’t get executed when ^ is used next to an argument, e.g. y = f(x^)
.
I think the problem is that you insist on using a very specific definition for 'operation', and 'ownership transformation' just happens not to be an "operation" according to that definition. As counter examples, the unary +
for Int
also "does nothing", and the identity function is still a function.
^
only implies an operation when it is used in statements of the form<expr> = <expr>^
. In this case, it signals a move operation. (i.e. the invocation of__moveinit__
.)
This is wrong (see example). The documentation made it very clear that "ownership transfer" and "move" are different things, and __moveinit__
being called in b = a^
is merely a implementation detail. I think we should be able to optimise the move away. I actually think we should be able to optimise the move away in simply cases like var b = a^
, and tying __moveinit__
to ownership transfer will prevent us from doing that.
fn main():
var a = S()
var b = a^ # __copyinit__
print(b.value)
struct S:
var value: Int
fn __init__(inout self):
self.value = 0
fn __copyinit__(inout self, other: Self):
self.value = other.value
print("__copyinit__")
In summary, I don't find the semantics of the transfer operator ^
itself confusing at all, and I don't think calling ^
the transfer sigil helps clarifying anything. That said, I do think we could do a better job in explaining when how the implementation works under the hood.
the unary + for Int also "does nothing"
Unary ^
is not the same kind of phenomenon as unary +
. The latter is syntactic sugar for a call to the __pos__
method. If __pos__
has not been defined, then (in Mojo) +
cannot be used. So conceptually, +
executes a piece of code. Yes, somebody could define +
as the identity function, and then "not much" code would need to be executed (you'd still need to copy the value), and potentially the compiler could optimize it away. But that's not the same thing as ^
, because in the context of f(x^)
, the sigil always does nothing, at least in today's Mojo. It does not invoke a dunder method, and nothing is being optimized away.
This is wrong [...] moveinit being called in b = a^ is merely a implementation detail.
I'd say my statement is mostly correct (not merely "wrong"), but yes, you're right, b = a^
appears to invoke __copyinit__
as a "backup" if a type neglects to define __moveinit__
. (This is an uncommon scenario, because by virtue of @value
, almost all data types that implement copyinit will implement both.) If you define both methods, it always calls __moveinit__
, and if you define neither method, the statement is rejected by the compiler.
Because the statement is rejected, the fact that it invokes one of these methods is not an implementation detail. The presence of these methods is essential to the meaning of the statement. This is in contrast to expressions such as f(x^)
. This expression is valid even if x
is non-copyable and non-movable.
Regardless, this is all nitpicking. Broadly, you seem to agree with my contention: unary ^
doesn't correspond to any particular code being executed. In some uses, it might invoke certain dunder methods, and in other uses, it will never invoke a dunder method, nor will it execute any other code—not even an identity function.
Given this, it doesn't really make sense to call ^
an operator, in my opinion.
^
does nothing" confusing: a^
ends the lifetime of a
(its effect might be clearer with the IR). So the problem might lie not in "operator" but "transfer", if there even is one.The fact that b = a^
needs a
to be copyable/movable because of =
(which is called the assignment operator). Let's assume that we have a let $a = b in ...
form on the meta level:
b = a^ → let $a = a^ in
`=`(b, $a)
f(a^) → let $a = a^ in
f($a) # "always valid" because `f` doesn't impose extra constraints
^
is also clearly an operator in some lifetime algebra: it's curial that a^^ ≡ a^
and a^ ≡ a
if a
is reg type.move
(you might as well say that they have default move
derived from copy
). What really matters is that when we do have both, copy; del
is equivalent move
. The fact that a (bad) compiler can always choose copy; del
makes calling exactly which dunder method "implementation detail".__name__
("dunder") which is entirely different.
Where is the problem?
https://docs.modular.com/mojo/manual/values/ownership#transfer-arguments-owned-and-
What can we do better?
Throughout the Mojo docs, the postfix
^
sigil is described as an operator. But I believe that most programmers who understand the purpose of this sigil would be reluctant to describe it as such. For example, when this sigil is placed next to a variable being passed as an argument, e.g.foo(x^)
, it doesn't trigger an operation, i.e. the execution of code. Instead, it just indicates thatfoo
will deinitialize the variable. (In other words,foo
is doing the work, not the^
sigil.)^
only implies an operation when it is used in statements of the form<expr> = <expr>^
. In this case, it usually triggers a move operation, i.e. the invocation of__moveinit__
.^
also has a secondary use as an "operation suppressor", stopping aCopyable
variable from being copied when the variable is used as an owned argument.I have seen several Mojo users confused by the
^
sigil. IMO this is caused by a combination of things:^
changes depending on where it is used.^
is being described as an "operator" even though it is not associated with a particular operation.I suggest referring to
^
as the "transfer marker" or "transfer sigil". At the very least, this will prevent people from conflating it with moving, which is an operation. There isn't a strong correlation between moving and transferring:^
won't necessarily trigger a move (and will never trigger a move for a non-movable type), and moving doesn't necessarily require the use of^
. (Because the compiler often optimizes copies into moves.)Update: There's been some discussion around renaming
owned
toconsumed
. If that happens, it would make sense to call^
the "consumption marker" or "consumption sigil". This allows us to avoid the term "transfer", and will prevent people making false connections to the concept of "moving".Also, the
^
operator is pronounced "carrot", and carrots are consumable. 🤯