frunk_proc_macro_helpers/
lib.rs

1#![doc(html_playground_url = "https://play.rust-lang.org/")]
2//! Frunk Proc Macro internals
3//!
4//! This library holds common logic for procedural macros used by frunk
5//!
6//! Links:
7//!   1. [Source on Github](https://github.com/lloydmeta/frunk)
8//!   2. [Crates.io page](https://crates.io/crates/frunk)
9
10extern crate frunk_core;
11extern crate proc_macro;
12extern crate proc_macro2;
13
14#[macro_use]
15extern crate quote;
16extern crate syn;
17
18use proc_macro::TokenStream;
19use proc_macro2::{Span, TokenStream as TokenStream2};
20use quote::ToTokens;
21use syn::spanned::Spanned;
22use syn::{
23    DeriveInput, Expr, Field, Fields, GenericParam, Generics, Ident, Lifetime, LifetimeParam,
24    Member, Variant,
25};
26
27/// These are assumed to exist as enums in frunk_core::labelled
28const ALPHA_CHARS: &[char] = &[
29    'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's',
30    't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
31    'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
32];
33
34/// These are assumed to exist as enums in frunk_core::labelled as underscore prepended enums
35const UNDERSCORE_CHARS: &[char] = &['_', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
36
37/// Parses a TokenStream (usually received as input into a
38/// custom derive function), into a syn MacroInput AST,
39/// which is nice.
40pub fn to_ast(input: TokenStream) -> DeriveInput {
41    // Parse the string representation
42    syn::parse(input).unwrap()
43}
44
45/// Returns an Ident
46pub fn call_site_ident(s: &str) -> Ident {
47    Ident::new(s, Span::call_site())
48}
49
50/// Given a list of types, creates an AST for the corresponding HList
51/// type.
52pub fn build_hlist_type<L: IntoIterator>(items: L) -> TokenStream2
53where
54    L::Item: ToTokens,
55    L::IntoIter: DoubleEndedIterator,
56{
57    let mut result = quote! { ::frunk_core::hlist::HNil };
58    for item in items.into_iter().rev() {
59        result = quote! { ::frunk_core::hlist::HCons<#item, #result> }
60    }
61    result
62}
63
64/// Given a list of expressions or patterns, creates an AST for the corresponding HList
65/// constructor, which may itself be used as an expression or a pattern.
66pub fn build_hlist_constr<L: IntoIterator>(items: L) -> TokenStream2
67where
68    L::Item: ToTokens,
69    L::IntoIter: DoubleEndedIterator,
70{
71    let mut result = quote! { ::frunk_core::hlist::HNil };
72    for item in items.into_iter().rev() {
73        result = quote! { ::frunk_core::hlist::HCons { head: #item, tail: #result }}
74    }
75    result
76}
77
78/// Given a list of types, creates an AST for the corresponding Coproduct
79/// type.
80pub fn build_coprod_type<L: IntoIterator>(items: L) -> TokenStream2
81where
82    L::Item: ToTokens,
83    L::IntoIter: DoubleEndedIterator,
84{
85    let mut result = quote! { ::frunk_core::coproduct::CNil };
86    for item in items.into_iter().rev() {
87        result = quote! { ::frunk_core::coproduct::Coproduct<#item, #result> }
88    }
89    result
90}
91
92/// Given an index and an expression or pattern, creates an AST for the corresponding Coproduct
93/// constructor, which may itself be used as an expression or a pattern.
94pub fn build_coprod_constr(index: usize, item: impl ToTokens) -> TokenStream2 {
95    let mut result = quote! { ::frunk_core::coproduct::Coproduct::Inl(#item) };
96    for _ in 0..index {
97        result = quote! { ::frunk_core::coproduct::Coproduct::Inr(#result) }
98    }
99    result
100}
101
102/// Given the length of a Coproduct type, generates an "unreachable" match arm, matching
103/// the CNil case in order to work around limitations in the compiler's exhaustiveness
104/// checking.
105pub fn build_coprod_unreachable_arm(length: usize, deref: bool) -> TokenStream2 {
106    let mut result = quote! { _frunk_unreachable_ };
107    for _ in 0..length {
108        result = quote! { ::frunk_core::coproduct::Coproduct::Inr(#result)}
109    }
110    if deref {
111        quote! { #result => match *_frunk_unreachable_ {} }
112    } else {
113        quote! { #result => match _frunk_unreachable_ {} }
114    }
115}
116
117pub fn build_field_type(name: &Ident, inner_type: impl ToTokens) -> TokenStream2 {
118    let label_type = build_label_type(name);
119    quote! { ::frunk_core::labelled::Field<#label_type, #inner_type> }
120}
121pub fn build_field_expr(name: &Ident, inner_expr: impl ToTokens) -> TokenStream2 {
122    let label_type = build_label_type(name);
123    let literal_name = name.to_string();
124    quote! { ::frunk_core::labelled::field_with_name::<#label_type, _>(#literal_name, #inner_expr) }
125}
126pub fn build_field_pat(inner_pat: impl ToTokens) -> TokenStream2 {
127    quote! { ::frunk_core::labelled::Field { value: #inner_pat, .. } }
128}
129
130/// Given an Ident returns an AST for its type level representation based on the
131/// enums generated in frunk_core::labelled.
132///
133/// For example, given first_name, returns an AST for (f,i,r,s,t,__,n,a,m,e)
134pub fn build_label_type(ident: &Ident) -> impl ToTokens {
135    let as_string = ident.to_string();
136    let name = as_string.as_str();
137    let name_as_idents: Vec<Ident> = name.chars().flat_map(|c| encode_as_ident(&c)).collect();
138    let name_as_tokens: Vec<_> = name_as_idents
139        .iter()
140        .map(|ident| {
141            quote! { ::frunk_core::labelled::chars::#ident }
142        })
143        .collect();
144    quote! { (#(#name_as_tokens),*) }
145}
146
147/// Given a char, encodes it as a vector of Ident
148///
149/// Takes care of checking to see whether the char can be used as is,
150/// or needs to be encoded as an underscored character (_ and numbers),
151/// or needs to be encoded as a unicode.
152///
153/// This method assumes that _uc and uc_ are in frunk_core::labelled as enums
154fn encode_as_ident(c: &char) -> Vec<Ident> {
155    if ALPHA_CHARS.contains(c) {
156        vec![call_site_ident(&c.to_string())]
157    } else if UNDERSCORE_CHARS.contains(c) {
158        vec![call_site_ident(&format!("_{}", c))]
159    } else {
160        // UTF escape and get the hexcode
161        let as_unicode = c.escape_unicode();
162        // as_unicode can be multiple unicode codepoints encoded as \u{2764}\u{fe0f} (❤️)
163        // so we filter on alphanumeric to get just u's as a delimiters along with the
164        // hex portions
165        let delimited_hex = as_unicode.filter(|c| c.is_alphanumeric());
166        let mut hex_idents: Vec<Ident> = delimited_hex.flat_map(|c| encode_as_ident(&c)).collect();
167        // sandwich between _uc and uc_
168        let mut book_ended: Vec<Ident> = vec![call_site_ident("_uc")];
169        book_ended.append(&mut hex_idents);
170        book_ended.push(call_site_ident("uc_"));
171        book_ended
172    }
173}
174
175pub fn build_path_type(path_expr: Expr) -> impl ToTokens {
176    let idents = find_idents_in_expr(path_expr);
177    idents
178        .iter()
179        .map(build_label_type)
180        .fold(quote!(::frunk_core::hlist::HNil), |acc, t| {
181            quote! {
182            ::frunk_core::path::Path<
183                ::frunk_core::hlist::HCons<
184                   #t,
185                   #acc
186                >
187              >
188            }
189        })
190}
191
192/// Returns the idents in a path like expression in reverse
193pub fn find_idents_in_expr(path_expr: Expr) -> Vec<Ident> {
194    fn go(current: Expr, mut v: Vec<Ident>) -> Vec<Ident> {
195        match current {
196            Expr::Field(e) => {
197                let m = e.member;
198                match m {
199                    Member::Named(i) => {
200                        v.push(i);
201                    }
202                    _ => panic!("Only named access is supported"),
203                }
204                go(*e.base, v)
205            }
206            Expr::Path(p) => {
207                if p.path.segments.len() != 1 {
208                    panic!("Invalid name; this has collons in it")
209                } else {
210                    let i = p.path.segments[0].ident.clone();
211                    v.push(i);
212                    v
213                }
214            }
215            _ => panic!("Invalid input"),
216        }
217    }
218    go(path_expr, Vec::new())
219}
220
221pub enum StructType {
222    Named,
223    Tuple,
224    Unit,
225}
226
227pub struct FieldBinding {
228    pub field: Field,
229    pub binding: Ident,
230}
231
232impl FieldBinding {
233    pub fn build_type(&self) -> TokenStream2 {
234        let ty = &self.field.ty;
235        quote! { #ty }
236    }
237    pub fn build_type_ref(&self) -> TokenStream2 {
238        let ty = &self.field.ty;
239        quote! { &'_frunk_ref_ #ty }
240    }
241    pub fn build_type_mut(&self) -> TokenStream2 {
242        let ty = &self.field.ty;
243        quote! { &'_frunk_ref_ mut #ty }
244    }
245    pub fn build(&self) -> TokenStream2 {
246        let binding = &self.binding;
247        quote! { #binding }
248    }
249    pub fn build_pat_ref(&self) -> TokenStream2 {
250        let binding = &self.binding;
251        quote! { ref #binding }
252    }
253    pub fn build_pat_mut(&self) -> TokenStream2 {
254        let binding = &self.binding;
255        quote! { ref mut #binding }
256    }
257    pub fn build_field_type(&self) -> TokenStream2 {
258        build_field_type(&self.binding, self.build_type())
259    }
260    pub fn build_field_type_ref(&self) -> TokenStream2 {
261        build_field_type(&self.binding, self.build_type_ref())
262    }
263    pub fn build_field_type_mut(&self) -> TokenStream2 {
264        build_field_type(&self.binding, self.build_type_mut())
265    }
266    pub fn build_field_expr(&self) -> TokenStream2 {
267        build_field_expr(&self.binding, &self.binding)
268    }
269    pub fn build_field_pat(&self) -> TokenStream2 {
270        build_field_pat(&self.binding)
271    }
272}
273
274/// Represents the binding of a struct or enum variant's fields to a corresponding
275/// set of similarly named local variables.
276pub struct FieldBindings {
277    pub type_: StructType,
278    pub fields: Vec<FieldBinding>,
279}
280
281impl FieldBindings {
282    pub fn new(fields: &Fields) -> Self {
283        Self {
284            type_: match fields {
285                Fields::Named(_) => StructType::Named,
286                Fields::Unnamed(_) => StructType::Tuple,
287                Fields::Unit => StructType::Unit,
288            },
289            fields: fields
290                .iter()
291                .enumerate()
292                .map(|(index, field)| FieldBinding {
293                    field: field.clone(),
294                    binding: field
295                        .ident
296                        .clone()
297                        .unwrap_or_else(|| Ident::new(&format!("_{}", index), field.span())),
298                })
299                .collect(),
300        }
301    }
302
303    /// Builds a type constructor for use with structs or enum variants. Does not include the name
304    /// of the type or variant.
305    pub fn build_type_constr<R: ToTokens>(&self, f: impl Fn(&FieldBinding) -> R) -> TokenStream2 {
306        let bindings: Vec<_> = self.fields.iter().map(f).collect();
307        match self.type_ {
308            StructType::Named => quote! { { #(#bindings,)* } },
309            StructType::Tuple => quote! { ( #(#bindings,)* ) },
310            StructType::Unit => TokenStream2::new(),
311        }
312    }
313
314    pub fn build_hlist_type<R: ToTokens>(&self, f: impl Fn(&FieldBinding) -> R) -> TokenStream2 {
315        build_hlist_type(self.fields.iter().map(f))
316    }
317
318    pub fn build_hlist_constr<R: ToTokens>(&self, f: impl Fn(&FieldBinding) -> R) -> TokenStream2 {
319        build_hlist_constr(self.fields.iter().map(f))
320    }
321}
322
323pub fn ref_generics(generics: &Generics) -> Generics {
324    let mut generics_ref = generics.clone();
325
326    // instantiate a lifetime and lifetime def to add
327    let ref_lifetime = Lifetime::new("'_frunk_ref_", Span::call_site());
328    let ref_lifetime_def = LifetimeParam::new(ref_lifetime.clone());
329
330    // Constrain the generic lifetimes present in the concrete type to the reference lifetime
331    // of our implementation of LabelledGeneric for the reference case (& and &mut)
332    {
333        let generics_ref_lifetimes_mut = generics_ref.lifetimes_mut();
334        for existing_lifetime_mut in generics_ref_lifetimes_mut {
335            existing_lifetime_mut.bounds.push(ref_lifetime.clone());
336        }
337    }
338
339    // Add our current generic lifetime to the list of generics
340    let ref_lifetime_generic = GenericParam::Lifetime(ref_lifetime_def);
341    generics_ref.params.push(ref_lifetime_generic);
342
343    generics_ref
344}
345
346pub struct VariantBinding {
347    pub name: Ident,
348    pub fields: FieldBindings,
349}
350
351impl VariantBinding {
352    pub fn build_type_constr(&self) -> TokenStream2 {
353        let name = &self.name;
354        let constr = self.fields.build_type_constr(FieldBinding::build);
355        quote! { #name #constr }
356    }
357    pub fn build_type_pat_ref(&self) -> TokenStream2 {
358        let name = &self.name;
359        let constr = self.fields.build_type_constr(FieldBinding::build_pat_ref);
360        quote! { #name #constr }
361    }
362    pub fn build_type_pat_mut(&self) -> TokenStream2 {
363        let name = &self.name;
364        let constr = self.fields.build_type_constr(FieldBinding::build_pat_mut);
365        quote! { #name #constr }
366    }
367    pub fn build_hlist_field_type(&self) -> TokenStream2 {
368        build_field_type(
369            &self.name,
370            self.fields.build_hlist_type(FieldBinding::build_field_type),
371        )
372    }
373    pub fn build_hlist_field_type_ref(&self) -> TokenStream2 {
374        build_field_type(
375            &self.name,
376            self.fields
377                .build_hlist_type(FieldBinding::build_field_type_ref),
378        )
379    }
380    pub fn build_hlist_field_type_mut(&self) -> TokenStream2 {
381        build_field_type(
382            &self.name,
383            self.fields
384                .build_hlist_type(FieldBinding::build_field_type_mut),
385        )
386    }
387    pub fn build_hlist_field_expr(&self) -> TokenStream2 {
388        build_field_expr(
389            &self.name,
390            self.fields
391                .build_hlist_constr(FieldBinding::build_field_expr),
392        )
393    }
394    pub fn build_hlist_field_pat(&self) -> TokenStream2 {
395        build_field_pat(
396            self.fields
397                .build_hlist_constr(FieldBinding::build_field_pat),
398        )
399    }
400}
401
402pub struct VariantBindings {
403    pub variants: Vec<VariantBinding>,
404}
405
406impl VariantBindings {
407    pub fn new<'a>(data: impl IntoIterator<Item = &'a Variant>) -> Self {
408        VariantBindings {
409            variants: data
410                .into_iter()
411                .map(|variant| VariantBinding {
412                    name: variant.ident.clone(),
413                    fields: FieldBindings::new(&variant.fields),
414                })
415                .collect(),
416        }
417    }
418
419    pub fn build_coprod_type<R: ToTokens>(&self, f: impl Fn(&VariantBinding) -> R) -> TokenStream2 {
420        build_coprod_type(self.variants.iter().map(f))
421    }
422
423    pub fn build_coprod_constrs<R: ToTokens>(
424        &self,
425        f: impl Fn(&VariantBinding) -> R,
426    ) -> Vec<TokenStream2> {
427        self.variants
428            .iter()
429            .enumerate()
430            .map(|(index, variant)| build_coprod_constr(index, f(variant)))
431            .collect()
432    }
433
434    pub fn build_variant_constrs<R: ToTokens>(&self, f: impl Fn(&VariantBinding) -> R) -> Vec<R> {
435        self.variants.iter().map(f).collect()
436    }
437
438    pub fn build_coprod_unreachable_arm(&self, deref: bool) -> TokenStream2 {
439        build_coprod_unreachable_arm(self.variants.len(), deref)
440    }
441}