M
Modular•11mo ago
NickJJ

Adding __takeinit__ makes a difference in assignment

I'm playing with HeapArray and I cannot understand what is going on.
struct HeapArray:
var data: Pointer[Int]
var size: Int

fn __init__(inout self, size: Int, val: Int):
self.data = Pointer[Int].alloc(size)
self.size = size

for i in range(size):
self.data.store(i, val)

fn dump(self):
for i in range(self.size):
print(self.data.load(i))

fn __copyinit__(inout self, existing: Self):
print("HeapArray copyinit")
self.data = Pointer[Int].alloc(existing.size)
self.size = existing.size
for i in range(self.size):
self.data.store(i, existing.data.load(i))

fn __moveinit__(inout self, owned existing: Self):
print("HeapArray moveinit")
print("existing address: ", existing.data.__as_index())

self.data = existing.data
self.size = existing.size
existing.data = Pointer[Int].get_null()
existing.size = 0

fn __takeinit__(inout self, inout existing: Self):
print("HeapArray takeinit")
self.data = existing.data
self.size = existing.size
existing.data = Pointer[Int].get_null()
existing.size = 0

fn __del__(owned self):
print("Memory freed at address: ", self.data.__as_index())

fn main():
let a = HeapArray(3, 2)
print("a address: ", a.data.__as_index())
let b = a
print("b address: ", b.data.__as_index())
a.dump()
b.dump()
struct HeapArray:
var data: Pointer[Int]
var size: Int

fn __init__(inout self, size: Int, val: Int):
self.data = Pointer[Int].alloc(size)
self.size = size

for i in range(size):
self.data.store(i, val)

fn dump(self):
for i in range(self.size):
print(self.data.load(i))

fn __copyinit__(inout self, existing: Self):
print("HeapArray copyinit")
self.data = Pointer[Int].alloc(existing.size)
self.size = existing.size
for i in range(self.size):
self.data.store(i, existing.data.load(i))

fn __moveinit__(inout self, owned existing: Self):
print("HeapArray moveinit")
print("existing address: ", existing.data.__as_index())

self.data = existing.data
self.size = existing.size
existing.data = Pointer[Int].get_null()
existing.size = 0

fn __takeinit__(inout self, inout existing: Self):
print("HeapArray takeinit")
self.data = existing.data
self.size = existing.size
existing.data = Pointer[Int].get_null()
existing.size = 0

fn __del__(owned self):
print("Memory freed at address: ", self.data.__as_index())

fn main():
let a = HeapArray(3, 2)
print("a address: ", a.data.__as_index())
let b = a
print("b address: ", b.data.__as_index())
a.dump()
b.dump()
a address: 94060226983856
HeapArray copyinit
HeapArray moveinit
existing address: 94060215912848
b address: 94060215912848
2
2
2
Memory freed at address: 94060226983856
2
2
2
Memory freed at address: 94060215912848
a address: 94060226983856
HeapArray copyinit
HeapArray moveinit
existing address: 94060215912848
b address: 94060215912848
2
2
2
Memory freed at address: 94060226983856
2
2
2
Memory freed at address: 94060215912848
This is super weird to me. It calls copyinit and moveinit, why?
14 Replies
ModularBot
ModularBot•11mo ago
Congrats @NickJJ, you just advanced to level 5!
Chris Lattner
Chris Lattner•11mo ago
Hey Nick, couple things here: first, you can drop takeinit from the picture. It's a weird thing that hasn't worked out well - so we're moving it to be a formal ".take()" method with its own trait in the future, instead of being a magic dunder method that is mixed into ^ semantics. It's not obvious to me why the move is being called. It should just call the copyinit. I'll investigate. Hrm, I'm not really sure. Top of tree on my laptop seems to work:
a address: 105553166371936
HeapArray copyinit
b address: 105553166499840
2
2
2
Memory freed at address: 105553166371936
2
2
2
Memory freed at address: 105553166499840
a address: 105553166371936
HeapArray copyinit
b address: 105553166499840
2
2
2
Memory freed at address: 105553166371936
2
2
2
Memory freed at address: 105553166499840
It is entirely possible that there was a transient bug that got introduced and fixed. The entire internals of the compiler are getting replumbed to use references instead of raw pointers, and this work is now settling out. It is possible that an extraneous copy was getting made because of a conversion from ptr<->ref or something. Oh, I didn't notice that this was correlated to adding takeinit actually! Well that make sense why it is fixed 🙂 Sneak peak from the next release's changelog:
- The `__takeinit__` special constructor form has been removed from the
language. This "non-destructive move" operation was previously wired into the
`x^` transfer operator, but had unpredictable behavior that wasn't consistent.
Now that Mojo has traits, it is better to model this as an explicit `.take()`
operation on a type, which makes it clear when a lifetime is ended vs when the
contents of an LValue are explicitly taken.
- The `__takeinit__` special constructor form has been removed from the
language. This "non-destructive move" operation was previously wired into the
`x^` transfer operator, but had unpredictable behavior that wasn't consistent.
Now that Mojo has traits, it is better to model this as an explicit `.take()`
operation on a type, which makes it clear when a lifetime is ended vs when the
contents of an LValue are explicitly taken.
NickJJ
NickJJOP•11mo ago
Hi Chris, thank you very much for your insightful response and for taking the time to investigate my question. Your explanation has been incredibly helpful, and I now have a clearer understanding of the upcoming changes regarding takeinit. I am excited to hear about the rapid evolution of Mojo and the updates regarding takeinit. In my view, takeinit seemed like a less memory-safe version of moveinit, so I am pleased with the shift towards an explicit .take() method.
Chris Lattner
Chris Lattner•11mo ago
Cool, it really helps clarify the model as well. It was very weird how x^ would sometimes end a lifetime and sometimes not.
gryznar
gryznar•11mo ago
Is that mean that ^ won't be needed anymore? It is only one magical char introduced to Mojo and it is not self-explanatory. It can be easily replaced with ".move()" or eventually take(obj) and move(obj)
ModularBot
ModularBot•11mo ago
Congrats @gryznar, you just advanced to level 7!
Chris Lattner
Chris Lattner•11mo ago
x^ is still allowed and is used when you want to end the lifetime of a value. This is an exotic thing that system programmers may care about (e.g. with move only or non-movable types) not something that app-level programmers will care about. this does refine/clarify/narrow that x^ does though!
gryznar
gryznar•11mo ago
I see, but still, it will be much clearer if keyword / method will be used instead of single char which does not express its intention without diving into docs Mojo may be way, way more accessible for low level programming, because it does not hides this complexity behind magical chars. Its syntax is self explanatory. Only "^" differs here. I see Mojo as a bridge which connects high and low level programming in very natural way. Every inconsistency against, looks strange imho
sora
sora•11mo ago
I would argue that exotic things should look exotic. I think we should distinguish confusion cause by unfamiliarity (what does let mean) from those caused by ambiguity (std::move usually comes in quantified form). I'm against using the name take for non-destructive moves as well. It looks too generic, and takes a perfectly good method name away from application developers. Imagine using a torch like api, where take sometimes means array indexing and sometimes move. Why don't we just call it nondestructive_move, take_lvalue, or something as ugly and long (again, exotic things should look exotic)? Moreover, it's always easy to migrate from a long and ugly name to a shorter one (if people really think a short generic name is a good one).
gryznar
gryznar•10mo ago
I agree, but still I think, that exotic name looks much better than exotic char 🙂
sora
sora•10mo ago
In this particular case, I think the ^ glyph looks like an arrow and converses the meaning of "move" pretty well. But that might be just me.
Melody Daniel
Melody Daniel•10mo ago
I agree with you. It somehow seemed to fit with its functionality when I learned what it was supposed to do. I'm not a fan of special characters in a language though. Simply because what they're supposed to do isn't clear at first glance, you need to get into learning the language to understand them.
gryznar
gryznar•10mo ago
Lack of clarity at first glance - my biggest concern
sora
sora•10mo ago
Yea, but (I think) it doesn't cause much confusion after literally the first glance. If we were to use a method or free function to indicate move, they are a) somehow magical; or b) we make their semantic explicit by using some kind of annotation, and somehow circle back to using symbol or tag. Taking an example from the doc, where transfer is indicated by the magical ^. How do you implement (a somehow blessed, and magical) move without using move?
# This context manager consumes itself and returns it as the value.
fn __enter__(owned self) -> Self:
return self^
# This context manager consumes itself and returns it as the value.
fn __enter__(owned self) -> Self:
return self^
Want results from more Discord servers?
Add your server