Skip to main content

mq_macros/
lib.rs

1use proc_macro::TokenStream;
2use proc_macro2::{Span, TokenStream as TokenStream2};
3use quote::quote;
4use syn::{
5    Attribute, Expr, Ident, LitStr, Meta, Token,
6    parse::{Parse, ParseStream, Parser},
7    parse_macro_input,
8    punctuated::Punctuated,
9};
10
11/// Defines a builtin function and creates the corresponding `LazyLock<BuiltinFunction>` static.
12///
13/// The static name is derived by uppercasing the `name` attribute value.
14///
15/// # Parameters
16/// - `name`: The string name of the builtin (e.g., `"sort_desc"`) — also used as the static name
17///   (`SORT_DESC`).
18/// - `params`: The `ParamNum` variant without the `ParamNum::` prefix (e.g., `None`, `Fixed(1)`,
19///   `Range(0, 255)`).
20///
21/// # Example
22/// ```ignore
23/// #[mq_fn(name = "sort_desc", params = Fixed(1))]
24/// fn sort_desc_impl(
25///     ident: &Ident,
26///     _: &RuntimeValue,
27///     mut args: Args,
28///     _: &SharedEnv,
29/// ) -> Result<RuntimeValue, Error> {
30///     // implementation
31/// }
32/// // Generates: static SORT_DESC: LazyLock<BuiltinFunction> = LazyLock::new(|| ...);
33/// // Register SORT_DESC in `builtin_dispatch!` to make it callable.
34/// ```
35#[proc_macro_attribute]
36pub fn mq_fn(attr: TokenStream, item: TokenStream) -> TokenStream {
37    let item_fn = parse_macro_input!(item as syn::ItemFn);
38
39    let parser = Punctuated::<Meta, Token![,]>::parse_terminated;
40    let metas = match parser.parse(attr) {
41        Ok(m) => m,
42        Err(e) => return e.to_compile_error().into(),
43    };
44
45    let mut name_lit: Option<LitStr> = None;
46    let mut params_expr: Option<Expr> = None;
47
48    for meta in &metas {
49        match meta {
50            Meta::NameValue(nv) if nv.path.is_ident("name") => {
51                if let Expr::Lit(syn::ExprLit {
52                    lit: syn::Lit::Str(s), ..
53                }) = &nv.value
54                {
55                    name_lit = Some(s.clone());
56                }
57            }
58            Meta::NameValue(nv) if nv.path.is_ident("params") => {
59                params_expr = Some(nv.value.clone());
60            }
61            _ => {
62                return syn::Error::new_spanned(meta, "unknown mq_fn attribute key")
63                    .to_compile_error()
64                    .into();
65            }
66        }
67    }
68
69    let name = match name_lit {
70        Some(n) => n,
71        None => {
72            return syn::Error::new(Span::call_site(), "mq_fn requires `name = \"...\"`")
73                .to_compile_error()
74                .into();
75        }
76    };
77
78    let params = match params_expr {
79        Some(p) => p,
80        None => {
81            return syn::Error::new(Span::call_site(), "mq_fn requires `params = ...`")
82                .to_compile_error()
83                .into();
84        }
85    };
86
87    let fn_ident = &item_fn.sig.ident;
88    let static_name = name.value().to_uppercase();
89    let static_ident = Ident::new(&static_name, Span::call_site());
90
91    let cfg_attrs: Vec<&Attribute> = item_fn.attrs.iter().filter(|a| a.path().is_ident("cfg")).collect();
92
93    quote! {
94        #item_fn
95
96        #(#cfg_attrs)*
97        #[allow(non_upper_case_globals)]
98        static #static_ident: ::std::sync::LazyLock<BuiltinFunction> =
99            ::std::sync::LazyLock::new(|| BuiltinFunction::new(#name, ParamNum::#params, #fn_ident));
100    }
101    .into()
102}
103
104struct BuiltinEntry {
105    attrs: Vec<Attribute>,
106    ident: Ident,
107}
108
109impl Parse for BuiltinEntry {
110    fn parse(input: ParseStream) -> syn::Result<Self> {
111        Ok(BuiltinEntry {
112            attrs: input.call(Attribute::parse_outer)?,
113            ident: input.parse()?,
114        })
115    }
116}
117
118struct BuiltinDispatchInput {
119    entries: Punctuated<BuiltinEntry, Token![,]>,
120}
121
122impl Parse for BuiltinDispatchInput {
123    fn parse(input: ParseStream) -> syn::Result<Self> {
124        Ok(BuiltinDispatchInput {
125            entries: Punctuated::parse_terminated(input)?,
126        })
127    }
128}
129
130/// Generates FNV-1a hash constants and the `get_builtin_functions_by_str` dispatch function
131/// from a compact list of builtin static names.
132///
133/// The string name used for hashing and lookup is derived by lowercasing the static identifier.
134/// The hash constant name strips any leading underscores from the static name (e.g., `_DIFF`
135/// becomes `HASH_DIFF`).
136///
137/// Supports `#[cfg(...)]` attributes on individual entries.
138///
139/// # Example
140/// ```ignore
141/// builtin_dispatch! {
142///     ABS,
143///     ADD,
144///     SORT_DESC,
145///     #[cfg(feature = "file-io")]
146///     READ_FILE,
147/// }
148/// ```
149/// Generates `const HASH_ABS`, `const HASH_ADD`, `const HASH_SORT_DESC`, and
150/// `pub fn get_builtin_functions_by_str` with a `match fnv1a_hash_64(name_str)` body.
151#[proc_macro]
152pub fn builtin_dispatch(input: TokenStream) -> TokenStream {
153    let BuiltinDispatchInput { entries } = parse_macro_input!(input as BuiltinDispatchInput);
154
155    let mut hash_consts: Vec<TokenStream2> = Vec::with_capacity(entries.len());
156    let mut match_arms: Vec<TokenStream2> = Vec::with_capacity(entries.len());
157
158    for entry in &entries {
159        let ident = &entry.ident;
160        let name_str = ident.to_string().to_lowercase();
161        let hash_name = format!("HASH_{}", ident.to_string().trim_start_matches('_'));
162        let hash_ident = Ident::new(&hash_name, ident.span());
163        let attrs = &entry.attrs;
164
165        hash_consts.push(quote! {
166            #(#attrs)*
167            const #hash_ident: u64 = fnv1a_hash_64(#name_str);
168        });
169
170        match_arms.push(quote! {
171            #(#attrs)*
172            #hash_ident => Some(&#ident),
173        });
174    }
175
176    quote! {
177        #(#hash_consts)*
178
179        pub fn get_builtin_functions_by_str(name_str: &str) -> Option<&'static BuiltinFunction> {
180            match fnv1a_hash_64(name_str) {
181                #(#match_arms)*
182                _ => None,
183            }
184            .filter(|func| func.name == name_str)
185            .map(|v| &**v)
186        }
187    }
188    .into()
189}