Skip to content
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

Open
wants to merge 16 commits into
base: main
Choose a base branch
from

Conversation

kitlith
Copy link
Contributor

@kitlith kitlith commented Aug 30, 2023

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.

(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.
@kitlith
Copy link
Contributor Author

kitlith commented Aug 30, 2023

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.

@kitlith
Copy link
Contributor Author

kitlith commented Aug 30, 2023

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.
@kitlith kitlith marked this pull request as ready for review September 4, 2023 02:14
bilge-impl/src/bitsize.rs Outdated Show resolved Hide resolved
Copy link
Collaborator

@pickx pickx left a 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.

Copy link
Collaborator

@pickx pickx left a 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.rs Outdated Show resolved Hide resolved
bilge-impl/src/bitsize_internal.rs Outdated Show resolved Hide resolved
bilge-impl/src/debug_bits.rs Outdated Show resolved Hide resolved
/// WARNING: modifying this value directly can break invariants
value: #arb_int,
_phantom: ::core::marker::PhantomData<(#(#phantom),*)>
Copy link
Collaborator

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?

Copy link
Contributor Author

@kitlith kitlith Sep 12, 2023

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.

@kitlith
Copy link
Contributor Author

kitlith commented Sep 8, 2023

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.

@pickx
Copy link
Collaborator

pickx commented Sep 9, 2023

so your example here made me realize that the code as it currently is, doesn't actually panic when the bitsizes differ.
the const isn't evaluated until it's explicitly accessed:

use bilge::prelude::*;

#[bitsize(2)]
struct Foo(u1);

// won't panic at compile time
// fn main() { }

// panics
fn main() {
    let _ = Foo::_BITSIZE_CHECK;
}

RE: the implementation detail const: we can generate an un-namable trait that has an associated const, and implement it.

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 something in the case where the struct has generics? how do we name this?
and again, even if the const assert is moved to the trait itself like in your example (which solves the naming problem), it won't automatically evaluate. we would then need to be able to name the const assert itself, from outside the trait impl block.

@kitlith
Copy link
Contributor Author

kitlith commented Sep 9, 2023

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)

@kitlith kitlith closed this Sep 9, 2023
@kitlith kitlith reopened this Sep 9, 2023
@kitlith
Copy link
Contributor Author

kitlith commented Sep 12, 2023

hm. Am I reading this correctly in that bitsize.rs only handles the assert, while bitsize_internal.rs handles everything else? That messes with the above idea a little bit.

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.
@kitlith
Copy link
Contributor Author

kitlith commented Sep 13, 2023

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.

@kitlith kitlith requested a review from pickx September 13, 2023 18:19
@kitlith
Copy link
Contributor Author

kitlith commented Sep 15, 2023

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.

Comment on lines +301 to +306
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;
}
};
Copy link
Collaborator

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!

@pickx
Copy link
Collaborator

pickx commented Sep 18, 2023

nice work.
I'd like to wait for @hecatia-elegua to take a look at this and give his opinion before we proceed. 👍

@hecatia-elegua
Copy link
Owner

Great work!

If you don't mind, could you add an example with your real usecase?
I would also have to experiment more on my own, when I get the time.

@nwtnni
Copy link

nwtnni commented Jun 6, 2024

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:

  • Assume generics will be (de-)serialized by default.
  • Introduce the equivalent of serde's skip, skip_serializing, and skip_deserializing attributes to control this behavior.

I can try to implement a proof of concept when I get the chance.

@kitlith
Copy link
Contributor Author

kitlith commented Jun 7, 2024

@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 u48::new in place of u48::from gets your example to run.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants