tokay_macros/
lib.rs

1/*! Tokay proc-macros
2
3This crate contains the proc-macro implementation for
4
5- tokay_function!(signature, expression) - Built-in function
6- tokay_method!(signature, expression) - Built-in object method
7- tokay_token!(signature, expression) - Built-in consuming function
8
9Every macro generates a slightly different version of a callable built-in.
10
11All macros require for two parameters:
12
13- *signature* is a Tokay-style function signature string, including default values.
14  This can be `f`, `f()`, `f(a, b)`, `f(a b = void)` or similar.
15  Currently, this does only accept for a subset of Tokay atomics: void, null, true, false,
16  and integer values.
17- *expression* is the Rust expression to be executed. This is the body of the function.
18*/
19
20use glob::glob;
21use proc_macro::TokenStream;
22use proc_macro2;
23use quote::{quote, quote_spanned};
24use syn;
25use tokay_04 as tokay; // This is Tokay v0.4
26
27/* Tokay v0.4 compat, the function has been reworked in v0.5 */
28fn tokay_run(src: &str, input: &str) -> Result<Option<tokay::value::Value>, String> {
29    // disable any debug inside of this process
30    std::env::set_var("TOKAY_DEBUG", "0");
31    std::env::set_var("TOKAY_PARSER_DEBUG", "0");
32
33    let mut compiler = tokay::compiler::Compiler::new();
34    let program = compiler.compile(tokay::reader::Reader::new(Box::new(std::io::Cursor::new(
35        src.to_owned(),
36    ))));
37
38    match program {
39        Ok(program) => program
40            .run_from_string(input.to_owned())
41            .map_err(|err| err.to_string()),
42        Err(errors) => Err(errors
43            .into_iter()
44            .map(|err| err.to_string())
45            .collect::<Vec<String>>()
46            .join("\n")),
47    }
48}
49
50/// Describes a builtin function and its arguments.
51struct BuiltinDef {
52    name: syn::Ident,
53    arguments: Vec<(String, String)>,
54    body: syn::Expr,
55}
56
57impl syn::parse::Parse for BuiltinDef {
58    fn parse(stream: syn::parse::ParseStream) -> syn::Result<Self> {
59        let signature = stream.parse::<syn::LitStr>()?;
60        let _ = stream.parse::<syn::Token![,]>()?;
61        let body = stream.parse::<syn::Expr>()?;
62
63        // Collect arguments and possible required marker.
64        let res = match tokay_run(include_str!("signature.tok"), &signature.value()) {
65            Err(msg) => return Err(syn::parse::Error::new(signature.span(), msg)),
66            Ok(ast) => ast.unwrap().to_list(),
67        };
68
69        let mut arguments = Vec::new();
70        let name = res[0].borrow().to_string();
71
72        if res.len() > 1 {
73            let args = res[1].borrow().to_list();
74
75            // fixme: This is a little bit ugly but is needed to use Tokay v0.4 here.
76            //        It has to be improved when using a higher Tokay version for building later.
77            for item in args.iter() {
78                let arg = &*item.borrow();
79                if let Some(arg) = arg.get_list() {
80                    //println!("{} {:?}", name, item);
81                    arguments.push((arg[0].borrow().to_string(), arg[1].borrow().to_string()));
82                } else {
83                    //println!("{} {:?}", name, args);
84                    arguments.push((args[0].borrow().to_string(), args[1].borrow().to_string()));
85                    break; // Tokay v0.4 special case... don't ask for this.
86                }
87            }
88        }
89
90        Ok(BuiltinDef {
91            name: syn::Ident::new(&name, proc_macro2::Span::call_site()),
92            arguments,
93            body,
94        })
95    }
96}
97
98fn gen_assign_arguments(arguments: Vec<(String, String)>) -> Vec<proc_macro2::TokenStream> {
99    let mut ret = Vec::new();
100
101    let mut count: usize = 0;
102    let mut args = false;
103    let mut nargs = false;
104
105    for (arg, default) in arguments {
106        if arg == "*args" {
107            // fixme: This must be handled by signature.tok later...
108            if args {
109                ret.push(quote! {
110                    compile_error!("Multiple usage of *args");
111                });
112            }
113
114            args = true;
115            continue;
116        } else if arg == "**nargs" {
117            // fixme: This must be handled by signature.tok later...
118            if nargs {
119                ret.push(quote! {
120                    compile_error!("Multiple usage of *nargs");
121                });
122            }
123
124            nargs = true;
125            continue;
126        }
127
128        count += 1;
129
130        let arg = syn::Ident::new(
131            &arg,
132            proc_macro2::Span::call_site(), // todo: this can be specified more specific
133        );
134
135        ret.push({
136            let required = default.is_empty();
137            let default = match &default[..] {
138                "void" | "" => quote!(tokay::value!(void)),
139                "null" => quote!(tokay::value!(null)),
140                "true" => quote!(tokay::value!(true)),
141                "false" => quote!(tokay::value!(false)),
142                int if int.parse::<i64>().is_ok() => {
143                    let int = int.parse::<i64>().unwrap();
144                    quote!(tokay::value!(#int))
145                }
146                _ => unreachable!(),
147            };
148
149            quote! {
150                let mut #arg =
151                    if !args.is_empty() {
152                        args.remove(0)
153                    }
154                    else {
155                        let mut value = None;
156
157                        if let Some(nargs) = &mut nargs {
158                            value = nargs.remove_str(stringify!(#arg));
159                        }
160
161                        if value.is_none() {
162                            if #required {
163                                return Err(format!("{} expected argument '{}'", __function, stringify!(#arg)).into()).into();
164                            }
165                            else {
166                                #default
167                            }
168                        }
169                        else {
170                            value.unwrap()
171                        }
172                    }
173                ;
174
175                //println!("{} = {}", stringify!(#arg), #arg);
176            }
177        });
178    }
179
180    if !args {
181        ret.push(quote! {
182            if args.len() > 0 {
183                return Err(
184                    match #count {
185                        0 => format!("{} doesn't accept any arguments ({} given)", __function, args.len()),
186                        1 => format!("{} takes exactly one argument ({} given)", __function, #count + args.len()),
187                        _ => format!("{} expected at most {} arguments ({} given)", __function, #count, #count + args.len()),
188                    }.into()
189                ).into()
190            }
191        });
192    }
193
194    if !nargs {
195        ret.push(quote! {
196            if let Some(mut nargs) = nargs {
197                if let Some((name, _)) = nargs.pop() {
198                    return Err(
199                        match nargs.len() {
200                            0 => format!("{} doesn't accept named argument '{}'", __function, name.to_string()),
201                            n => format!("{} doesn't accept named arguments ({} given)", __function, n + 1),
202                        }.into()
203                    ).into()
204                }
205            }
206        });
207    }
208
209    ret
210}
211
212#[proc_macro]
213pub fn tokay_method(input: TokenStream) -> TokenStream {
214    let def = syn::parse_macro_input!(input as BuiltinDef);
215
216    let name = def.name;
217    let internal = syn::Ident::new(
218        &format!("{}_internal", name.to_string()),
219        proc_macro2::Span::call_site(),
220    );
221    let callable = syn::Ident::new(
222        &format!("tokay_method_{}", name.to_string()),
223        proc_macro2::Span::call_site(),
224    );
225
226    // Method names must start with a lower-case letter
227    if !name.to_string().chars().next().unwrap().is_lowercase() {
228        return quote_spanned! {
229            name.span() => compile_error!(
230                "Method identifier must start with a lower-case letter"
231            );
232        }
233        .into();
234    }
235
236    // Generate assignment to identifier for each argument.
237    let arguments = gen_assign_arguments(def.arguments);
238    let body = def.body;
239
240    // Generate two functions: One for direct usage from other Rust code,
241    // and one wrapping function for calls from the Tokay VM or a Method.
242    // The direct usage function will return an Result<RefValue, Error>
243    // instead of an Result<Accept, Reject>.
244    let gen = quote! {
245        fn #internal(
246            context: Option<&mut tokay::Context>,
247            mut args: Vec<tokay::RefValue>,
248            mut nargs: Option<tokay::Dict>
249        ) -> Result<tokay::RefValue, tokay::Error> {
250            // The function's original name in Tokay
251            let __function = concat!(stringify!(#name), "()");
252
253            // Arguments
254            #(#arguments)*
255
256            // Body
257            #body
258        }
259
260        pub fn #name(
261            args: Vec<tokay::RefValue>,
262            nargs: Option<tokay::Dict>
263        ) -> Result<tokay::RefValue, tokay::Error> {
264            Self::#internal(None, args, nargs)
265        }
266
267        pub fn #callable(
268            context: Option<&mut tokay::Context>,
269            args: Vec<tokay::RefValue>,
270            nargs: Option<tokay::Dict>
271        ) -> Result<tokay::Accept, tokay::Reject> {
272            let ret = Self::#internal(context, args, nargs)?;
273            Ok(tokay::Accept::Push(tokay::Capture::Value(ret, None, 10)))
274        }
275    };
276
277    //println!("{} {:?}", function.to_string(), def.required);
278
279    TokenStream::from(gen)
280}
281
282#[proc_macro]
283pub fn tokay_function(input: TokenStream) -> TokenStream {
284    let def = syn::parse_macro_input!(input as BuiltinDef);
285
286    let name = def.name;
287    let callable = syn::Ident::new(
288        &format!("tokay_function_{}", name.to_string()),
289        proc_macro2::Span::call_site(),
290    );
291
292    // Function names must start with a lower-case letter
293    if !name.to_string().chars().next().unwrap().is_lowercase() {
294        return quote_spanned! {
295            name.span() => compile_error!(
296                "Function identifier must start with a lower-case letter"
297            );
298        }
299        .into();
300    }
301
302    // Generate assignment to identifier for each argument.
303    let arguments = gen_assign_arguments(def.arguments);
304    let body = def.body;
305
306    // Generate function
307    let gen = quote! {
308        pub fn #callable(
309            context: Option<&mut tokay::vm::Context>,
310            mut args: Vec<tokay::RefValue>,
311            mut nargs: Option<tokay::Dict>
312        ) -> Result<tokay::vm::Accept, tokay::vm::Reject> {
313            // The function's original name in Tokay
314            let __function = concat!(stringify!(#name), "()");
315
316            // Arguments
317            #(#arguments)*
318
319            // Body
320            #body
321        }
322    };
323
324    TokenStream::from(gen)
325}
326
327#[proc_macro]
328pub fn tokay_token(input: TokenStream) -> TokenStream {
329    let def = syn::parse_macro_input!(input as BuiltinDef);
330
331    let name = def.name;
332
333    // Token names must start with an upper-case letter or underscore
334    if !{
335        let ch = name.to_string().chars().next().unwrap();
336        ch.is_uppercase() || ch == '_'
337    } {
338        return quote_spanned! {
339            name.span() => compile_error!(
340                "Token identifier must start with an upper-case letter or underscore"
341            );
342        }
343        .into();
344    }
345
346    let function = syn::Ident::new(
347        &name.to_string().to_lowercase(),
348        proc_macro2::Span::call_site(),
349    );
350    let callable = syn::Ident::new(
351        &format!("tokay_token_{}", name.to_string().to_lowercase()),
352        proc_macro2::Span::call_site(),
353    );
354
355    // Generate assignment to identifier for each argument.
356    let arguments = gen_assign_arguments(def.arguments);
357    let body = def.body;
358
359    // Generate function and wrapper
360    let gen = quote! {
361        pub fn #function(
362            context: &mut tokay::vm::Context,
363            mut args: Vec<tokay::RefValue>,
364            mut nargs: Option<tokay::Dict>
365        ) -> Result<tokay::Accept, tokay::Reject> {
366            // The function's original name in Tokay
367            let __function = concat!(stringify!(#name), "()");
368
369            // Arguments
370            #(#arguments)*
371
372            // Body
373            #body
374        }
375
376        pub fn #callable(
377            context: Option<&mut tokay::Context>,
378            args: Vec<tokay::RefValue>,
379            nargs: Option<tokay::Dict>
380        ) -> Result<tokay::Accept, tokay::Reject> {
381            #function(context.unwrap(), args, nargs)
382        }
383    };
384
385    TokenStream::from(gen)
386}
387
388#[proc_macro]
389pub fn tokay_tests(input: TokenStream) -> TokenStream {
390    let pattern = syn::parse_macro_input!(input as syn::LitStr);
391    let pattern = pattern.value();
392
393    let mut tests = Vec::new();
394
395    for test in glob(&pattern).expect(&format!("Failed to read {:?}", pattern)) {
396        let test = test.unwrap();
397        let name = format!("test_{}", test.file_stem().unwrap().to_str().unwrap());
398        let name = syn::Ident::new(&name, proc_macro2::Span::call_site());
399        let path = test.to_str().unwrap();
400
401        tests.push(TokenStream::from(quote!(
402            #[test]
403            fn #name() {
404                crate::utils::testcase(#path);
405            }
406        )));
407    }
408
409    //println!("tests = {:#?}", tests);
410
411    return TokenStream::from_iter(tests.into_iter());
412}