-
Notifications
You must be signed in to change notification settings - Fork 52
Add #[empty_trace]
attribute for Trace
derive macro
#185
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
base: master
Are you sure you want to change the base?
Conversation
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 should work. The safety requirement is that all fields must implement EmptyTrace.
I'd actually go as far as generating calls to an empty function assert_empty_trace<T: EmptyTrace>(&T)
in these bodies so that even if we change how the bounds get generated it will get verified.
Do you mean instead of generating the #[derive(Trace, Finalize)]
struct Foo<T>(T);
fn test() {
Gc::new(Foo(0));
// This would fail:
// Gc::new(Foo(Rc::new(0)));
} This And what would be the precise safety requirements for custom implementations of |
No, you've misunderstood the proposition: it's not "instead of" , it's "in addition to", and it doesn't involve asserting things about type parameters, it's asserting things about fields. So #[derive(EmptyTrace)]
struct Foo<T, U> {
x: u8,
y: SomeGeneric<T>,
z: U
} would produce an assertion for
Yes. A Gc or a GcCell. |
gc_derive/src/lib.rs
Outdated
// Add where bounds for all bindings manually because synstructure only adds them if they depend on one of the parameters. | ||
let mut where_predicates = Vec::new(); | ||
for v in s.variants() { | ||
for bi in v.bindings() { | ||
let ty = &bi.ast().ty; | ||
let span = ty.span(); | ||
where_predicates.push(parse_quote_spanned! { span=> #ty: ::gc::EmptyTrace }); | ||
} | ||
} | ||
for p in where_predicates { | ||
s.add_where_predicate(p); | ||
} | ||
s.add_bounds(AddBounds::None); |
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.
Isn’t this what AddBounds::Fields
is for?
// Add where bounds for all bindings manually because synstructure only adds them if they depend on one of the parameters. | |
let mut where_predicates = Vec::new(); | |
for v in s.variants() { | |
for bi in v.bindings() { | |
let ty = &bi.ast().ty; | |
let span = ty.span(); | |
where_predicates.push(parse_quote_spanned! { span=> #ty: ::gc::EmptyTrace }); | |
} | |
} | |
for p in where_predicates { | |
s.add_where_predicate(p); | |
} | |
s.add_bounds(AddBounds::None); | |
s.add_bounds(AddBounds::Fields); |
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.
Also, yeah, this. I found this strange but hadn't yet dug deeper.
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.
It's what I tried to explain in the comment at the top. If you use AddBounds::Fields
then it skips any field that does not mention a parameter. But we need those to implement EmptyTrace
too.
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.
Ah, yeah, I suggest generating assertions for them; the typical way that custom derives work is that the actual method code enforces that each field implements the trait. This trait doesn't have a method (we could add a hidden defaulted one).
Otherwise the field trait bounds escape into the docs.
gc/src/empty_trace.rs
Outdated
/// TODO: Safety conditions | ||
pub unsafe trait EmptyTrace {} | ||
|
||
unsafe impl EmptyTrace for String {} |
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.
We should unify this with simple_empty_finalize_trace!
in trace.rs
and get implementations for almost everything for which we provide Finalize
and Trace
.
Sort of a side note, but Box<String>
/Rc<String>
are unusual because of the double indirection; typically Box<str>
/Rc<str>
/some thin pointer equivalent should be preferred.
Also, to clarify, my request for generating assertions is more of a defense-in-depth mechanism. Adding the right where bounds works, this just provides an extra check for the same thing done differently. For complex custom derives I've found it useful when you start tweaking the actual bounds produced. |
The the assertions seem redundant to me. I see that they would make sense if the trait contained any methods that actually handled values of the type and required that condition, but, in this situation, they would just literally repeat the types from the where clause. It would also some significant complexity since you can't put them inside the trait implementation because there are no methods to implement. |
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.
If we have an EmptyTrace
trait, the #[empty_trace]
attribute should be unnecessary. We can just provide Trace
implementations:
impl<T: EmptyTrace + ?Sized> Finalize for Rc<T> {}
unsafe impl<T: EmptyTrace + ?Sized> EmptyTrace for Rc<T> {}
unsafe impl<T: EmptyTrace + ?Sized> Trace for Rc<T> {
unsafe_empty_trace!();
}
and then users can put Rc
in their structs without extra annotation.
gc/src/empty_trace.rs
Outdated
unsafe impl<T: EmptyTrace> EmptyTrace for Rc<T> {} | ||
|
||
unsafe impl<T: EmptyTrace> EmptyTrace for Box<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.
Should allow T: ?Sized
.
The ideal is to not have extraneous where bounds in the where clause; the impl gets wordy otherwise.
It's mild complexity: you add a doc(hidden) defaulted method, or you generate an additional hidden function. I don't think this is strictly necessary, I just have found for more complex impls that this type of assertion has saved us from problems more often nthan not. |
Keeping trivial where bounds out of the docs sounds like a good argument. I will see if it's easy to do. Instead of using a hidden method, one could also use a separate The only downside for the user would be that the error messages are slightly less consistent and helpful. I do find it a bit strange when |
The |
@s9ferech yeah, you can just make a function, potentially stuffed inside a I'm still not sure if we should do this but I mostly wanted to offer it. We can at least start with just adding field bounds. |
Looking back at #109 (comment), it seems the argument was that if we provide However, given #134, I don’t think it’s possible to have a correct |
I have just added some trait implementations and was wondering if there is a reason why |
pub unsafe trait EmptyTrace {} | ||
|
||
// TODO: The README needs to be updated to explain when `Rc` and the other types here can be managed by GC. | ||
impl<T: EmptyTrace + ?Sized> Finalize for Rc<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.
impl<T: EmptyTrace + ?Sized> Finalize for Rc<T> {} | |
impl<T: ?Sized> Finalize for Rc<T> {} |
since Finalize
itself doesn’t recursively finalize inner members (we have Trace::finalize_glue
for that).
Same for the rest.
impl<T: EmptyTrace + ?Sized> Finalize for RefCell<T> {} | ||
unsafe impl<T: EmptyTrace + ?Sized> Trace for RefCell<T> { | ||
unsafe_empty_trace!(); | ||
} | ||
unsafe impl<T: EmptyTrace + ?Sized> EmptyTrace for RefCell<T> {} | ||
|
||
impl<T: EmptyTrace + ?Sized> Finalize for Cell<T> {} | ||
unsafe impl<T: EmptyTrace + ?Sized> Trace for Cell<T> { | ||
unsafe_empty_trace!(); | ||
} | ||
unsafe impl<T: EmptyTrace + ?Sized> EmptyTrace for Cell<T> {} | ||
|
||
impl<T: EmptyTrace> Finalize for OnceCell<T> {} | ||
unsafe impl<T: EmptyTrace> Trace for OnceCell<T> { | ||
unsafe_empty_trace!(); | ||
} | ||
unsafe impl<T: EmptyTrace> EmptyTrace for OnceCell<T> {} | ||
|
||
impl<T: EmptyTrace + ?Sized> Finalize for Mutex<T> {} | ||
unsafe impl<T: EmptyTrace + ?Sized> Trace for Mutex<T> { | ||
unsafe_empty_trace!(); | ||
} | ||
unsafe impl<T: EmptyTrace + ?Sized> EmptyTrace for Mutex<T> {} | ||
|
||
impl<T: EmptyTrace + ?Sized> Finalize for RwLock<T> {} | ||
unsafe impl<T: EmptyTrace + ?Sized> Trace for RwLock<T> { | ||
unsafe_empty_trace!(); | ||
} | ||
unsafe impl<T: EmptyTrace + ?Sized> EmptyTrace for RwLock<T> {} | ||
|
||
impl<T: EmptyTrace> Finalize for OnceLock<T> {} | ||
unsafe impl<T: EmptyTrace> Trace for OnceLock<T> { | ||
unsafe_empty_trace!(); | ||
} | ||
unsafe impl<T: EmptyTrace> EmptyTrace for OnceLock<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.
None of these other types have the sharing problem that Rc<T>
and Arc<T>
have. It should be possible to give them full Trace
implementations with only a T: Trace
constraint.
(There are some subtleties here—for example, we need to trace a Mutex
with try_lock
instead of lock
in case safe code has leaked a MutexGuard
. But I think they’re all solvable.)
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.
Is the internal mutability not a problem too? I thought that was the reason why GcCell
is required.
@@ -286,6 +353,7 @@ unsafe impl<T: Trace, E: Trace> Trace for Result<T, E> { | |||
} | |||
}); | |||
} | |||
unsafe impl<T: EmptyTrace, E: Trace> EmptyTrace for Result<T, E> {} |
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.
unsafe impl<T: EmptyTrace, E: Trace> EmptyTrace for Result<T, E> {} | |
unsafe impl<T: EmptyTrace, E: EmptyTrace> EmptyTrace for Result<T, E> {} |
@@ -325,6 +396,7 @@ unsafe impl<K: Trace, V: Trace, S: Trace> Trace for HashMap<K, V, S> { | |||
} | |||
}); | |||
} | |||
unsafe impl<K: EmptyTrace, V: EmptyTrace> EmptyTrace for HashMap<K, V> {} |
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.
unsafe impl<K: EmptyTrace, V: EmptyTrace> EmptyTrace for HashMap<K, V> {} | |
unsafe impl<K: EmptyTrace, V: EmptyTrace, S: EmptyTrace> EmptyTrace for HashMap<K, V, S> {} |
|
||
impl<T: ?Sized> Finalize for PhantomData<T> {} | ||
unsafe impl<T: ?Sized> Trace for PhantomData<T> { | ||
unsafe_empty_trace!(); | ||
} | ||
unsafe impl<T: EmptyTrace> EmptyTrace for PhantomData<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.
unsafe impl<T: EmptyTrace> EmptyTrace for PhantomData<T> {} | |
unsafe impl<T> EmptyTrace for PhantomData<T> {} |
By definition, PhantomData<T>
does not contain a T
.
I just realized that the derive macro for |
As suggested in issue #109, I have added a marker trait,
EmptyTrace
, for types that don't require tracing, a derive macro for this trait and an attribute ,#[empty_trace]
, for theTrace
derive macro that allows users to safely skip fields which implementEmptyTrace
. All of this is demonstrated in gc/tests/empty_trace.rs. I believe that something like this would be very nice to have, especially since theTrace
implementation forRc
does not exist anymore.Would this implementation be safe? I am not familiar with the exact safety assumptions that the library makes about
Trace
implementations. Or would there be any other issues with this approach?