-
Notifications
You must be signed in to change notification settings - Fork 18
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
Derive on structs with generics #79
base: main
Are you sure you want to change the base?
Conversation
(Drawing the rest of the owl) The workaround for DebugBits and DefaultBits is a little overkill, but much more trivial than trying to figure out whether we need to emit the where clause for each field. We should be able to filter it in the future if necessary. Perhaps this should also be refactored into a shared helper function, since it was copy-pasted into two locations.
huh, apparently I caused rust to say "you might be interested in the trivial_bounds feature" and that broke the UI test. Not sure if that means I should update the UI test or try and filter out where clauses where they probably aren't necessary. |
Ah. I should probably switch to just requiring that the generic parameters implement the traits for the same reasons as in rust-lang/rust#26925 and dtolnay/syn#370 , even though those constraints can actually be incorrect (as those issues note) |
In some respects, this is a downgrade, but it matches the behaviour of the standard library derives, syn, serde, etc.
This was a copy-paste error.
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.
thank you for your work, it is appreciated. for now, just a quick thing I've noticed, and I'll take a more thorough look later.
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.
so overall this looks quite good to me. thanks again.
one unfortunate side effect here is that we're back to having an implementation detail const
in the impl block of the generated struct, and now there's an additional PhantomData
field as well.
fortunately these two don't need to be pub
. but even non-pub
things are still in scope for the same module, since this struct is generated in the user's crate.
bilge-impl/src/bitsize_internal.rs
Outdated
/// WARNING: modifying this value directly can break invariants | ||
value: #arb_int, | ||
_phantom: ::core::marker::PhantomData<(#(#phantom),*)> |
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 it correct that adding a PhantomData
here is only needed when generics are involved?
if so, do you think it's possible (and practical) to not generate a _phantom
field unless it's actually needed, just by looking at item.generics
?
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 should be possible, yes, but it would create additional conditions at the construction sites (there are 4 in this PR) and potentially any external derive macros. We could refactor those to use a common constructor method, but I figured I'd go for the approach that requires the least conditional code.
I'm fine either way, though.
RE: the implementation detail const: we can generate an un-namable trait that has an associated const, and implement it. struct Example;
// macro generated
const _: () = {
trait Assertion {
const TEST: ();
}
impl Assertion for Example {
const TEST: () = assert!(true); // try changing to false
}
};
fn main() {
// A::TEST // compile error
} should probably make sure that this still works with generic impls, though. |
so your example here made me realize that the code as it currently is, doesn't actually panic when the bitsizes differ. use bilge::prelude::*;
#[bitsize(2)]
struct Foo(u1);
// won't panic at compile time
// fn main() { }
// panics
fn main() {
let _ = Foo::_BITSIZE_CHECK;
}
you're absolutely right. I think this is a good idea: quote! {
#item
const _: () = {
// doesn't seem like the trait name needs to be mangled here:
// both the impl block and the assert, appear to resolve `Assertion` to the trait defined here
trait Assertion {
const SIZE_CHECK: bool;
}
impl #impl_generics Assertion for #ident #ty_generics #where_clause {
const SIZE_CHECK: bool = (#computed_bitsize) == (#declared_bitsize);
}
assert!(<something as Assertion>::SIZE_CHECK, "insert message here")
};
} ...except, what do we put instead of |
I did some experimentation with that -- indeed, we can't get evaluation without an access, but we can embed an access into relevant public methods. This would mean that we'd need to also generate the impl blocks that we want to prevent the user from calling inside the anonymous block, instead of outside. The modular nature of bilge will mean that we can't necessarily block all methods, unless the other implementations go through methods that we have blocked using this method. I.e., using the formulation of the trait I specified it'd be impl<T> Example<T> {
pub fn something_public() -> Self {
Self::TEST;
todo!();
}
} which will fail to compile in the case where: the assert would fail, and the user makes a call to something_public. (apologies for the close/reopen, on my phone and had an accidental tap while writing this message) |
hm. Am I reading this correctly in that |
50738a5
to
dd67083
Compare
Still needs some methods to explicitly reference the constant in order to produce a compile error;
This allows me to make accesses to the consts on the Bitsize trait error. We could nix the Assertion trait here and just use the check expression directly.
Alright, I have a possible solution to the compile time error thing, and that's to make the consts on Bitsized rely on the assertion. This means that using any method that uses either const on the Bitsized trait will produce a compiler error if the sizes mismatch. This required moving the assertion generation into bitsize_internal. |
hm, might still need to add some usages of the consts from Bitsized into some of the other trait methods -- but at least we're not exposing an extra name for that i guess. |
impl #impl_generics ::bilge::Bitsized for #name #ty_generics #where_clause { | ||
type ArbitraryInt = #arb_int; | ||
const BITS: usize = (<Self::ArbitraryInt as Bitsized>::BITS, <Self as Assertion>::SIZE_CHECK).0; | ||
const MAX: Self::ArbitraryInt = (<Self::ArbitraryInt as Bitsized>::MAX, <Self as Assertion>::SIZE_CHECK).0; | ||
} | ||
}; |
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.
that's clever! congratulations on finding this workaround!
nice work. |
Great work! If you don't mind, could you add an example with your real usecase? |
Hi all, thanks for your work on this crate :) I've been looking for a bitfield implementation that would support the following use-case: // examples/generic_wrapper.rs
use bilge::prelude::*;
/// Prefix `T` with an [ABA version counter][aba] for use in a lock-free data structure.
///
/// [aba]: https://en.wikipedia.org/wiki/ABA_problem
#[bitsize(64)]
#[derive(DebugBits, FromBits)]
struct Versioned<T> {
version: u16,
inner: T,
}
#[bitsize(48)]
#[derive(DebugBits, FromBits)]
struct Data {
a: u32,
b: u16,
}
fn main() {
let mut versioned = Versioned::<Data>::from(0x0123_4567_89ab_cdefu64);
println!("{versioned:?}");
println!("{:#x?}", versioned.version());
println!("{:#x?}", versioned.inner());
println!("{:#x?}", versioned.value);
versioned.set_version(27u16);
println!("{:#x?}", versioned.version());
println!("{:#x?}", versioned.inner());
println!("{versioned:?}");
versioned.set_inner(Data::from(u48::from(0xfedc_ba98_7654u64)));
println!("{:#x?}", versioned.inner().a());
println!("{:#x?}", versioned.inner().b());
println!("{:#x?}", versioned.inner().value);
println!("{versioned:?}");
} If I understand this PR correctly, it seems like the generic types aren't actually used during (de-)serialization? I'm wondering if it would make sense to instead:
I can try to implement a proof of concept when I get the chance. |
@nwtnni This PR should be able to enable use-cases like that, yes, with the use of additional annotations. i.e.: #[bitsize(64)]
#[derive(DebugBits, FromBits)]
struct Versioned<T: Bitsized<ArbitraryInt = u48> + Filled + From<u48>>
where
u48: From<T>,
{
version: u16,
inner: T,
} This, along with the substitution of |
@kitlith with substitution, do you mean in the macro code? Honestly I'm sorry, I don't have any time to spend on this right now / it's too complicated to wrap my head around when I'm not working with bitfields anymore at this time. I can do some simple checks and publish this PR though, since we are not at a 1.0 version yet. I don't remember which parts I wanted to check more. TLDR; I can't keep my own standards right now, but that's not too bad in this case. |
Extremely understandable. I'm in a similar boat, in that I'm not currently actively working with bitfields, and have not had the energy to spend time on things like this outside of work for awhile.
Was referring to the example code they posted, because I didn't want to copy-paste the whole thing. They wrote
I'm somewhat hesitant to advocate for that, because one of your stated issues is that it's too complicated for the amount of time you can dedicate to this right now, and I don't want this to cause issues for you down the line. If I have the time/energy, I might try to look for anything "magic" and document it better/wrap it up in a better abstraction. The cleverness that pickx commented on, for instance. Can't guarantee anything though, as right now I'm mainly just trying to take time for myself. |
Still a work in progress, this mostly consists of threading generics through to the right place in each relevant macro (and i have several more macros to go), and generating the relevant PhantomData field to allow the generics to stick around.
I'm a little nervous about my potential fix for the bitsize check -- all I know so far is that it still allows it to compile, but I haven't checked if it still functions as intended yet.