1use proc_macro::TokenStream;
46use proc_macro2::TokenStream as TokenStream2;
47use quote::{format_ident, quote};
48use syn::{
49 Attribute, DeriveInput, FnArg, ImplItem, ImplItemFn, Item, ItemImpl, Lit, LitStr, Meta,
50 MetaNameValue, ReturnType, Type, parse_macro_input, spanned::Spanned,
51};
52
53#[proc_macro_derive(LuaUserdata, attributes(lua_type_name))]
87pub fn derive_lua_userdata(input: TokenStream) -> TokenStream {
88 let input = parse_macro_input!(input as DeriveInput);
89 let name = &input.ident;
90
91 match &input.data {
96 syn::Data::Struct(_) => {}
97 syn::Data::Enum(_) => {
98 return syn::Error::new(
99 input.ident.span(),
100 "#[derive(LuaUserdata)] only supports structs; got an enum. \
101 Wrap the enum in a newtype struct for now (v1.3 limitation).",
102 )
103 .to_compile_error()
104 .into();
105 }
106 syn::Data::Union(_) => {
107 return syn::Error::new(
108 input.ident.span(),
109 "#[derive(LuaUserdata)] only supports structs; got a union.",
110 )
111 .to_compile_error()
112 .into();
113 }
114 }
115
116 let type_name_override = parse_lua_type_name(&input.attrs);
118
119 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
138
139 let type_name_body = match type_name_override {
140 Some(lit) => quote! { #lit },
141 None => {
142 let n = name.to_string();
143 quote! { #n }
144 }
145 };
146
147 let expanded = quote! {
148 impl #impl_generics ::luna_core::vm::LuaUserdata for #name #ty_generics #where_clause {
149 fn type_name() -> &'static str { #type_name_body }
150 fn add_methods<__M: ::luna_core::vm::UserdataMethods<Self>>(__m: &mut __M) {
151 <Self>::__luna_userdata_register(__m);
152 }
153 }
154 };
155 expanded.into()
156}
157
158fn parse_lua_type_name(attrs: &[Attribute]) -> Option<LitStr> {
159 for attr in attrs {
160 if !attr.path().is_ident("lua_type_name") {
161 continue;
162 }
163 if let Meta::NameValue(MetaNameValue {
164 value: syn::Expr::Lit(syn::ExprLit {
165 lit: Lit::Str(s), ..
166 }),
167 ..
168 }) = &attr.meta
169 {
170 return Some(s.clone());
171 }
172 }
173 None
174}
175
176#[proc_macro_attribute]
195pub fn lua_userdata_methods(_attr: TokenStream, item: TokenStream) -> TokenStream {
196 let mut input = parse_macro_input!(item as ItemImpl);
197 let self_ty = &*input.self_ty;
198
199 let mut registrations: Vec<TokenStream2> = Vec::new();
200 let mut errors: Vec<syn::Error> = Vec::new();
201
202 for impl_item in &mut input.items {
203 if let ImplItem::Fn(method) = impl_item {
204 match classify_method(method) {
205 Ok(MethodKind::Skip) => {
206 strip_helper_attrs(&mut method.attrs);
207 }
208 Ok(MethodKind::Register(reg)) => {
209 strip_helper_attrs(&mut method.attrs);
210 registrations.push(reg.emit(self_ty));
211 }
212 Ok(MethodKind::Plain) => {
213 }
215 Err(e) => errors.push(e),
216 }
217 }
218 }
219
220 if !errors.is_empty() {
221 let combined = errors
222 .into_iter()
223 .map(|e| e.to_compile_error())
224 .collect::<TokenStream2>();
225 return combined.into();
226 }
227
228 let register_fn = quote! {
231 #[doc(hidden)]
232 #[allow(non_snake_case)]
233 pub fn __luna_userdata_register<__M: ::luna_core::vm::UserdataMethods<Self>>(__m: &mut __M) {
234 #(#registrations)*
235 }
236 };
237
238 let register_item: ImplItem = syn::parse2(register_fn).expect("registry fn parse");
240 input.items.push(register_item);
241
242 quote! { #input }.into()
243}
244
245enum MethodKind {
250 Plain,
251 Skip,
252 Register(Registration),
253}
254
255struct Registration {
256 builder_method: &'static str, name: LitStr,
258 fn_ident: syn::Ident,
259 meta_variant: Option<syn::Ident>, has_receiver: bool,
261}
262
263impl Registration {
264 fn emit(&self, self_ty: &Type) -> TokenStream2 {
265 let fn_ident = &self.fn_ident;
266 let lua_name = &self.name;
267 let builder = format_ident!("{}", self.builder_method);
268
269 if let Some(variant) = &self.meta_variant {
274 if self.has_receiver {
276 quote! {
277 __m.#builder(
278 ::luna_core::vm::MetaMethod::#variant,
279 |__vm, __this, __args| <#self_ty>::#fn_ident(__this, __vm, __args),
280 );
281 }
282 } else {
283 quote! {
286 __m.#builder(
287 ::luna_core::vm::MetaMethod::#variant,
288 |__vm, _, __args| <#self_ty>::#fn_ident(__vm, __args),
289 );
290 }
291 }
292 } else if self.builder_method == "add_function" {
293 quote! {
295 __m.#builder(#lua_name, |__vm, __args| <#self_ty>::#fn_ident(__vm, __args));
296 }
297 } else if self.builder_method == "add_field_method_get" {
298 quote! {
300 __m.#builder(#lua_name, |__vm, __this| <#self_ty>::#fn_ident(__this, __vm));
301 }
302 } else {
303 quote! {
306 __m.#builder(#lua_name, |__vm, __this, __args| {
307 <#self_ty>::#fn_ident(__this, __vm, __args)
308 });
309 }
310 }
311 }
312}
313
314fn classify_method(method: &ImplItemFn) -> Result<MethodKind, syn::Error> {
315 let mut found: Option<(&'static str, Option<LitStr>, Option<syn::Ident>)> = None;
316
317 for attr in &method.attrs {
318 let path = attr.path();
319
320 if path.is_ident("lua_skip") {
321 return Ok(MethodKind::Skip);
322 }
323
324 let mut try_simple = |bm: &'static str| -> Result<(), syn::Error> {
325 let name_lit = attr_string_arg_opt(attr)?;
326 found = Some((bm, name_lit, None));
327 Ok(())
328 };
329
330 if path.is_ident("lua_method") {
331 try_simple("add_method")?;
332 } else if path.is_ident("lua_method_mut") {
333 try_simple("add_method_mut")?;
334 } else if path.is_ident("lua_function") {
335 try_simple("add_function")?;
336 } else if path.is_ident("lua_field_get") {
337 try_simple("add_field_method_get")?;
338 } else if path.is_ident("lua_field_set") {
339 try_simple("add_field_method_set")?;
340 } else if path.is_ident("lua_meta_method") {
341 let variant = attr_ident_arg(attr)?;
342 found = Some(("add_meta_method", None, Some(variant)));
343 } else if path.is_ident("lua_meta_method_mut") {
344 let variant = attr_ident_arg(attr)?;
345 found = Some(("add_meta_method_mut", None, Some(variant)));
346 }
347 }
348
349 let (builder_method, name_lit, meta_variant) = match found {
350 Some(t) => t,
351 None => return Ok(MethodKind::Plain),
352 };
353
354 let name = name_lit
356 .unwrap_or_else(|| LitStr::new(&method.sig.ident.to_string(), method.sig.ident.span()));
357
358 let has_receiver = matches!(method.sig.inputs.first(), Some(FnArg::Receiver(_)));
362 let expects_receiver = !matches!(builder_method, "add_function");
363 if expects_receiver && !has_receiver {
364 return Err(syn::Error::new(
365 method.sig.ident.span(),
366 format!(
367 "#[lua_*] attribute lowering to `{}` requires a `&self` or `&mut self` receiver",
368 builder_method
369 ),
370 ));
371 }
372 if !expects_receiver && has_receiver {
373 return Err(syn::Error::new(
374 method.sig.ident.span(),
375 "#[lua_function] must NOT have a `self` receiver — it lowers to a static \
376 `add_function` call. Use #[lua_method] for receiver-bearing methods.",
377 ));
378 }
379
380 if let ReturnType::Default = method.sig.output {
383 return Err(syn::Error::new(
384 method.sig.output.span(),
385 "luna userdata methods must return `Result<R, LuaError>`; got `()`",
386 ));
387 }
388
389 Ok(MethodKind::Register(Registration {
390 builder_method,
391 name,
392 fn_ident: method.sig.ident.clone(),
393 meta_variant,
394 has_receiver,
395 }))
396}
397
398fn attr_string_arg_opt(attr: &Attribute) -> Result<Option<LitStr>, syn::Error> {
401 match &attr.meta {
402 Meta::Path(_) => Ok(None),
403 Meta::List(_) => {
404 let s: LitStr = attr.parse_args().map_err(|e| {
406 syn::Error::new(
407 attr.span(),
408 format!(
409 "expected a single string-literal argument, e.g. \
410 #[lua_method(\"name\")]; got: {e}"
411 ),
412 )
413 })?;
414 Ok(Some(s))
415 }
416 Meta::NameValue(_) => Err(syn::Error::new(
417 attr.span(),
418 "expected #[lua_method(\"name\")] or bare #[lua_method], \
419 not #[lua_method = \"...\"]",
420 )),
421 }
422}
423
424fn attr_ident_arg(attr: &Attribute) -> Result<syn::Ident, syn::Error> {
426 attr.parse_args().map_err(|e| {
427 syn::Error::new(
428 attr.span(),
429 format!(
430 "expected a single MetaMethod ident, e.g. #[lua_meta_method(Add)]; \
431 got: {e}"
432 ),
433 )
434 })
435}
436
437fn strip_helper_attrs(attrs: &mut Vec<Attribute>) {
438 attrs.retain(|a| {
439 let p = a.path();
440 !(p.is_ident("lua_method")
441 || p.is_ident("lua_method_mut")
442 || p.is_ident("lua_function")
443 || p.is_ident("lua_field_get")
444 || p.is_ident("lua_field_set")
445 || p.is_ident("lua_meta_method")
446 || p.is_ident("lua_meta_method_mut")
447 || p.is_ident("lua_skip"))
448 });
449}
450
451#[allow(dead_code)]
454fn _reserved(_: Item) {}
455
456#[cfg(test)]
469mod tests {
470 use super::*;
471 use syn::parse_quote;
472
473 #[test]
476 fn parse_lua_type_name_extracts_override() {
477 let with_override: DeriveInput = parse_quote! {
478 #[lua_type_name = "MyType"]
479 struct Foo;
480 };
481 let got = parse_lua_type_name(&with_override.attrs);
482 let lit = got.expect("override missing");
483 assert_eq!(lit.value(), "MyType");
484
485 let no_override: DeriveInput = parse_quote! {
486 struct Bar;
487 };
488 assert!(
489 parse_lua_type_name(&no_override.attrs).is_none(),
490 "absent #[lua_type_name] must return None"
491 );
492 }
493
494 #[test]
496 fn parse_lua_type_name_ignores_unrelated_attrs() {
497 let input: DeriveInput = parse_quote! {
498 #[derive(Clone)]
499 #[doc = "unrelated"]
500 struct Baz;
501 };
502 assert!(parse_lua_type_name(&input.attrs).is_none());
503 }
504
505 #[test]
508 fn attr_string_arg_opt_parses_three_forms() {
509 let with_arg: ImplItemFn = parse_quote! {
512 #[lua_method("get")]
513 fn dummy() {}
514 };
515 let lit = attr_string_arg_opt(&with_arg.attrs[0])
516 .expect("parse")
517 .expect("string arg present");
518 assert_eq!(lit.value(), "get");
519
520 let bare: ImplItemFn = parse_quote! {
521 #[lua_method]
522 fn dummy() {}
523 };
524 let none = attr_string_arg_opt(&bare.attrs[0]).expect("parse");
525 assert!(none.is_none(), "bare #[lua_method] must give None");
526
527 let eq_form: ImplItemFn = parse_quote! {
529 #[lua_method = "x"]
530 fn dummy() {}
531 };
532 let err = match attr_string_arg_opt(&eq_form.attrs[0]) {
533 Err(e) => e,
534 Ok(_) => panic!("`=` form must be rejected"),
535 };
536 assert!(
537 err.to_string().contains("#[lua_method"),
538 "err msg should mention the attribute, got {err}"
539 );
540 }
541
542 #[test]
544 fn attr_ident_arg_parses_meta_method_variant() {
545 let method: ImplItemFn = parse_quote! {
546 #[lua_meta_method(Add)]
547 fn dummy() {}
548 };
549 let id = attr_ident_arg(&method.attrs[0]).expect("parse Add");
550 assert_eq!(id.to_string(), "Add");
551
552 let bare: ImplItemFn = parse_quote! {
554 #[lua_meta_method]
555 fn dummy() {}
556 };
557 assert!(
558 attr_ident_arg(&bare.attrs[0]).is_err(),
559 "missing ident arg must error"
560 );
561 }
562
563 #[test]
567 fn classify_method_handles_skip_plain_and_register() {
568 let skipped: ImplItemFn = parse_quote! {
569 #[lua_skip]
570 fn helper(&self) -> i64 { 0 }
571 };
572 assert!(matches!(
573 classify_method(&skipped).expect("parse"),
574 MethodKind::Skip
575 ));
576
577 let plain: ImplItemFn = parse_quote! {
578 fn helper(&self) -> i64 { 0 }
579 };
580 assert!(matches!(
581 classify_method(&plain).expect("parse"),
582 MethodKind::Plain
583 ));
584
585 let with_method: ImplItemFn = parse_quote! {
586 #[lua_method("get")]
587 fn get(&self, _vm: &mut Vm, _args: ()) -> Result<i64, LuaError> { Ok(0) }
588 };
589 match classify_method(&with_method).expect("classify") {
590 MethodKind::Register(reg) => {
591 assert_eq!(reg.builder_method, "add_method");
592 assert_eq!(reg.name.value(), "get");
593 assert!(reg.has_receiver);
594 assert!(reg.meta_variant.is_none());
595 }
596 _ => panic!("expected Register"),
597 }
598 }
599
600 #[test]
603 fn classify_method_rejects_method_without_receiver() {
604 let bad: ImplItemFn = parse_quote! {
605 #[lua_method("oops")]
606 fn no_self(_vm: &mut Vm, _args: ()) -> Result<i64, LuaError> { Ok(0) }
607 };
608 let err = match classify_method(&bad) {
609 Err(e) => e,
610 Ok(_) => panic!("missing self must error"),
611 };
612 assert!(
613 err.to_string().contains("receiver"),
614 "err msg should mention receiver requirement, got: {err}"
615 );
616 }
617
618 #[test]
621 fn classify_method_rejects_function_with_receiver() {
622 let bad: ImplItemFn = parse_quote! {
623 #[lua_function("oops")]
624 fn has_self(&self, _vm: &mut Vm, _args: ()) -> Result<i64, LuaError> { Ok(0) }
625 };
626 let err = match classify_method(&bad) {
627 Err(e) => e,
628 Ok(_) => panic!("self on lua_function must error"),
629 };
630 assert!(
631 err.to_string().contains("self"),
632 "err msg should mention 'self', got: {err}"
633 );
634 }
635
636 #[test]
638 fn classify_method_rejects_unit_return() {
639 let bad: ImplItemFn = parse_quote! {
640 #[lua_method("bad")]
641 fn no_ret(&self, _vm: &mut Vm, _args: ()) {}
642 };
643 let err = match classify_method(&bad) {
644 Err(e) => e,
645 Ok(_) => panic!("unit return must error"),
646 };
647 assert!(
648 err.to_string().contains("Result"),
649 "err msg should mention 'Result', got: {err}"
650 );
651 }
652
653 #[test]
656 fn classify_method_defaults_name_to_fn_ident() {
657 let m: ImplItemFn = parse_quote! {
658 #[lua_method]
659 fn width(&self, _vm: &mut Vm, _args: ()) -> Result<i64, LuaError> { Ok(0) }
660 };
661 match classify_method(&m).expect("classify") {
662 MethodKind::Register(reg) => {
663 assert_eq!(
664 reg.name.value(),
665 "width",
666 "default name must match fn ident"
667 );
668 }
669 _ => panic!("expected Register"),
670 }
671 }
672
673 #[test]
676 fn classify_method_meta_method_path() {
677 let m: ImplItemFn = parse_quote! {
678 #[lua_meta_method(Add)]
679 fn __add(&self, _vm: &mut Vm, _args: ()) -> Result<i64, LuaError> { Ok(0) }
680 };
681 match classify_method(&m).expect("classify") {
682 MethodKind::Register(reg) => {
683 assert_eq!(reg.builder_method, "add_meta_method");
684 assert_eq!(
685 reg.meta_variant.as_ref().map(|i| i.to_string()).as_deref(),
686 Some("Add")
687 );
688 }
689 _ => panic!("expected Register"),
690 }
691 }
692
693 #[test]
696 fn strip_helper_attrs_removes_only_helpers() {
697 let mut m: ImplItemFn = parse_quote! {
698 #[lua_method("get")]
699 #[inline]
700 #[lua_skip]
701 #[doc = "kept"]
702 fn dummy() {}
703 };
704 assert_eq!(m.attrs.len(), 4);
705 strip_helper_attrs(&mut m.attrs);
706 assert_eq!(m.attrs.len(), 2);
708 let keep_paths: Vec<String> = m
709 .attrs
710 .iter()
711 .map(|a| {
712 a.path()
713 .get_ident()
714 .map(|i| i.to_string())
715 .unwrap_or_default()
716 })
717 .collect();
718 assert!(
719 keep_paths.contains(&"inline".to_string()),
720 "kept attrs: {keep_paths:?}"
721 );
722 assert!(
723 keep_paths.contains(&"doc".to_string()),
724 "kept attrs: {keep_paths:?}"
725 );
726 }
727}