diff --git a/derives/Cargo.toml b/derives/Cargo.toml index fdac7867e..14c30588b 100644 --- a/derives/Cargo.toml +++ b/derives/Cargo.toml @@ -16,8 +16,9 @@ travis-ci = { repository = "lloydmeta/frunk" } proc-macro = true [dependencies] -syn = "2" +syn = { version = "2", features = ["full"] } quote = "1" +proc-macro2 = "1.0.66" [dependencies.frunk_proc_macro_helpers] path = "../proc-macro-helpers" diff --git a/derives/src/lib.rs b/derives/src/lib.rs index 02e78c2fd..e1149f089 100644 --- a/derives/src/lib.rs +++ b/derives/src/lib.rs @@ -1,5 +1,6 @@ #![recursion_limit = "128"] #![doc(html_playground_url = "https://play.rust-lang.org/")] +#![cfg_attr(feature = "nightly", feature(proc_macro_diagnostic))] //! Frunk Derives //! //! This library holds logic for the nice custom derives in Frunk. @@ -23,6 +24,8 @@ use crate::derive_generic::impl_generic; mod derive_labelled_generic; use crate::derive_labelled_generic::impl_labelled_generic; +mod list_builder; + use quote::ToTokens; /// Derives a Generic instance based on HList for @@ -51,3 +54,41 @@ pub fn labelled_generic(input: TokenStream) -> TokenStream { // Return the generated impl gen.into_token_stream().into() } + +/// Constructs a struct using an `HList`. +/// +/// This trait allows you to create an instance of a struct from an HList. You keep the +/// remaining items in the `HList` after `DerivedListBuild::hl_new(...)` +/// +/// # Examples +/// +/// ```ignore +/// use frunk::hlist::HList; +/// use frunk::hlist; +/// +/// #[derive(Debug, Eq, PartialEq, frunk::hlist::ListBuild)] +/// struct ListConstructed { +/// field0: bool, +/// field1: u8, +/// #[list_build_ignore] +/// fn_built: &'static str, +/// } +/// +/// // Create a struct manually for comparison +/// let manually_made = ListConstructed { +/// field0: true, +/// field1: 3, +/// fn_built: "passed_in", +/// }; +/// +/// // Use `hl_new` to construct a struct and remaining HList +/// let (built, list): (ListConstructed, HList!(u32)) = +/// ListConstructed::hl_new(hlist![true, 3u8, 42u32], "passed_in"); +/// +/// assert_eq!(built, manually_made); +/// assert_eq!(list, hlist![42u32]); +/// ``` +#[proc_macro_derive(ListBuild, attributes(list_build_ignore, plucker))] +pub fn list_build(item: TokenStream) -> TokenStream { + list_builder::list_build_inner(item) +} diff --git a/derives/src/list_builder.rs b/derives/src/list_builder.rs new file mode 100644 index 000000000..e25025036 --- /dev/null +++ b/derives/src/list_builder.rs @@ -0,0 +1,168 @@ +extern crate proc_macro; +use proc_macro::TokenStream; +use proc_macro2::Span; +use quote::quote; +#[cfg(feature = "nightly")] +use syn::spanned::Spanned; +use syn::{ + parse_macro_input, punctuated::Punctuated, token::Comma, Block, DeriveInput, GenericParam, + Generics, Stmt, +}; + +mod type_helpers; +use type_helpers::{Annotation, ArgPair, PredicateVec, WhereLine}; + +use self::type_helpers::AnnoErr; + +pub fn list_build_inner(item: TokenStream) -> TokenStream { + let input = parse_macro_input!(item as DeriveInput); + // get the fields: the list-built fields, and the manually-built fields + let (input, list_built_fields, ignored_fields) = parse_fields(input); + + let block = gen_stmts(&list_built_fields, &ignored_fields); + + // hl_new args include the injected list, and values for the non-list built args + let args = ArgPair::make_args(ignored_fields); + + if list_built_fields.len() == 0 { + panic!("redundant builder annotations"); + } + let types = list_built_fields + .iter() + .map(|(ArgPair { tp, .. }, _)| tp.clone()) + .collect::>(); + + // make all where-clauses + let lines: Vec = WhereLine::gen_lines_top(&types); + + // Take the last clause and absorb that to make the ret-val + let ret = WhereLine::absorb(lines.last().expect("last line").clone()); + + let output = quote! { -> (Self, #ret) }; + let where_clause: syn::WhereClause = PredicateVec::from(lines.clone()).into(); + let gens = make_generic_params(list_built_fields.len()); + let struct_ident = input.clone().ident; + let fun = quote! { + fn hl_new<#gens>(#(#args),*) #output + #where_clause + { + #block + } + }; + + // et, voile! en a des code magnifique! + quote! { + impl #struct_ident { + #fun + } + } + .into() +} +/// collects the field name/type pairs, splitting them according to fields being built by the list +/// or as args passed into the constructor +fn parse_fields( + mut input: DeriveInput, +) -> (DeriveInput, Vec<(ArgPair, Option)>, Vec) { + let mut list_built = Vec::new(); + let mut ignored_fields = Vec::new(); + + if let syn::Data::Struct(syn::DataStruct { + fields: syn::Fields::Named(named), + .. + }) = &mut input.data + { + for field in &named.named { + // ignored and pluck-type are mutually exclusive... + match Annotation::try_from(&field.attrs[..]) { + Ok(Annotation::Plucker { ty, map }) => list_built.push(( + (field.ident.clone().expect("field_ident"), ty).into(), + Some(map), + )), + Ok(Annotation::Ignore) => ignored_fields + .push((field.ident.clone().expect("field_ident"), field.ty.clone()).into()), + Err(AnnoErr::NoMatch) => todo!(), + Err(AnnoErr::XOR) => { + #[cfg(feature = "nightly")] + field + .span() + .unwrap() + .error("Redundant pluck-type on ignored field") + .emit(); + #[cfg(not(feature = "nightly"))] + panic!("don't ignore fields with pluckers"); + } + }; + } + } + (input, list_built, ignored_fields) +} + +// `` for the `fn hl_new` +fn make_generic_params(count: usize) -> Punctuated { + let gens: String = (0..count + 1) + .map(|i| format!("L{}", i)) + .collect::>() + .join(", "); + let gens = format!("<{}>", gens); + syn::parse_str::(&gens) + .expect("parsing the make_generic_params") + .params +} + +// generates a line of code for each field that needs a value plucked out of the constructor list. +// ```ignore +// let (list_built0, l1) = frunk::hlist::Plucker::pluck(l0); +// ``` +// ...and for the fileds ignored, just moves from the function argument to the rusulting structs +// field +fn gen_stmts(fields: &[(ArgPair, Option)], args: &[ArgPair]) -> Block { + let mut list_n = 0; + let mut stmts: Vec = vec![]; + // Generate the "let (field, lX) = lY.pluck();" statements + for (arg_pair, expr) in fields { + let next_list = list_n + 1; + let next_list = syn::Ident::new(&format!("l{}", next_list), Span::call_site()); + let list_n_tok = syn::Ident::new(&format!("l{}", list_n), Span::call_site()); + let field_name = &arg_pair.ident; + let plucking = quote! { + let (#field_name, #next_list) = frunk::hlist::Plucker::pluck(#list_n_tok); + }; + + let stmt: Stmt = syn::parse2(plucking.clone()) + .unwrap_or_else(|_| panic!("Failed to parse statement: {}", plucking.to_string())); + + stmts.push(stmt); + if let Some(expr) = expr { + let mapping = quote! { + let #field_name = #expr; + }; + stmts.push(syn::parse2(mapping).unwrap()); + }; + list_n += 1; + } + + // Generate the "Self { fields... }" part of the block + let args = args + .iter() + .map(|ArgPair { ident, .. }| ident.clone()) + .collect::>(); + let all_fields = [ + &fields + .iter() + .map(|(field, _)| field.ident.clone()) + .collect::>()[..], + &args[..], + ] + .concat(); + let list_n_ident = syn::Ident::new(&format!("l{}", list_n), proc_macro2::Span::call_site()); + let self_stmt: Stmt = syn::parse2(quote! { + return (Self { #(#all_fields,)* }, #list_n_ident); + }) + .expect("generating the Self..."); + stmts.push(self_stmt); + + Block { + stmts, + brace_token: Default::default(), + } +} diff --git a/derives/src/list_builder/type_helpers.rs b/derives/src/list_builder/type_helpers.rs new file mode 100644 index 000000000..957a02f1d --- /dev/null +++ b/derives/src/list_builder/type_helpers.rs @@ -0,0 +1,8 @@ +mod arg_pair; +pub(crate) use arg_pair::*; +mod where_line; +pub(crate) use where_line::*; +mod pluck_param; +pub(crate) use pluck_param::*; +mod annotations; +pub(crate) use annotations::*; diff --git a/derives/src/list_builder/type_helpers/annotations.rs b/derives/src/list_builder/type_helpers/annotations.rs new file mode 100644 index 000000000..ae15aa817 --- /dev/null +++ b/derives/src/list_builder/type_helpers/annotations.rs @@ -0,0 +1,59 @@ +pub(crate) enum Annotation { + Plucker { ty: syn::Type, map: syn::Expr }, + Ignore, +} + +pub(crate) enum AnnoErr { + XOR, + NoMatch, +} +impl core::convert::TryFrom<&[syn::Attribute]> for Annotation { + type Error = AnnoErr; + fn try_from(attrs: &[syn::Attribute]) -> Result { + let mut annos = attrs.iter().filter_map(|attr| Self::try_from(attr).ok()); + let Some(fst) = annos.next() else { + return Err(AnnoErr::NoMatch); + }; + + if let Some(_) = annos.next() { + return Err(AnnoErr::XOR); + } + Ok(fst) + } +} +impl TryFrom<&syn::Attribute> for Annotation { + type Error = (); + + fn try_from(attr: &syn::Attribute) -> Result { + match attr.path().get_ident().map(|id| quote! {#id}.to_string()) { + Some(ref s) if s == "plucker" => { + let arg_parser = |input: syn::parse::ParseStream| { + // Eg, with the arg: u8, map=core::convert::From::from(arg_name) + + // consume leading type + let ty: syn::Type = input.parse().expect("type here"); + // , map=core::convert::From::from(arg_name) + // consume comma + let _ = input.parse::>().expect("comma here"); + // map=core::convert::From::from(arg_name) + // consume `map` + let _ = input.parse::().expect("'map' here"); + // =core::convert::From::from(arg_name) + // consume `=` + input.parse::().expect("equalse sign here"); + // core::convert::From::from(arg_name) + // parse in the expression + let mapping = input.parse().expect("parsing expression"); + Ok((ty, mapping)) + }; + + let (ty, map) = attr + .parse_args_with(arg_parser) + .expect("mapping with arg parser"); + Ok(Annotation::Plucker { ty, map }) + } + Some(ref s) if s == "list_build_ignore" => Ok(Self::Ignore), + _ => Err(()), + } + } +} diff --git a/derives/src/list_builder/type_helpers/arg_pair.rs b/derives/src/list_builder/type_helpers/arg_pair.rs new file mode 100644 index 000000000..039d7ce55 --- /dev/null +++ b/derives/src/list_builder/type_helpers/arg_pair.rs @@ -0,0 +1,46 @@ +// struct field or function argument +pub(crate) struct ArgPair { + pub(crate) ident: syn::Ident, + pub(crate) tp: syn::Type, +} + +impl ArgPair { + /// ```ignore + /// #[derive(ListBuild)] + /// struct Foo { + /// foo: u8, + /// #[list_build_ignore] + /// bar: u16 + /// } + /// // -> fn(l0: HList!(u8), bar: u16) + pub(crate) fn make_args(fields: Vec) -> impl Iterator { + std::iter::once(syn::parse2(quote! {l0: L0}).unwrap()) + .chain(fields.into_iter().map(syn::FnArg::from)) + } +} +impl From<(syn::Ident, syn::Type)> for ArgPair { + fn from(value: (syn::Ident, syn::Type)) -> Self { + Self { + ident: value.0, + tp: value.1, + } + } +} +impl From for syn::FnArg { + fn from(value: ArgPair) -> Self { + syn::FnArg::Typed(syn::PatType { + attrs: Vec::new(), + pat: Box::new(syn::Pat::Ident(syn::PatIdent { + attrs: Vec::new(), + by_ref: None, + mutability: None, + ident: value.ident.clone(), + subpat: None, + })), + colon_token: syn::token::Colon { + spans: [proc_macro2::Span::call_site()], + }, + ty: Box::new(value.tp.clone()), + }) + } +} diff --git a/derives/src/list_builder/type_helpers/pluck_param.rs b/derives/src/list_builder/type_helpers/pluck_param.rs new file mode 100644 index 000000000..10dee5980 --- /dev/null +++ b/derives/src/list_builder/type_helpers/pluck_param.rs @@ -0,0 +1,12 @@ +use proc_macro2::Span; + +/// Shim to allow a type-conversions fom a type + LN pair into a paramater bound +#[derive(Clone)] +pub(crate) struct PluckParam(pub(crate) syn::TypeParamBound); +impl From<(syn::Type, u8)> for PluckParam { + fn from((ty, n): (syn::Type, u8)) -> Self { + // Create an ident for "LN" where N is the u8 value + let l_ident = syn::Ident::new(&format!("L{}", n), Span::call_site()); + return Self(syn::parse2(quote! {frunk::hlist::Plucker<#ty, #l_ident>}).unwrap()); + } +} diff --git a/derives/src/list_builder/type_helpers/where_line.rs b/derives/src/list_builder/type_helpers/where_line.rs new file mode 100644 index 000000000..44ecf30d6 --- /dev/null +++ b/derives/src/list_builder/type_helpers/where_line.rs @@ -0,0 +1,98 @@ +use super::PluckParam; + +#[derive(Clone)] +pub(crate) struct WhereLine { + tp: syn::Type, + pred: PluckParam, +} + +impl WhereLine { + /// entry into the recursive build-up of the where-lines + /// + /// generates the base-line (L0: Plucker) then goes into the recursive call + pub(crate) fn gen_lines_top(types: &[syn::Type]) -> Vec { + if types.len() == 0 { + panic!("no pluckings happening"); + } + let base = Self::gen_base(&types[0]); + if types.len() == 1 { + return vec![base]; + } + Self::gen_lines_recur(vec![base], &types[1..]) + } + + /// Beginning with the base line, it generates a new where-line for each type. + /// Each line is an "absorbed" version of the previous line, with `Plucker` as + /// the new trait-impl requirement. + /// + /// ```ignore + /// LN: Plucker // is saved, the next being... + /// >::Remainder: Plucker + /// ``` + fn gen_lines_recur(mut acc: Vec, types: &[syn::Type]) -> Vec { + // use the previous predicate to make the new type + let tp = acc + .last() + .cloned() + .expect("should never recurse without the base...") + .absorb(); + + let pred = PluckParam::from((types[0].clone(), acc.len() as u8 + 1)); + acc.push(Self { tp, pred }); + if types.len() == 1 { + return acc; + } + Self::gen_lines_recur(acc, &types[1..]) + } + + /// L0: Plucker + fn gen_base(tp: &syn::Type) -> Self { + let pred = + syn::parse2(quote! {frunk::hlist::Plucker<#tp, L1>}).expect("quote the base plucker"); + // Create the WhereLine + WhereLine { + tp: syn::parse2(quote! {L0}).expect("quote the L0"), + pred: PluckParam(pred), + } + } + + /// Does the "absorption" needed for the next line for each gen_lines_recur + /// `Tn: Pn` -> `::Remainder` + pub(crate) fn absorb(self) -> syn::Type { + let WhereLine { tp, pred } = self; + let pred = pred.0; + + let res = quote! {<#tp as #pred>::Remainder}; + syn::parse2(res).expect("absorbing") + } +} + +impl From for syn::WherePredicate { + fn from(line: WhereLine) -> Self { + let WhereLine { tp, pred } = line; + let bound: syn::TypeParamBound = pred.0.into(); + let predicate = quote! { #tp: #bound }; + syn::parse2(predicate).expect("whereline to pred") + } +} + +/// shim allowing PredicateVec::from(line_vec).into() where a `syn::WhereClause` is needed +pub(crate) struct PredicateVec { + pub(crate) preds: Vec, +} + +impl From> for PredicateVec { + fn from(value: Vec) -> Self { + Self { + preds: value.into_iter().map(syn::WherePredicate::from).collect(), + } + } +} +impl From for syn::WhereClause { + fn from(value: PredicateVec) -> Self { + Self { + where_token: syn::Token![where](proc_macro2::Span::call_site()), + predicates: syn::punctuated::Punctuated::from_iter(value.preds), + } + } +}