Skip to main content

runmat_macros/
lib.rs

1use proc_macro::TokenStream;
2use quote::{format_ident, quote};
3use std::fs;
4use std::path::{Path, PathBuf};
5use std::sync::{Mutex, OnceLock};
6use syn::parse::{Parse, ParseStream};
7use syn::{
8    parse_macro_input, AttributeArgs, Expr, FnArg, ItemConst, ItemFn, Lit, LitStr, Meta,
9    MetaNameValue, NestedMeta, Pat,
10};
11
12static WASM_REGISTRY_PATH: OnceLock<Option<PathBuf>> = OnceLock::new();
13static WASM_REGISTRY_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
14static WASM_REGISTRY_INIT: OnceLock<()> = OnceLock::new();
15
16/// Attribute used to mark functions as implementing a runtime builtin.
17///
18/// Example:
19/// ```rust,ignore
20/// use runmat_macros::runtime_builtin;
21///
22/// #[runtime_builtin(name = "plot")]
23/// pub fn plot_line(xs: &[f64]) {
24///     /* implementation */
25/// }
26/// ```
27///
28/// This registers the function with the `runmat-builtins` inventory
29/// so the runtime can discover it at start-up.
30#[proc_macro_attribute]
31pub fn runtime_builtin(args: TokenStream, input: TokenStream) -> TokenStream {
32    // Parse attribute arguments as `name = "..."`
33    let args = parse_macro_input!(args as AttributeArgs);
34    let mut name_lit: Option<Lit> = None;
35    let mut category_lit: Option<Lit> = None;
36    let mut summary_lit: Option<Lit> = None;
37    let mut keywords_lit: Option<Lit> = None;
38    let mut errors_lit: Option<Lit> = None;
39    let mut related_lit: Option<Lit> = None;
40    let mut introduced_lit: Option<Lit> = None;
41    let mut status_lit: Option<Lit> = None;
42    let mut examples_lit: Option<Lit> = None;
43    let mut accel_values: Vec<String> = Vec::new();
44    let mut builtin_path_lit: Option<LitStr> = None;
45    let mut type_resolver_path: Option<syn::Path> = None;
46    let mut type_resolver_ctx_path: Option<syn::Path> = None;
47    let mut descriptor_path: Option<syn::Path> = None;
48    let mut extensions_path: Option<syn::Path> = None;
49    let mut integer_capabilities_path: Option<syn::Path> = None;
50    let mut integer_audit_path: Option<syn::Path> = None;
51    let mut binding_variant_lit: Option<LitStr> = None;
52    let mut sink_flag = false;
53    let mut suppress_auto_output_flag = false;
54    for arg in args {
55        match arg {
56            NestedMeta::Meta(Meta::NameValue(MetaNameValue { path, lit, .. })) => {
57                if path.is_ident("name") {
58                    name_lit = Some(lit);
59                } else if path.is_ident("category") {
60                    category_lit = Some(lit);
61                } else if path.is_ident("summary") {
62                    summary_lit = Some(lit);
63                } else if path.is_ident("keywords") {
64                    keywords_lit = Some(lit);
65                } else if path.is_ident("errors") {
66                    errors_lit = Some(lit);
67                } else if path.is_ident("related") {
68                    related_lit = Some(lit);
69                } else if path.is_ident("introduced") {
70                    introduced_lit = Some(lit);
71                } else if path.is_ident("status") {
72                    status_lit = Some(lit);
73                } else if path.is_ident("examples") {
74                    examples_lit = Some(lit);
75                } else if path.is_ident("accel") {
76                    if let Lit::Str(ls) = lit {
77                        accel_values.extend(
78                            ls.value()
79                                .split(|c: char| c == ',' || c == '|' || c.is_ascii_whitespace())
80                                .filter(|s| !s.is_empty())
81                                .map(|s| s.to_ascii_lowercase()),
82                        );
83                    }
84                } else if path.is_ident("sink") {
85                    if let Lit::Bool(lb) = lit {
86                        sink_flag = lb.value;
87                    }
88                } else if path.is_ident("suppress_auto_output") {
89                    if let Lit::Bool(lb) = lit {
90                        suppress_auto_output_flag = lb.value;
91                    }
92                } else if path.is_ident("builtin_path") {
93                    if let Lit::Str(ls) = lit {
94                        builtin_path_lit = Some(ls);
95                    } else {
96                        panic!("builtin_path must be a string literal");
97                    }
98                } else if path.is_ident("binding_variant") {
99                    if let Lit::Str(ls) = lit {
100                        binding_variant_lit = Some(ls);
101                    } else {
102                        panic!("binding_variant must be a string literal");
103                    }
104                } else if path.is_ident("type_resolver") {
105                    if let Lit::Str(ls) = lit {
106                        let parsed: syn::Path = ls.parse().expect("type_resolver must be a path");
107                        type_resolver_path = Some(parsed);
108                    } else {
109                        panic!("type_resolver must be a string literal path");
110                    }
111                } else if path.is_ident("type_resolver_ctx") {
112                    if let Lit::Str(ls) = lit {
113                        let parsed: syn::Path =
114                            ls.parse().expect("type_resolver_ctx must be a path");
115                        type_resolver_ctx_path = Some(parsed);
116                    } else {
117                        panic!("type_resolver_ctx must be a string literal path");
118                    }
119                } else {
120                    // Gracefully ignore unknown parameters for better IDE experience
121                }
122            }
123            NestedMeta::Meta(Meta::List(list)) if list.path.is_ident("type_resolver") => {
124                if list.nested.len() != 1 {
125                    panic!("type_resolver expects exactly one path argument");
126                }
127                let nested = list.nested.first().unwrap();
128                if let NestedMeta::Meta(Meta::Path(path)) = nested {
129                    type_resolver_path = Some(path.clone());
130                } else {
131                    panic!("type_resolver expects a path argument");
132                }
133            }
134            NestedMeta::Meta(Meta::List(list)) if list.path.is_ident("type_resolver_ctx") => {
135                if list.nested.len() != 1 {
136                    panic!("type_resolver_ctx expects exactly one path argument");
137                }
138                let nested = list.nested.first().unwrap();
139                if let NestedMeta::Meta(Meta::Path(path)) = nested {
140                    type_resolver_ctx_path = Some(path.clone());
141                } else {
142                    panic!("type_resolver_ctx expects a path argument");
143                }
144            }
145            NestedMeta::Meta(Meta::List(list)) if list.path.is_ident("descriptor") => {
146                if list.nested.len() != 1 {
147                    panic!("descriptor expects exactly one path argument");
148                }
149                let nested = list.nested.first().unwrap();
150                if let NestedMeta::Meta(Meta::Path(path)) = nested {
151                    descriptor_path = Some(path.clone());
152                } else {
153                    panic!("descriptor expects a path argument");
154                }
155            }
156            NestedMeta::Meta(Meta::List(list)) if list.path.is_ident("extensions") => {
157                if list.nested.len() != 1 {
158                    panic!("extensions expects exactly one path argument");
159                }
160                let nested = list.nested.first().unwrap();
161                if let NestedMeta::Meta(Meta::Path(path)) = nested {
162                    extensions_path = Some(path.clone());
163                } else {
164                    panic!("extensions expects a path argument");
165                }
166            }
167            NestedMeta::Meta(Meta::List(list)) if list.path.is_ident("integer_capabilities") => {
168                if list.nested.len() != 1 {
169                    panic!("integer_capabilities expects exactly one path argument");
170                }
171                let nested = list.nested.first().unwrap();
172                if let NestedMeta::Meta(Meta::Path(path)) = nested {
173                    integer_capabilities_path = Some(path.clone());
174                } else {
175                    panic!("integer_capabilities expects a path argument");
176                }
177            }
178            NestedMeta::Meta(Meta::List(list)) if list.path.is_ident("integer_audit") => {
179                if list.nested.len() != 1 {
180                    panic!("integer_audit expects exactly one path argument");
181                }
182                let nested = list.nested.first().unwrap();
183                if let NestedMeta::Meta(Meta::Path(path)) = nested {
184                    integer_audit_path = Some(path.clone());
185                } else {
186                    panic!("integer_audit expects a path argument");
187                }
188            }
189            _ => {}
190        }
191    }
192    let name_lit = name_lit.expect("expected `name = \"...\"` argument");
193    let name_str = if let Lit::Str(ref s) = name_lit {
194        s.value()
195    } else {
196        panic!("name must be a string literal");
197    };
198
199    let func: ItemFn = parse_macro_input!(input as ItemFn);
200    let ident = &func.sig.ident;
201    let is_async = func.sig.asyncness.is_some();
202
203    // Extract param idents and types
204    let mut param_idents = Vec::new();
205    let mut param_types = Vec::new();
206    for arg in &func.sig.inputs {
207        match arg {
208            FnArg::Typed(pt) => {
209                // pattern must be ident
210                if let Pat::Ident(pi) = pt.pat.as_ref() {
211                    param_idents.push(pi.ident.clone());
212                } else {
213                    panic!("parameters must be simple identifiers");
214                }
215                param_types.push((*pt.ty).clone());
216            }
217            _ => panic!("self parameter not allowed"),
218        }
219    }
220    let param_len = param_idents.len();
221
222    // Infer parameter types for BuiltinFunction
223    let inferred_param_types: Vec<proc_macro2::TokenStream> =
224        param_types.iter().map(infer_builtin_type).collect();
225
226    // Infer return type for BuiltinFunction
227    let inferred_return_type = match &func.sig.output {
228        syn::ReturnType::Default => quote! { runmat_builtins::Type::Void },
229        syn::ReturnType::Type(_, ty) => infer_builtin_type(ty),
230    };
231
232    // Detect if last parameter is variadic Vec<Value>
233    let is_last_variadic = param_types
234        .last()
235        .map(|ty| {
236            // crude detection: type path starts with Vec and inner type is runmat_value::Value or Value
237            if let syn::Type::Path(tp) = ty {
238                if tp
239                    .path
240                    .segments
241                    .last()
242                    .map(|s| s.ident == "Vec")
243                    .unwrap_or(false)
244                {
245                    if let syn::PathArguments::AngleBracketed(ab) =
246                        &tp.path.segments.last().unwrap().arguments
247                    {
248                        if let Some(syn::GenericArgument::Type(syn::Type::Path(inner))) =
249                            ab.args.first()
250                        {
251                            return inner
252                                .path
253                                .segments
254                                .last()
255                                .map(|s| s.ident == "Value")
256                                .unwrap_or(false);
257                        }
258                    }
259                }
260            }
261            false
262        })
263        .unwrap_or(false);
264
265    // Generate wrapper ident
266    let wrapper_ident = format_ident!("__rt_wrap_{}", ident);
267
268    let conv_stmts: Vec<proc_macro2::TokenStream> = if is_last_variadic && param_len > 0 {
269        let mut stmts = Vec::new();
270        // Convert fixed params (all but last)
271        for (i, (ident, ty)) in param_idents
272            .iter()
273            .zip(param_types.iter())
274            .enumerate()
275            .take(param_len - 1)
276        {
277            stmts.push(quote! { let #ident : #ty = std::convert::TryInto::try_into(&args[#i])?; });
278        }
279        // Collect the rest into Vec<Value>
280        let last_ident = &param_idents[param_len - 1];
281        stmts.push(quote! {
282            let #last_ident : Vec<runmat_value::Value> = {
283                let mut v = Vec::new();
284                for j in (#param_len-1)..args.len() {
285                    let item : runmat_value::Value = std::convert::TryInto::try_into(&args[j])?;
286                    v.push(item);
287                }
288                v
289            };
290        });
291        stmts
292    } else {
293        param_idents
294            .iter()
295            .zip(param_types.iter())
296            .enumerate()
297            .map(|(i, (ident, ty))| {
298                quote! { let #ident : #ty = std::convert::TryInto::try_into(&args[#i])?; }
299            })
300            .collect()
301    };
302
303    let call_expr = if is_async {
304        quote! { #ident(#(#param_idents),*).await? }
305    } else {
306        quote! { #ident(#(#param_idents),*)? }
307    };
308
309    let wrapper = quote! {
310        fn #wrapper_ident(args: &[runmat_value::Value]) -> runmat_builtins::BuiltinFuture {
311            #![allow(unused_variables)]
312            let args = args.to_vec();
313            Box::pin(async move {
314                if #is_last_variadic {
315                    if args.len() < #param_len - 1 {
316                        return Err(std::convert::From::from(format!(
317                            "expected at least {} args, got {}",
318                            #param_len - 1,
319                            args.len()
320                        )));
321                    }
322                } else if args.len() != #param_len {
323                    return Err(std::convert::From::from(format!(
324                        "expected {} args, got {}",
325                        #param_len,
326                        args.len()
327                    )));
328                }
329                #(#conv_stmts)*
330                let value = #call_expr;
331                Ok(runmat_value::Value::from(value))
332            })
333        }
334    };
335
336    // Prepare tokens for defaults and options
337    let default_category = syn::LitStr::new("general", proc_macro2::Span::call_site());
338    let default_summary =
339        syn::LitStr::new("Runtime builtin function", proc_macro2::Span::call_site());
340
341    let category_tok: proc_macro2::TokenStream = match &category_lit {
342        Some(syn::Lit::Str(ls)) => quote! { #ls },
343        _ => quote! { #default_category },
344    };
345    let summary_tok: proc_macro2::TokenStream = match &summary_lit {
346        Some(syn::Lit::Str(ls)) => quote! { #ls },
347        _ => quote! { #default_summary },
348    };
349
350    fn opt_tok(lit: &Option<syn::Lit>) -> proc_macro2::TokenStream {
351        if let Some(syn::Lit::Str(ls)) = lit {
352            quote! { Some(#ls) }
353        } else {
354            quote! { None }
355        }
356    }
357    let category_opt_tok = opt_tok(&category_lit);
358    let summary_opt_tok = opt_tok(&summary_lit);
359    let keywords_opt_tok = opt_tok(&keywords_lit);
360    let errors_opt_tok = opt_tok(&errors_lit);
361    let related_opt_tok = opt_tok(&related_lit);
362    let introduced_opt_tok = opt_tok(&introduced_lit);
363    let status_opt_tok = opt_tok(&status_lit);
364    let examples_opt_tok = opt_tok(&examples_lit);
365
366    let accel_tokens: Vec<proc_macro2::TokenStream> = accel_values
367        .iter()
368        .map(|mode| match mode.as_str() {
369            "unary" => quote! { runmat_builtins::AccelTag::Unary },
370            "binary" => quote! { runmat_builtins::AccelTag::Elementwise },
371            "elementwise" => quote! { runmat_builtins::AccelTag::Elementwise },
372            "reduction" => quote! { runmat_builtins::AccelTag::Reduction },
373            "matmul" => quote! { runmat_builtins::AccelTag::MatMul },
374            "transpose" => quote! { runmat_builtins::AccelTag::Transpose },
375            "array_construct" => quote! { runmat_builtins::AccelTag::ArrayConstruct },
376            _ => quote! {},
377        })
378        .filter(|ts| !ts.is_empty())
379        .collect();
380    let accel_slice = if accel_tokens.is_empty() {
381        quote! { &[] as &[runmat_builtins::AccelTag] }
382    } else {
383        quote! { &[#(#accel_tokens),*] }
384    };
385    let type_resolver_expr = if let Some(path) = type_resolver_ctx_path.as_ref() {
386        quote! { Some(runmat_builtins::type_resolver_kind_ctx(#path)) }
387    } else if let Some(path) = type_resolver_path.as_ref() {
388        quote! { Some(runmat_builtins::type_resolver_kind_ctx(#path)) }
389    } else {
390        quote! { None }
391    };
392    let sink_bool = sink_flag;
393    let suppress_auto_output_bool = suppress_auto_output_flag;
394    let descriptor_expr = if let Some(path) = descriptor_path.as_ref() {
395        quote! { Some(&#path) }
396    } else {
397        quote! { None }
398    };
399    let extensions_expr = if let Some(path) = extensions_path.as_ref() {
400        quote! { &#path }
401    } else {
402        quote! { &[] }
403    };
404    let integer_capabilities_expr = if let Some(path) = integer_capabilities_path.as_ref() {
405        quote! { &#path }
406    } else {
407        quote! { &[] }
408    };
409    let integer_audit_expr = if let Some(path) = integer_audit_path.as_ref() {
410        quote! { Some(&#path) }
411    } else {
412        quote! { None }
413    };
414
415    let builtin_expr = quote! {
416        runmat_builtins::BuiltinFunction::new(
417            #name_str,
418            #summary_tok,
419            #category_tok,
420            "",
421            "",
422            vec![#(#inferred_param_types),*],
423            #inferred_return_type,
424            #type_resolver_expr,
425            #wrapper_ident,
426            #accel_slice,
427            #sink_bool,
428            #suppress_auto_output_bool,
429        )
430        .with_descriptor_option(#descriptor_expr)
431        .with_extensions(#extensions_expr)
432        .with_integer_capabilities(#integer_capabilities_expr)
433        .with_integer_audit(#integer_audit_expr)
434    };
435
436    if binding_variant_lit.is_some()
437        && (category_lit.is_some()
438            || summary_lit.is_some()
439            || keywords_lit.is_some()
440            || errors_lit.is_some()
441            || related_lit.is_some()
442            || introduced_lit.is_some()
443            || status_lit.is_some()
444            || examples_lit.is_some()
445            || !accel_values.is_empty()
446            || type_resolver_path.is_some()
447            || type_resolver_ctx_path.is_some()
448            || descriptor_path.is_some()
449            || extensions_path.is_some()
450            || integer_capabilities_path.is_some()
451            || integer_audit_path.is_some()
452            || sink_flag
453            || suppress_auto_output_flag)
454    {
455        panic!(
456            "catalog-backed runtime bindings may declare only name, binding_variant, and builtin_path"
457        );
458    }
459
460    let doc_expr = quote! {
461        runmat_builtins::BuiltinDoc {
462            name: #name_str,
463            category: #category_opt_tok,
464            summary: #summary_opt_tok,
465            keywords: #keywords_opt_tok,
466            errors: #errors_opt_tok,
467            related: #related_opt_tok,
468            introduced: #introduced_opt_tok,
469            status: #status_opt_tok,
470            examples: #examples_opt_tok,
471        }
472    };
473
474    let builtin_path_lit =
475        builtin_path_lit.expect("runtime_builtin requires `builtin_path = \"...\"`");
476    let builtin_path: syn::Path = syn::parse_str(&builtin_path_lit.value())
477        .expect("runtime_builtin `builtin_path` must be a valid path");
478    let helper_ident = format_ident!("__runmat_wasm_register_builtin_{}", ident);
479    let builtin_expr_helper = builtin_expr.clone();
480    let doc_expr_helper = doc_expr.clone();
481    let (wasm_helper, register_native) = if let Some(variant) = binding_variant_lit {
482        let native_symbol = runmat_builtins::native_binding_symbol(&name_str, &variant.value());
483        let native_symbol = syn::LitStr::new(&native_symbol, proc_macro2::Span::call_site());
484        let native_binding_ident = format_ident!(
485            "__RUNMAT_NATIVE_BINDING_{}",
486            ident.to_string().to_ascii_uppercase()
487        );
488        let binding_expr = quote! {
489            crate::builtin::RuntimeBuiltinBinding::new(
490                runmat_builtins::BuiltinBindingIdentity {
491                    builtin: runmat_builtins::BuiltinCatalogIdentity { name: #name_str },
492                    variant: #variant,
493                },
494                #wrapper_ident,
495            )
496        };
497        (
498            quote! {
499                #[cfg(target_arch = "wasm32")]
500                #[allow(non_snake_case)]
501                pub(crate) fn #helper_ident() {
502                    crate::builtin::wasm_registry::submit(#binding_expr);
503                }
504            },
505            quote! {
506                #[cfg(not(target_arch = "wasm32"))]
507                runmat_builtins::inventory::submit! { #binding_expr }
508                // A package's unit-test harness can link both its cfg(test)
509                // copy and a normal dependency copy of the runtime. Only the
510                // latter should publish the process-wide AOT ABI symbols.
511                #[cfg(all(not(target_arch = "wasm32"), not(test)))]
512                #[export_name = #native_symbol]
513                static #native_binding_ident: crate::builtin::RuntimeBuiltinBinding = #binding_expr;
514            },
515        )
516    } else {
517        (
518            quote! {
519                #[cfg(target_arch = "wasm32")]
520                #[allow(non_snake_case)]
521                pub(crate) fn #helper_ident() {
522                    runmat_builtins::wasm_registry::submit_builtin_function(#builtin_expr_helper);
523                    runmat_builtins::wasm_registry::submit_builtin_doc(#doc_expr_helper);
524                }
525            },
526            quote! {
527                #[cfg(not(target_arch = "wasm32"))]
528                runmat_builtins::inventory::submit! { #builtin_expr }
529                #[cfg(not(target_arch = "wasm32"))]
530                runmat_builtins::inventory::submit! { #doc_expr }
531            },
532        )
533    };
534    append_wasm_block(quote! {
535        #builtin_path::#helper_ident();
536    });
537
538    TokenStream::from(quote! {
539        #[cfg_attr(target_arch = "wasm32", allow(dead_code))]
540        #func
541        #[cfg_attr(target_arch = "wasm32", allow(dead_code))]
542        #wrapper
543        #wasm_helper
544        #register_native
545    })
546}
547
548/// Attribute used to declare a runtime constant.
549///
550/// Example:
551/// ```rust,ignore
552/// use runmat_macros::runtime_constant;
553/// use runmat_value::Value;
554///
555/// #[runtime_constant(name = "pi", value = std::f64::consts::PI)]
556/// const PI_CONSTANT: ();
557/// ```
558///
559/// This registers the constant with the `runmat-builtins` inventory
560/// so the runtime can discover it at start-up.
561#[proc_macro_attribute]
562pub fn runtime_constant(args: TokenStream, input: TokenStream) -> TokenStream {
563    let args = parse_macro_input!(args as AttributeArgs);
564    let mut name_lit: Option<Lit> = None;
565    let mut value_expr: Option<Expr> = None;
566    let mut builtin_path_lit: Option<LitStr> = None;
567
568    for arg in args {
569        match arg {
570            NestedMeta::Meta(Meta::NameValue(MetaNameValue { path, lit, .. })) => {
571                if path.is_ident("name") {
572                    name_lit = Some(lit);
573                } else if path.is_ident("builtin_path") {
574                    if let Lit::Str(ls) = lit {
575                        builtin_path_lit = Some(ls);
576                    } else {
577                        panic!("builtin_path must be a string literal");
578                    }
579                } else {
580                    panic!("Unknown attribute parameter: {}", quote!(#path));
581                }
582            }
583            NestedMeta::Meta(Meta::Path(path)) if path.is_ident("value") => {
584                panic!("value parameter requires assignment: value = expression");
585            }
586            NestedMeta::Lit(lit) => {
587                // This handles the case where value is provided as a literal
588                value_expr = Some(syn::parse_quote!(#lit));
589            }
590            _ => panic!("Invalid attribute syntax"),
591        }
592    }
593
594    let name = match name_lit {
595        Some(Lit::Str(s)) => s.value(),
596        _ => panic!("name parameter must be a string literal"),
597    };
598
599    let value = value_expr.unwrap_or_else(|| {
600        panic!("value parameter is required");
601    });
602
603    let builtin_path_lit =
604        builtin_path_lit.expect("runtime_constant requires `builtin_path = \"...\"` argument");
605    let builtin_path: syn::Path = syn::parse_str(&builtin_path_lit.value())
606        .expect("runtime_constant `builtin_path` must be a valid path");
607    let item = parse_macro_input!(input as syn::Item);
608
609    let constant_expr = quote! {
610        runmat_builtins::Constant {
611            name: #name,
612            value: #value,
613        }
614    };
615
616    let helper_ident = helper_ident_from_name("__runmat_wasm_register_const_", &name);
617    let constant_expr_helper = constant_expr.clone();
618    let wasm_helper = quote! {
619        #[cfg(target_arch = "wasm32")]
620        #[allow(non_snake_case)]
621        pub(crate) fn #helper_ident() {
622            runmat_builtins::wasm_registry::submit_constant(#constant_expr_helper);
623        }
624    };
625    let register_native = quote! {
626        #[cfg(not(target_arch = "wasm32"))]
627        #[allow(non_upper_case_globals)]
628        runmat_builtins::inventory::submit! { #constant_expr }
629    };
630    append_wasm_block(quote! {
631        #builtin_path::#helper_ident();
632    });
633
634    TokenStream::from(quote! {
635        #item
636        #wasm_helper
637        #register_native
638    })
639}
640
641struct RegisterConstantArgs {
642    name: LitStr,
643    value: Expr,
644    builtin_path: LitStr,
645}
646
647impl syn::parse::Parse for RegisterConstantArgs {
648    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
649        let name: LitStr = input.parse()?;
650        input.parse::<syn::Token![,]>()?;
651        let value: Expr = input.parse()?;
652        input.parse::<syn::Token![,]>()?;
653        let builtin_path: LitStr = input.parse()?;
654        if input.peek(syn::Token![,]) {
655            input.parse::<syn::Token![,]>()?;
656        }
657        Ok(RegisterConstantArgs {
658            name,
659            value,
660            builtin_path,
661        })
662    }
663}
664
665#[proc_macro]
666pub fn register_constant(input: TokenStream) -> TokenStream {
667    let RegisterConstantArgs {
668        name,
669        value,
670        builtin_path,
671    } = parse_macro_input!(input as RegisterConstantArgs);
672    let constant_expr = quote! {
673        runmat_builtins::Constant {
674            name: #name,
675            value: #value,
676        }
677    };
678    let helper_ident = helper_ident_from_name("__runmat_wasm_register_const_", &name.value());
679    let builtin_path: syn::Path = syn::parse_str(&builtin_path.value())
680        .expect("register_constant `builtin_path` must be a valid path");
681    let constant_expr_helper = constant_expr.clone();
682    let wasm_helper = quote! {
683        #[cfg(target_arch = "wasm32")]
684        #[allow(non_snake_case)]
685        pub(crate) fn #helper_ident() {
686            runmat_builtins::wasm_registry::submit_constant(#constant_expr_helper);
687        }
688    };
689    append_wasm_block(quote! {
690        #builtin_path::#helper_ident();
691    });
692    TokenStream::from(quote! {
693        #wasm_helper
694        #[cfg(not(target_arch = "wasm32"))]
695        runmat_builtins::inventory::submit! { #constant_expr }
696    })
697}
698
699struct RegisterSpecAttrArgs {
700    spec_expr: Option<Expr>,
701    builtin_path: Option<LitStr>,
702}
703
704impl Parse for RegisterSpecAttrArgs {
705    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
706        let mut spec_expr = None;
707        let mut builtin_path = None;
708        while !input.is_empty() {
709            let ident: syn::Ident = input.parse()?;
710            input.parse::<syn::Token![=]>()?;
711            if ident == "spec" {
712                spec_expr = Some(input.parse()?);
713            } else if ident == "builtin_path" {
714                let lit: LitStr = input.parse()?;
715                builtin_path = Some(lit);
716            } else {
717                return Err(syn::Error::new(ident.span(), "unknown attribute argument"));
718            }
719            if input.peek(syn::Token![,]) {
720                input.parse::<syn::Token![,]>()?;
721            }
722        }
723        Ok(Self {
724            spec_expr,
725            builtin_path,
726        })
727    }
728}
729
730#[proc_macro_attribute]
731pub fn register_gpu_spec(attr: TokenStream, item: TokenStream) -> TokenStream {
732    let args = parse_macro_input!(attr as RegisterSpecAttrArgs);
733    let RegisterSpecAttrArgs {
734        spec_expr,
735        builtin_path,
736    } = args;
737    let item_const = parse_macro_input!(item as ItemConst);
738    let spec_tokens = spec_expr.map(|expr| quote! { #expr }).unwrap_or_else(|| {
739        let ident = &item_const.ident;
740        quote! { #ident }
741    });
742    let spec_for_native = spec_tokens.clone();
743    let builtin_path_lit =
744        builtin_path.expect("register_gpu_spec requires `builtin_path = \"...\"` argument");
745    let builtin_path: syn::Path = syn::parse_str(&builtin_path_lit.value())
746        .expect("register_gpu_spec `builtin_path` must be a valid path");
747    let helper_ident = format_ident!(
748        "__runmat_wasm_register_gpu_spec_{}",
749        item_const.ident.to_string()
750    );
751    let spec_tokens_helper = spec_tokens.clone();
752    let wasm_helper = quote! {
753        #[cfg(target_arch = "wasm32")]
754        #[allow(non_snake_case)]
755        pub(crate) fn #helper_ident() {
756            crate::builtins::common::spec::wasm_registry::submit_gpu_spec(&#spec_tokens_helper);
757        }
758    };
759    append_wasm_block(quote! {
760        #builtin_path::#helper_ident();
761    });
762    let expanded = quote! {
763        #[cfg_attr(target_arch = "wasm32", allow(dead_code))]
764        #item_const
765        #wasm_helper
766        #[cfg(not(target_arch = "wasm32"))]
767        inventory::submit! {
768            crate::builtins::common::spec::GpuSpecInventory { spec: &#spec_for_native }
769        }
770    };
771    expanded.into()
772}
773
774#[proc_macro_attribute]
775pub fn register_fusion_spec(attr: TokenStream, item: TokenStream) -> TokenStream {
776    let args = parse_macro_input!(attr as RegisterSpecAttrArgs);
777    let RegisterSpecAttrArgs {
778        spec_expr,
779        builtin_path,
780    } = args;
781    let item_const = parse_macro_input!(item as ItemConst);
782    let spec_tokens = spec_expr.map(|expr| quote! { #expr }).unwrap_or_else(|| {
783        let ident = &item_const.ident;
784        quote! { #ident }
785    });
786    let spec_for_native = spec_tokens.clone();
787    let builtin_path_lit =
788        builtin_path.expect("register_fusion_spec requires `builtin_path = \"...\"` argument");
789    let builtin_path: syn::Path = syn::parse_str(&builtin_path_lit.value())
790        .expect("register_fusion_spec `builtin_path` must be a valid path");
791    let helper_ident = format_ident!(
792        "__runmat_wasm_register_fusion_spec_{}",
793        item_const.ident.to_string()
794    );
795    let spec_tokens_helper = spec_tokens.clone();
796    let wasm_helper = quote! {
797        #[cfg(target_arch = "wasm32")]
798        #[allow(non_snake_case)]
799        pub(crate) fn #helper_ident() {
800            crate::builtins::common::spec::wasm_registry::submit_fusion_spec(&#spec_tokens_helper);
801        }
802    };
803    append_wasm_block(quote! {
804        #builtin_path::#helper_ident();
805    });
806    let expanded = quote! {
807        #[cfg_attr(target_arch = "wasm32", allow(dead_code))]
808        #item_const
809        #wasm_helper
810        #[cfg(not(target_arch = "wasm32"))]
811        inventory::submit! {
812            crate::builtins::common::spec::FusionSpecInventory { spec: &#spec_for_native }
813        }
814    };
815    expanded.into()
816}
817
818fn append_wasm_block(block: proc_macro2::TokenStream) {
819    if !should_generate_wasm_registry() {
820        return;
821    }
822    let path = match wasm_registry_path() {
823        Some(p) => p,
824        None => return,
825    };
826    let _guard = wasm_registry_lock().lock().unwrap();
827    initialize_registry_file(path);
828    let mut contents = fs::read_to_string(path).expect("failed to read wasm registry file");
829    let insertion = format!("    {}\n", block);
830    if let Some(pos) = contents.rfind('}') {
831        contents.insert_str(pos, &insertion);
832    } else {
833        contents.push_str(&insertion);
834        contents.push_str("}\n");
835    }
836    fs::write(path, contents).expect("failed to update wasm registry file");
837}
838
839fn wasm_registry_path() -> Option<&'static PathBuf> {
840    WASM_REGISTRY_PATH
841        .get_or_init(workspace_registry_path)
842        .as_ref()
843}
844
845fn wasm_registry_lock() -> &'static Mutex<()> {
846    WASM_REGISTRY_LOCK.get_or_init(|| Mutex::new(()))
847}
848
849fn initialize_registry_file(path: &Path) {
850    WASM_REGISTRY_INIT.get_or_init(|| {
851        if let Some(parent) = path.parent() {
852            let _ = fs::create_dir_all(parent);
853        }
854        if fs::metadata(path)
855            .map(|metadata| metadata.len() > 0)
856            .unwrap_or(false)
857        {
858            return;
859        }
860        const HEADER: &str = "// @generated by `scripts/regenerate-wasm-registry.sh`\n\
861pub const REGISTRY_COMPLETE: bool = false;\n\
862pub const REGISTRY_SOURCE_FINGERPRINT: &str = \"missing-build-script\";\n\
863pub const REGISTRY_BUILD_CONFIGURATION: &str = \"missing-build-script\";\n\
864pub const REGISTRY_ENTRY_COUNT: usize = 0;\n\n\
865pub fn register_all() {\n}\n";
866        fs::write(path, HEADER).expect("failed to initialize wasm registry file");
867    });
868}
869
870fn should_generate_wasm_registry() -> bool {
871    // Only write to the registry file when explicitly requested. Writing on every
872    // build would keep the included generated source changing and force repeated
873    // runmat-runtime recompiles. The regeneration script sets this together with
874    // RUNMAT_WASM_REGISTRY_OUT so helper calls are written to a temporary file and
875    // copied into place only after cargo succeeds.
876    matches!(
877        std::env::var("RUNMAT_GENERATE_WASM_REGISTRY"),
878        Ok(ref value) if value == "1"
879    )
880}
881
882fn workspace_registry_path() -> Option<PathBuf> {
883    if let Ok(path) = std::env::var("RUNMAT_WASM_REGISTRY_OUT") {
884        return Some(PathBuf::from(path));
885    }
886    let mut dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").ok()?);
887    loop {
888        if dir.join("Cargo.lock").exists() {
889            return Some(
890                dir.join("crates")
891                    .join("runmat-runtime")
892                    .join("src")
893                    .join("builtins")
894                    .join("generated_wasm_registry.rs"),
895            );
896        }
897        if !dir.pop() {
898            return None;
899        }
900    }
901}
902
903fn helper_ident_from_name(prefix: &str, name: &str) -> proc_macro2::Ident {
904    let mut sanitized = String::new();
905    for ch in name.chars() {
906        if ch.is_ascii_alphanumeric() || ch == '_' {
907            sanitized.push(ch);
908        } else {
909            sanitized.push('_');
910        }
911    }
912    format_ident!("{}{}", prefix, sanitized)
913}
914
915/// Smart type inference from Rust types to our enhanced Type enum
916fn infer_builtin_type(ty: &syn::Type) -> proc_macro2::TokenStream {
917    use syn::Type;
918
919    match ty {
920        // Basic primitive types
921        Type::Path(type_path) => {
922            if let Some(ident) = type_path.path.get_ident() {
923                match ident.to_string().as_str() {
924                    "i32" | "i64" | "isize" => quote! { runmat_builtins::Type::Int },
925                    "f32" | "f64" => quote! { runmat_builtins::Type::Num },
926                    "bool" => quote! { runmat_builtins::Type::Bool },
927                    "String" => quote! { runmat_builtins::Type::String },
928                    _ => infer_complex_type(type_path),
929                }
930            } else {
931                infer_complex_type(type_path)
932            }
933        }
934
935        // Reference types like &str, &Value, &Matrix
936        Type::Reference(type_ref) => match type_ref.elem.as_ref() {
937            Type::Path(type_path) => {
938                if let Some(ident) = type_path.path.get_ident() {
939                    match ident.to_string().as_str() {
940                        "str" => quote! { runmat_builtins::Type::String },
941                        _ => infer_builtin_type(&type_ref.elem),
942                    }
943                } else {
944                    infer_builtin_type(&type_ref.elem)
945                }
946            }
947            _ => infer_builtin_type(&type_ref.elem),
948        },
949
950        // Slice types like &[Value], &[f64]
951        Type::Slice(type_slice) => {
952            let element_type = infer_builtin_type(&type_slice.elem);
953            quote! { runmat_builtins::Type::Cell {
954                element_type: Some(Box::new(#element_type)),
955                length: None
956            } }
957        }
958
959        // Array types like [f64; N]
960        Type::Array(type_array) => {
961            let element_type = infer_builtin_type(&type_array.elem);
962            // Try to extract length if it's a literal
963            if let syn::Expr::Lit(expr_lit) = &type_array.len {
964                if let syn::Lit::Int(lit_int) = &expr_lit.lit {
965                    if let Ok(length) = lit_int.base10_parse::<usize>() {
966                        return quote! { runmat_builtins::Type::Cell {
967                            element_type: Some(Box::new(#element_type)),
968                            length: Some(#length)
969                        } };
970                    }
971                }
972            }
973            // Fallback to unknown length
974            quote! { runmat_builtins::Type::Cell {
975                element_type: Some(Box::new(#element_type)),
976                length: None
977            } }
978        }
979
980        // Generic or complex types
981        _ => quote! { runmat_builtins::Type::Unknown },
982    }
983}
984
985/// Infer types for complex path types like Result<T, E>, Option<T>, Matrix, Value
986fn infer_complex_type(type_path: &syn::TypePath) -> proc_macro2::TokenStream {
987    let path_str = quote! { #type_path }.to_string();
988
989    // Handle common patterns
990    if path_str.contains("Matrix") || path_str.contains("Tensor") {
991        quote! { runmat_builtins::Type::tensor() }
992    } else if path_str.contains("Value") {
993        quote! { runmat_builtins::Type::Unknown } // Value can be anything
994    } else if path_str.starts_with("Result") {
995        // Extract the Ok type from Result<T, E>
996        if let syn::PathArguments::AngleBracketed(angle_bracketed) =
997            &type_path.path.segments.last().unwrap().arguments
998        {
999            if let Some(syn::GenericArgument::Type(ty)) = angle_bracketed.args.first() {
1000                return infer_builtin_type(ty);
1001            }
1002        }
1003        quote! { runmat_builtins::Type::Unknown }
1004    } else if path_str.starts_with("Option") {
1005        // Extract the Some type from Option<T>
1006        if let syn::PathArguments::AngleBracketed(angle_bracketed) =
1007            &type_path.path.segments.last().unwrap().arguments
1008        {
1009            if let Some(syn::GenericArgument::Type(ty)) = angle_bracketed.args.first() {
1010                return infer_builtin_type(ty);
1011            }
1012        }
1013        quote! { runmat_builtins::Type::Unknown }
1014    } else if path_str.starts_with("Vec") {
1015        // Extract element type from Vec<T>
1016        if let syn::PathArguments::AngleBracketed(angle_bracketed) =
1017            &type_path.path.segments.last().unwrap().arguments
1018        {
1019            if let Some(syn::GenericArgument::Type(ty)) = angle_bracketed.args.first() {
1020                let element_type = infer_builtin_type(ty);
1021                return quote! { runmat_builtins::Type::Cell {
1022                    element_type: Some(Box::new(#element_type)),
1023                    length: None
1024                } };
1025            }
1026        }
1027        quote! { runmat_builtins::Type::cell() }
1028    } else {
1029        // Unknown type
1030        quote! { runmat_builtins::Type::Unknown }
1031    }
1032}