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

Add #[derive(ListBuild)] #223

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion derives/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
41 changes: 41 additions & 0 deletions derives/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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)
}
168 changes: 168 additions & 0 deletions derives/src/list_builder.rs
Original file line number Diff line number Diff line change
@@ -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::<Vec<_>>();

// make all where-clauses
let lines: Vec<WhereLine> = 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<syn::Expr>)>, Vec<ArgPair>) {
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)
}

// `<L0, L1, ..., L(N-1)>` for the `fn hl_new<L1, ...>`
fn make_generic_params(count: usize) -> Punctuated<GenericParam, Comma> {
let gens: String = (0..count + 1)
.map(|i| format!("L{}", i))
.collect::<Vec<String>>()
.join(", ");
let gens = format!("<{}>", gens);
syn::parse_str::<Generics>(&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<syn::Expr>)], args: &[ArgPair]) -> Block {
let mut list_n = 0;
let mut stmts: Vec<Stmt> = 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::<Vec<_>>();
let all_fields = [
&fields
.iter()
.map(|(field, _)| field.ident.clone())
.collect::<Vec<_>>()[..],
&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(),
}
}
8 changes: 8 additions & 0 deletions derives/src/list_builder/type_helpers.rs
Original file line number Diff line number Diff line change
@@ -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::*;
59 changes: 59 additions & 0 deletions derives/src/list_builder/type_helpers/annotations.rs
Original file line number Diff line number Diff line change
@@ -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<Self, Self::Error> {
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<Self, Self::Error> {
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::<Option<syn::Token![,]>>().expect("comma here");
// map=core::convert::From::from(arg_name)
// consume `map`
let _ = input.parse::<syn::Ident>().expect("'map' here");
// =core::convert::From::from(arg_name)
// consume `=`
input.parse::<syn::Token![=]>().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(()),
}
}
}
46 changes: 46 additions & 0 deletions derives/src/list_builder/type_helpers/arg_pair.rs
Original file line number Diff line number Diff line change
@@ -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<ArgPair>) -> impl Iterator<Item = syn::FnArg> {
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<ArgPair> 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()),
})
}
}
12 changes: 12 additions & 0 deletions derives/src/list_builder/type_helpers/pluck_param.rs
Original file line number Diff line number Diff line change
@@ -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());
}
}
Loading