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}