-
Notifications
You must be signed in to change notification settings - Fork 2.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[stdlib] Implement Weak[T] refs for Arc[T] #3450
base: nightly
Are you sure you want to change the base?
Conversation
Signed-off-by: Sawyer Bergeron <sawyerbergeron@gmail.com>
Signed-off-by: Sawyer Bergeron <sawyerbergeron@gmail.com>
Signed-off-by: Sawyer Bergeron <sawyerbergeron@gmail.com>
Signed-off-by: Sawyer Bergeron <sawyerbergeron@gmail.com>
Signed-off-by: Sawyer Bergeron <sawyerbergeron@gmail.com>
5a66a4e
to
b19789b
Compare
Signed-off-by: Sawyer Bergeron <sawyerbergeron@gmail.com>
Signed-off-by: Sawyer Bergeron <sawyerbergeron@gmail.com>
Needs some fixes after parameterizing Arc for Weak, but recent compiler changes have fixed the parameter propagation/inference issues so it's just a matter of fixing the broken ref count. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @szbergeron, thanks for tackling this! 🙂 This will be a nice QoL thing for folks doing self-referential data structures in Mojo.
I've left a few initial comments on the implementation strategy, particularly about how we might be able to make stronger guarantees around the enable_weak: Bool
parameter using the type system.
Let me know if my suggestions are workable! 🙂
Thanks again for your contribution, awesome stuff 🔥
T: The type of the value that this Weak may be upgraded into an Arc[T] of. | ||
""" | ||
|
||
alias _inner_type = _ArcInner[T] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question Should this be:
alias _inner_type = _ArcInner[T] | |
alias _inner_type = _ArcInner[T, True] |
since this Weak
must have been derived from an Arc[T, True]
?
return self._inner[].payload | ||
return self._inner[].payload.assume_initialized() | ||
|
||
fn downgrade(self) -> Weak[T, enable_weak]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fn downgrade(self) -> Weak[T, enable_weak]: | |
fn downgrade(self: Arc[T, True]) -> Weak[T]: |
else: | ||
constrained[False, "A typical Arc[T] does not support downgrading." + | ||
"If you want to enable downgrading of this Arc[T], convert it to an Arc[T, enable_weak = True]"]() | ||
return abort[Weak[T, enable_weak]]("unreachable") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion By constraining self: Arc[T, True
(shown in my comment above), I think this whole statement could be simplified to just:
return abort[Weak[T, enable_weak]]("unreachable") | |
return Weak(self) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the simplification, but is there a good way with that change of presenting specific, actionable instructions to the user that what they are trying to do can only be done if they make their Arc enable_weak = True
?
@@ -110,6 +309,16 @@ struct Arc[T: Movable](CollectionElement, CollectionElementNew): | |||
other._inner[].add_ref() | |||
self._inner = other._inner | |||
|
|||
fn __init__(inout self, ptr: UnsafePointer[Self._inner_type], private: ()): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion We could annotate this with @doc_private
instead of using a private
parameter.
@@ -24,7 +24,7 @@ from memory import UnsafePointer, bitcast | |||
from sys.info import triple_is_nvidia_cuda | |||
|
|||
|
|||
struct Atomic[type: DType]: | |||
struct Atomic[type: DType](ExplicitlyCopyable): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question Why is this needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was to allow me to construct the [0|1] length array for weak. I wanted to revisit the specifics, since having an array of atomics can be useful in a lot of contexts but there isn't currently a good way of constructing one without arcane MaybeUninit incantations. I do have some questions about the atomic type that can be had out-of-thread too, that (I think) make Arc technically unsound in a single threaded context and relatively impossible to use in a multi-threaded one
alias _inner_type = _ArcInner[T] | ||
var _inner: UnsafePointer[Self._inner_type] | ||
|
||
fn __init__(inout self, strong: Arc[T, enable_weak]): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion Likewise, we can change this initializer so that Weak[T]
can only be constructed frfom an Arc[T, True]
:
fn __init__(inout self, strong: Arc[T, True]):
This would let you remove the constrained[enable_weak, "..."]()
below as well 🙂
other._inner[].add_weak() | ||
self._inner = other._inner | ||
|
||
fn upgrade(inout self) -> Optional[Arc[T]]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion I think this needs to encode that this is an Arc
containing a weak count:
fn upgrade(inout self) -> Optional[Arc[T]]: | |
fn upgrade(inout self) -> Optional[Arc[T, True]]: |
(self._inner).destroy_pointee() | ||
self._inner.free() | ||
self._inner = UnsafePointer[Self._inner_type]() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Request This "nulling out" of fields is not required within __del__()
methods. After the __del__()
completes, this object instance is no longer alive, so there's no code that could observe self._inner
one way or the other 🙂
|
||
alias _weak_refcount_type = __mlir_type[ | ||
`!pop.array<`, Int(enable_weak).value, `, `, Atomic[DType.int64], `>` | ||
] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment This is clever using the boolean to make this a zero-sized field, neat trick! 🙂
""" | ||
self.payload.assume_initialized_destroy() | ||
|
||
fn try_upgrade_weak(inout self) -> Bool: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion I think this could use the "custom self types" / "conditional conformance" trick to be statically constrained to:
fn try_upgrade_weak(inout self) -> Bool: | |
fn try_upgrade_weak(inout self: Arc[T, True]) -> Bool |
and then the @parameter if enable_weak
statement below could be simplified.
This implements a Weak[T] non-owning ref for Arc[T] that should allow for more ergonomic implementation of cyclic data structures without having to fall back to UnsafePointer[T] or Optional[Arc[T]] + complex directed drop impls.
This increases the footprint of _ArcInner by (minimally) 8 bytes. If we wish to cut this down, specialization and conditional inclusion of the field (once implemented) may allow for Arc[T, Kind=StrongOnly] constructs.