fift_proc/
lib.rs

1use std::collections::HashMap;
2
3use darling::{Error, FromMeta};
4use proc_macro::TokenStream;
5use quote::quote;
6use syn::ItemImpl;
7
8#[derive(Debug, FromMeta)]
9struct FiftCmdArgs {
10    #[darling(default)]
11    tail: bool,
12    #[darling(default)]
13    active: bool,
14    #[darling(default)]
15    stack: bool,
16
17    #[darling(default)]
18    without_space: bool,
19
20    name: String,
21
22    #[darling(default)]
23    args: Option<HashMap<String, syn::Expr>>,
24}
25
26#[proc_macro_attribute]
27pub fn fift_module(_: TokenStream, input: TokenStream) -> TokenStream {
28    let mut input = syn::parse_macro_input!(input as ItemImpl);
29
30    let dict_arg = quote::format_ident!("__dict");
31
32    let mut definitions = Vec::new();
33    let mut errors = Vec::new();
34
35    let mut init_function_names = Vec::new();
36    let mut init_functions = Vec::new();
37    let mut other_functions = Vec::new();
38
39    for impl_item in input.items.drain(..) {
40        let syn::ImplItem::Fn(mut fun) = impl_item else {
41            other_functions.push(impl_item);
42            continue;
43        };
44
45        let mut has_init = false;
46
47        let mut cmd_attrs = Vec::with_capacity(fun.attrs.len());
48        let mut remaining_attr = Vec::new();
49        for attr in fun.attrs.drain(..) {
50            if let Some(path) = attr.meta.path().get_ident() {
51                if path == "cmd" {
52                    cmd_attrs.push(attr);
53                    continue;
54                } else if path == "init" {
55                    has_init = true;
56                    continue;
57                }
58            }
59
60            remaining_attr.push(attr);
61        }
62        fun.attrs = remaining_attr;
63
64        if has_init {
65            fun.sig.ident = quote::format_ident!("__{}", fun.sig.ident);
66            init_function_names.push(fun.sig.ident.clone());
67            init_functions.push(fun);
68        } else {
69            for attr in cmd_attrs {
70                match process_cmd_definition(&fun, &dict_arg, attr) {
71                    Ok(definition) => definitions.push(definition),
72                    Err(e) => errors.push(e),
73                }
74            }
75
76            other_functions.push(syn::ImplItem::Fn(fun));
77        }
78    }
79
80    if !errors.is_empty() {
81        return TokenStream::from(Error::multiple(errors).write_errors());
82    }
83
84    let ty = input.self_ty;
85    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
86
87    quote! {
88        impl #impl_generics #ty #ty_generics #where_clause {
89            #(#init_functions)*
90        }
91
92        #[automatically_derived]
93        impl #impl_generics ::fift::core::Module for #ty #ty_generics #where_clause {
94            fn init(
95                &self,
96                #dict_arg: &mut ::fift::core::Dictionary,
97            ) -> ::core::result::Result<(), ::fift::error::Error> {
98                #(self.#init_function_names(#dict_arg)?;)*
99                #(#definitions?;)*
100                Ok(())
101            }
102        }
103
104        #(#other_functions)*
105    }
106    .into()
107}
108
109fn process_cmd_definition(
110    function: &syn::ImplItemFn,
111    dict_arg: &syn::Ident,
112    attr: syn::Attribute,
113) -> Result<syn::Expr, Error> {
114    let cmd = FiftCmdArgs::from_meta(&attr.meta)?;
115
116    let reg_fn = match (cmd.tail, cmd.active, cmd.stack) {
117        (false, false, false) => quote! { define_context_word },
118        (true, false, false) => quote! { define_context_tail_word },
119        (false, true, false) => quote! { define_active_word },
120        (false, false, true) => quote! { define_stack_word },
121        _ => {
122            return Err(Error::custom(
123                "`tail`, `active` and `stack` cannot be used together",
124            ));
125        }
126    };
127
128    let cmd_name = if cmd.without_space {
129        cmd.name.trim().to_owned()
130    } else {
131        format!("{} ", cmd.name.trim())
132    };
133
134    let function_name = function.sig.ident.clone();
135    let expr = match cmd.args {
136        None => {
137            quote! { #function_name }
138        }
139        Some(mut provided_args) => {
140            let ctx_arg = quote::format_ident!("__c");
141            let required_args = find_command_args(function)?;
142
143            let mut errors = Vec::new();
144            let mut closure_args = vec![quote! { #ctx_arg }];
145            for arg in required_args {
146                match provided_args.remove(&arg) {
147                    Some(value) => closure_args.push(quote! { #value }),
148                    None => errors.push(Error::custom(format!(
149                        "No value provided for the argument `{arg}`"
150                    ))),
151                }
152            }
153
154            for arg in provided_args.into_keys() {
155                errors.push(Error::custom(format!("Unknown function argument `{arg}`")));
156            }
157
158            if !errors.is_empty() {
159                return Err(Error::multiple(errors).with_span(&attr));
160            }
161
162            quote! { |#ctx_arg| #function_name(#(#closure_args),*)  }
163        }
164    };
165
166    Ok(syn::parse_quote! { #dict_arg.#reg_fn(#cmd_name, #expr) })
167}
168
169fn find_command_args(function: &syn::ImplItemFn) -> Result<Vec<String>, Error> {
170    let mut inputs = function.sig.inputs.iter();
171
172    if let Some(first) = inputs.next()
173        && !matches!(first, syn::FnArg::Typed(_))
174    {
175        return Err(Error::custom("Command context argument not found").with_span(&function));
176    }
177
178    let mut args = Vec::new();
179    for input in inputs {
180        let syn::FnArg::Typed(input) = input else {
181            continue;
182        };
183        let syn::Pat::Ident(pat) = &*input.pat else {
184            return Err(Error::custom("Unsupported argument binding").with_span(&input.pat));
185        };
186        args.push(pat.ident.to_string());
187    }
188
189    Ok(args)
190}