flense-derive 0.4.1

Derive macros for flense `Adapter`/`Field` implementations
Documentation
//! Derive macros for [`flense`](https://docs.rs/flense).
//!
//! This crate provides [`macro@Adapter`], a derive that generates the `unsafe
//! impl Adapter<...>` blocks `flense` needs, with the byte offset filled in
//! correctly from [`offset_of!`](core::mem::offset_of).
//!
//! Use of this derive is never required; you could directly write these
//! implementations yourself, but the derive lets you avoid explicitly writing
//! the unsafe block and performs some safety checks.

use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{
    Data,
    DeriveInput,
    Fields,
    Path,
    Token,
    Type,
    Visibility,
    parse::{
        Parse,
        ParseStream,
    },
    parse_macro_input,
    punctuated::Punctuated,
    spanned::Spanned as _,
    token,
};

/// Derives `flense`'s `Adapter` implementations for the annotated fields of a
/// struct.
///
/// Annotate each field that should be lensable with `#[field(...)]`. Each entry
/// is either:
///
/// * `Marker` - reference an existing `Field` implementation. The macro emits
///   an `unsafe impl Adapter<Marker>` for the struct and a compile-time check
///   that the field's type is exactly `<Marker as Field>::Type`.
/// * `new Marker` - also mints a fresh marker type `Marker` (inheriting the
///   struct's visibility) whose `Field::Type` is the field's declared type.
/// * `new reflexive Marker` - as `new Marker`, and additionally emits an
///   `Adapter<Marker>` impl for the field type itself (`<Marker as
///   Field>::Type` at offset zero), so a bare value of that type can be lensed
///   as `Marker`. You could write this `unsafe impl` yourself; letting the
///   derive emit it avoids needing to author and maintain a `unsafe` block.
///
/// A single `#[field(...)]` may list several comma-separated entries, and the
/// attribute may be repeated on one field.
///
/// ```
/// use flense::prelude::*;
/// # use flense_derive::Adapter;
///
/// enum Position {}
/// impl Field for Position {
///     type Type = [f32; 3];
/// }
///
/// #[derive(Adapter)]
/// struct Vertex {
///     #[field(Position)]
///     pos: [f32; 3],
///     #[field(new Color)]
///     color: [u8; 3],
/// }
///
/// #[derive(Adapter)]
/// struct Mesh {
///     #[field(Position)]
///     world_pos: [f32; 3],
///     #[field(Color)]
///     color: [u8; 3],
/// }
/// ```
#[proc_macro_derive(Adapter, attributes(field))]
pub fn derive_adapter(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    expand(&input)
        .unwrap_or_else(syn::Error::into_compile_error)
        .into()
}

/// A single entry inside a `#[field(...)]` attribute: optional `new` and
/// `reflexive` keywords followed by the marker path.
struct FieldEntry {
    is_new: bool,
    is_reflexive: bool,
    path: Path,
}

/// Consume a contextual keyword if it leads `input`.
///
/// These keywords (`new`, `reflexive`) are not Rust keywords, so treat a
/// leading ident as the keyword only when it is followed by another path (so a
/// marker literally named `new`, or a path like `new::Marker`, still parses as
/// a plain reference). Returns whether the keyword was present and consumed.
fn eat_keyword(input: ParseStream, keyword: &str) -> syn::Result<bool> {
    let is_kw = input.peek(syn::Ident) && !input.peek2(Token![::]) && {
        let fork = input.fork();
        let ident: syn::Ident = fork.parse()?;
        ident == keyword && !fork.is_empty() && !fork.peek(Token![,])
    };
    if is_kw {
        let _: syn::Ident = input.parse()?;
    }
    Ok(is_kw)
}

impl Parse for FieldEntry {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let is_new = eat_keyword(input, "new")?;
        let is_reflexive = eat_keyword(input, "reflexive")?;
        let path: Path = input.parse()?;
        Ok(Self {
            is_new,
            is_reflexive,
            path,
        })
    }
}

fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
    let Data::Struct(data) = &input.data else {
        return Err(syn::Error::new(
            input.span(),
            "`Adapter` can only be derived for structs",
        ));
    };
    let Fields::Named(fields) = &data.fields else {
        return Err(syn::Error::new(
            data.fields.span(),
            "`Adapter` can only be derived for structs with named fields",
        ));
    };

    // `#[repr(packed)]` fields may be under-aligned. Referencing an under-aligned
    // field is unsound, so a sound `Adapter` is impossible. Reject it.
    for attr in &input.attrs {
        if !attr.path().is_ident("repr") {
            continue;
        }
        let mut packed = false;
        attr.parse_nested_meta(|meta| {
            if meta.path.is_ident("packed") {
                packed = true;
            }
            // Consume an optional `(N)` argument (e.g. `packed(2)`, `align(8)`)
            // so the nested-meta parse does not error.
            if meta.input.peek(token::Paren) {
                let content;
                syn::parenthesized!(content in meta.input);
                let _: TokenStream2 = content.parse()?;
            }
            Ok(())
        })?;
        if packed {
            return Err(syn::Error::new(
                attr.span(),
                "`Adapter` cannot be derived for `#[repr(packed)]` structs: \
                 packed fields may be under-aligned, which would make the \
                 generated `Adapter` unsound",
            ));
        }
    }

    let struct_name = &input.ident;
    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();

    let mut out = TokenStream2::new();

    for field in &fields.named {
        let field_name = field.ident.as_ref().expect("named field has an ident");
        let field_ty = &field.ty;

        for attr in &field.attrs {
            if !attr.path().is_ident("field") {
                continue;
            }
            let entries =
                attr.parse_args_with(Punctuated::<FieldEntry, Token![,]>::parse_terminated)?;
            for entry in entries {
                out.extend(emit_entry(
                    &entry,
                    &input.vis,
                    struct_name,
                    &impl_generics,
                    &ty_generics,
                    where_clause,
                    field_name,
                    field_ty,
                )?);
            }
        }
    }

    Ok(out)
}

#[allow(clippy::too_many_arguments, reason = "splicing several syn fragments")]
fn emit_entry(
    entry: &FieldEntry,
    vis: &Visibility,
    struct_name: &syn::Ident,
    impl_generics: &syn::ImplGenerics,
    ty_generics: &syn::TypeGenerics,
    where_clause: Option<&syn::WhereClause>,
    field_name: &syn::Ident,
    field_ty: &Type,
) -> syn::Result<TokenStream2> {
    let marker = &entry.path;

    // For `new Marker`, define the marker type and its `Field` impl. The marker
    // must be a single identifier, since it is declared as a new item.
    let new_marker = if entry.is_new {
        let Some(ident) = marker.get_ident() else {
            return Err(syn::Error::new(
                marker.span(),
                "`new` markers must be a single identifier, not a path",
            ));
        };
        quote! {
            #vis enum #ident {}
            #[automatically_derived]
            impl ::flense::Field for #ident {
                type Type = #field_ty;
            }
        }
    } else {
        TokenStream2::new()
    };

    // For a `reflexive` marker, also emit an `Adapter` impl for the field type
    // itself, treating the whole value as the field at offset zero. This lets a
    // bare `<Marker as Field>::Type` (e.g. `[f32; 3]`) be lensed as `Marker`
    // directly, not just the struct it was declared in.
    let reflexive_impl = if entry.is_reflexive {
        quote! {
            // SAFETY: `Self` *is* `<#marker as Field>::Type` (which is
            // `#field_ty` for this `new` marker), so the field sits at offset
            // zero and spans the whole value; the offset is trivially aligned
            // because the value is already a valid, aligned instance of its own
            // type.
            //
            // The self type is spelled as the concrete `#field_ty`, not the
            // projection `<#marker as Field>::Type`: a projection self-type is
            // not normalized by downstream coherence checks, so it would
            // spuriously overlap any other `Adapter<#marker>` impl.
            #[automatically_derived]
            unsafe impl ::flense::Adapter<#marker> for #field_ty {
                const OFFSET: usize = 0;
            }
        }
    } else {
        TokenStream2::new()
    };

    // For an existing marker, guard that the field's type is exactly `<Marker as
    // Field>::Type`. `offset_of!` alone does not check this, and a mismatch would
    // be unsound, effectively allowing a silent transmute. The check lives inside
    // the `OFFSET` const so the struct's generics and `Self` are in scope. `T:
    // SameType<U>` holds only when `T == U`. The `new` case needs no guard: its
    // `Type` is defined to be the field type.
    let type_guard = if entry.is_new {
        TokenStream2::new()
    } else {
        quote! {
            trait SameType<T> {}
            impl<T> SameType<T> for T {}
            fn assert_field_type<FLENSE_DERIVE_T: SameType<<#marker as ::flense::Field>::Type>>() {}
            let _ = assert_field_type::<#field_ty>;
        }
    };

    Ok(quote! {
        #new_marker

        #reflexive_impl

        // SAFETY: `OFFSET` is produced by `offset_of!` on the annotated field,
        // so it is the field's true byte offset; the field's type is checked to
        // equal `<#marker as Field>::Type` (or defined to be it for `new`
        // markers); and the offset is asserted to be aligned for that type, so
        // the field pointer the lens derives is always well-aligned.
        #[automatically_derived]
        unsafe impl #impl_generics ::flense::Adapter<#marker>
            for #struct_name #ty_generics #where_clause
        {
            const OFFSET: usize = {
                #type_guard
                let offset = ::core::mem::offset_of!(Self, #field_name);
                // Second safeguard (independent of the `#[repr(packed)]`
                // rejection): the field must sit at an offset aligned for its
                // type, otherwise the references a lens hands out would be
                // unaligned, hence unsound.
                ::core::assert!(
                    offset % ::core::mem::align_of::<#field_ty>() == 0,
                    "flense: field is not aligned for its type; the derived `Adapter` would be unsound",
                );
                offset
            };
        }
    })
}