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#[proc_macro_attribute]
31pub fn runtime_builtin(args: TokenStream, input: TokenStream) -> TokenStream {
32 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 }
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 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 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 let inferred_param_types: Vec<proc_macro2::TokenStream> =
224 param_types.iter().map(infer_builtin_type).collect();
225
226 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 let is_last_variadic = param_types
234 .last()
235 .map(|ty| {
236 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 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 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 let last_ident = ¶m_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 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 #[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#[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 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 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
915fn infer_builtin_type(ty: &syn::Type) -> proc_macro2::TokenStream {
917 use syn::Type;
918
919 match ty {
920 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 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 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 Type::Array(type_array) => {
961 let element_type = infer_builtin_type(&type_array.elem);
962 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 quote! { runmat_builtins::Type::Cell {
975 element_type: Some(Box::new(#element_type)),
976 length: None
977 } }
978 }
979
980 _ => quote! { runmat_builtins::Type::Unknown },
982 }
983}
984
985fn infer_complex_type(type_path: &syn::TypePath) -> proc_macro2::TokenStream {
987 let path_str = quote! { #type_path }.to_string();
988
989 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 } } else if path_str.starts_with("Result") {
995 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 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 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 quote! { runmat_builtins::Type::Unknown }
1031 }
1032}