use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::{
Attribute, DeriveInput, FnArg, ImplItem, ImplItemFn, Item, ItemImpl, Lit, LitStr, Meta,
MetaNameValue, ReturnType, Type, parse_macro_input, spanned::Spanned,
};
#[proc_macro_derive(LuaUserdata, attributes(lua_type_name))]
pub fn derive_lua_userdata(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
match &input.data {
syn::Data::Struct(_) => {}
syn::Data::Enum(_) => {
return syn::Error::new(
input.ident.span(),
"#[derive(LuaUserdata)] only supports structs; got an enum. \
Wrap the enum in a newtype struct for now (v1.3 limitation).",
)
.to_compile_error()
.into();
}
syn::Data::Union(_) => {
return syn::Error::new(
input.ident.span(),
"#[derive(LuaUserdata)] only supports structs; got a union.",
)
.to_compile_error()
.into();
}
}
let type_name_override = parse_lua_type_name(&input.attrs);
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let type_name_body = match type_name_override {
Some(lit) => quote! { #lit },
None => {
let n = name.to_string();
quote! { #n }
}
};
let expanded = quote! {
impl #impl_generics ::luna_core::vm::LuaUserdata for #name #ty_generics #where_clause {
fn type_name() -> &'static str { #type_name_body }
fn add_methods<__M: ::luna_core::vm::UserdataMethods<Self>>(__m: &mut __M) {
<Self>::__luna_userdata_register(__m);
}
}
};
expanded.into()
}
fn parse_lua_type_name(attrs: &[Attribute]) -> Option<LitStr> {
for attr in attrs {
if !attr.path().is_ident("lua_type_name") {
continue;
}
if let Meta::NameValue(MetaNameValue {
value: syn::Expr::Lit(syn::ExprLit {
lit: Lit::Str(s), ..
}),
..
}) = &attr.meta
{
return Some(s.clone());
}
}
None
}
#[proc_macro_attribute]
pub fn lua_userdata_methods(_attr: TokenStream, item: TokenStream) -> TokenStream {
let mut input = parse_macro_input!(item as ItemImpl);
let self_ty = &*input.self_ty;
let mut registrations: Vec<TokenStream2> = Vec::new();
let mut errors: Vec<syn::Error> = Vec::new();
for impl_item in &mut input.items {
if let ImplItem::Fn(method) = impl_item {
match classify_method(method) {
Ok(MethodKind::Skip) => {
strip_helper_attrs(&mut method.attrs);
}
Ok(MethodKind::Register(reg)) => {
strip_helper_attrs(&mut method.attrs);
registrations.push(reg.emit(self_ty));
}
Ok(MethodKind::Plain) => {
}
Err(e) => errors.push(e),
}
}
}
if !errors.is_empty() {
let combined = errors
.into_iter()
.map(|e| e.to_compile_error())
.collect::<TokenStream2>();
return combined.into();
}
let register_fn = quote! {
#[doc(hidden)]
#[allow(non_snake_case)]
pub fn __luna_userdata_register<__M: ::luna_core::vm::UserdataMethods<Self>>(__m: &mut __M) {
#(#registrations)*
}
};
let register_item: ImplItem = syn::parse2(register_fn).expect("registry fn parse");
input.items.push(register_item);
quote! { #input }.into()
}
enum MethodKind {
Plain,
Skip,
Register(Registration),
}
struct Registration {
builder_method: &'static str, name: LitStr,
fn_ident: syn::Ident,
meta_variant: Option<syn::Ident>, has_receiver: bool,
}
impl Registration {
fn emit(&self, self_ty: &Type) -> TokenStream2 {
let fn_ident = &self.fn_ident;
let lua_name = &self.name;
let builder = format_ident!("{}", self.builder_method);
if let Some(variant) = &self.meta_variant {
if self.has_receiver {
quote! {
__m.#builder(
::luna_core::vm::MetaMethod::#variant,
|__vm, __this, __args| <#self_ty>::#fn_ident(__this, __vm, __args),
);
}
} else {
quote! {
__m.#builder(
::luna_core::vm::MetaMethod::#variant,
|__vm, _, __args| <#self_ty>::#fn_ident(__vm, __args),
);
}
}
} else if self.builder_method == "add_function" {
quote! {
__m.#builder(#lua_name, |__vm, __args| <#self_ty>::#fn_ident(__vm, __args));
}
} else if self.builder_method == "add_field_method_get" {
quote! {
__m.#builder(#lua_name, |__vm, __this| <#self_ty>::#fn_ident(__this, __vm));
}
} else {
quote! {
__m.#builder(#lua_name, |__vm, __this, __args| {
<#self_ty>::#fn_ident(__this, __vm, __args)
});
}
}
}
}
fn classify_method(method: &ImplItemFn) -> Result<MethodKind, syn::Error> {
let mut found: Option<(&'static str, Option<LitStr>, Option<syn::Ident>)> = None;
for attr in &method.attrs {
let path = attr.path();
if path.is_ident("lua_skip") {
return Ok(MethodKind::Skip);
}
let mut try_simple = |bm: &'static str| -> Result<(), syn::Error> {
let name_lit = attr_string_arg_opt(attr)?;
found = Some((bm, name_lit, None));
Ok(())
};
if path.is_ident("lua_method") {
try_simple("add_method")?;
} else if path.is_ident("lua_method_mut") {
try_simple("add_method_mut")?;
} else if path.is_ident("lua_function") {
try_simple("add_function")?;
} else if path.is_ident("lua_field_get") {
try_simple("add_field_method_get")?;
} else if path.is_ident("lua_field_set") {
try_simple("add_field_method_set")?;
} else if path.is_ident("lua_meta_method") {
let variant = attr_ident_arg(attr)?;
found = Some(("add_meta_method", None, Some(variant)));
} else if path.is_ident("lua_meta_method_mut") {
let variant = attr_ident_arg(attr)?;
found = Some(("add_meta_method_mut", None, Some(variant)));
}
}
let (builder_method, name_lit, meta_variant) = match found {
Some(t) => t,
None => return Ok(MethodKind::Plain),
};
let name = name_lit
.unwrap_or_else(|| LitStr::new(&method.sig.ident.to_string(), method.sig.ident.span()));
let has_receiver = matches!(method.sig.inputs.first(), Some(FnArg::Receiver(_)));
let expects_receiver = !matches!(builder_method, "add_function");
if expects_receiver && !has_receiver {
return Err(syn::Error::new(
method.sig.ident.span(),
format!(
"#[lua_*] attribute lowering to `{}` requires a `&self` or `&mut self` receiver",
builder_method
),
));
}
if !expects_receiver && has_receiver {
return Err(syn::Error::new(
method.sig.ident.span(),
"#[lua_function] must NOT have a `self` receiver — it lowers to a static \
`add_function` call. Use #[lua_method] for receiver-bearing methods.",
));
}
if let ReturnType::Default = method.sig.output {
return Err(syn::Error::new(
method.sig.output.span(),
"luna userdata methods must return `Result<R, LuaError>`; got `()`",
));
}
Ok(MethodKind::Register(Registration {
builder_method,
name,
fn_ident: method.sig.ident.clone(),
meta_variant,
has_receiver,
}))
}
fn attr_string_arg_opt(attr: &Attribute) -> Result<Option<LitStr>, syn::Error> {
match &attr.meta {
Meta::Path(_) => Ok(None),
Meta::List(_) => {
let s: LitStr = attr.parse_args().map_err(|e| {
syn::Error::new(
attr.span(),
format!(
"expected a single string-literal argument, e.g. \
#[lua_method(\"name\")]; got: {e}"
),
)
})?;
Ok(Some(s))
}
Meta::NameValue(_) => Err(syn::Error::new(
attr.span(),
"expected #[lua_method(\"name\")] or bare #[lua_method], \
not #[lua_method = \"...\"]",
)),
}
}
fn attr_ident_arg(attr: &Attribute) -> Result<syn::Ident, syn::Error> {
attr.parse_args().map_err(|e| {
syn::Error::new(
attr.span(),
format!(
"expected a single MetaMethod ident, e.g. #[lua_meta_method(Add)]; \
got: {e}"
),
)
})
}
fn strip_helper_attrs(attrs: &mut Vec<Attribute>) {
attrs.retain(|a| {
let p = a.path();
!(p.is_ident("lua_method")
|| p.is_ident("lua_method_mut")
|| p.is_ident("lua_function")
|| p.is_ident("lua_field_get")
|| p.is_ident("lua_field_set")
|| p.is_ident("lua_meta_method")
|| p.is_ident("lua_meta_method_mut")
|| p.is_ident("lua_skip"))
});
}
#[allow(dead_code)]
fn _reserved(_: Item) {}