From c760d12c39f4926c17ed5530784ccee9954de771 Mon Sep 17 00:00:00 2001 From: luaneko Date: Sat, 28 Jun 2025 07:58:52 +1000 Subject: [PATCH] Rewrite stab generation to be more extensible --- crates/luaffi_impl/src/cdef.rs | 6 +- crates/luaffi_impl/src/metatype.rs | 302 +++++++++++++++++++---------- crates/luaffi_impl/src/utils.rs | 16 ++ 3 files changed, 217 insertions(+), 107 deletions(-) diff --git a/crates/luaffi_impl/src/cdef.rs b/crates/luaffi_impl/src/cdef.rs index 6931c24..dc46f08 100644 --- a/crates/luaffi_impl/src/cdef.rs +++ b/crates/luaffi_impl/src/cdef.rs @@ -101,7 +101,7 @@ fn generate_cdef_structure(str: &mut ItemStruct) -> Result { let ffi = ffi_crate(); let ty = &str.ident; - let build = generate_cdef_build(&get_cfields(&mut str.fields)?)?; + let build = generate_cdef_build(&parse_cfields(&mut str.fields)?)?; Ok(quote!( unsafe impl #ffi::Cdef for #ty { @@ -123,7 +123,7 @@ fn generate_cdef_enum(enu: &mut ItemEnum) -> Result { .variants .iter_mut() .map(|variant| { - let build = generate_cdef_build(&get_cfields(&mut variant.fields)?)?; + let build = generate_cdef_build(&parse_cfields(&mut variant.fields)?)?; Ok(quote!(b.inner_struct(|b| { #build }))) }) .collect::>>()?; @@ -148,7 +148,7 @@ struct CFieldAttrs { opaque: bool, } -fn get_cfields(fields: &mut Fields) -> Result> { +fn parse_cfields(fields: &mut Fields) -> Result> { match fields { Fields::Named(fields) => fields.named.iter_mut(), Fields::Unnamed(fields) => fields.unnamed.iter_mut(), diff --git a/crates/luaffi_impl/src/metatype.rs b/crates/luaffi_impl/src/metatype.rs index 326b7bd..5d73d32 100644 --- a/crates/luaffi_impl/src/metatype.rs +++ b/crates/luaffi_impl/src/metatype.rs @@ -1,6 +1,6 @@ use crate::utils::{ - StringLike, ffi_crate, is_optionlike, is_primitivelike, is_stringlike, is_unit, pat_ident, - syn_assert, syn_error, ty_name, + StringLike, ffi_crate, is_optionlike, is_primitivelike, is_resultlike, is_stringlike, is_unit, + pat_ident, syn_assert, syn_error, ty_name, }; use darling::FromMeta; use proc_macro2::TokenStream; @@ -78,7 +78,7 @@ fn generate_impls(_args: &Args, imp: &mut ItemImpl) -> Result { }); // process extern "Lua-C" ffi functions - for func in get_ffi_functions(imp)? { + for func in parse_ffi_functions(imp)? { if let Some(mm) = func.attrs.metamethod { syn_assert!( mms.insert(mm), @@ -91,7 +91,7 @@ fn generate_impls(_args: &Args, imp: &mut ItemImpl) -> Result { } // process extern "Lua" lua functions - for func in get_lua_functions(imp)? { + for func in parse_lua_functions(imp)? { if let Some(mm) = func.attrs.metamethod { syn_assert!( mms.insert(mm), @@ -230,8 +230,8 @@ struct FfiFunctionAttrs { metamethod: Option, } -fn get_ffi_functions(imp: &mut ItemImpl) -> Result> { - let mut funcs = vec![]; +fn parse_ffi_functions(imp: &mut ItemImpl) -> Result> { + let mut parsed = vec![]; for item in imp.items.iter_mut() { if let ImplItem::Fn(func) = item @@ -284,22 +284,19 @@ fn get_ffi_functions(imp: &mut ItemImpl) -> Result> { } } - let attrs = parse_ffi_function_attrs(&mut func.attrs)?; - attrs.metamethod.map(|mm| document_metamethod(func, mm)); - func.sig.asyncness.is_some().then(|| document_async(func)); - document_ffi_function(func); - - funcs.push(FfiFunction { + parsed.push(FfiFunction { name: func.sig.ident.clone(), params, ret, - attrs, + attrs: parse_ffi_function_attrs(&mut func.attrs)?, is_async: func.sig.asyncness.is_some(), }); + + document_ffi_function(func, parsed.last().unwrap()); } } - Ok(funcs) + Ok(parsed) } fn parse_ffi_function_attrs(attrs: &mut Vec) -> Result { @@ -547,8 +544,8 @@ struct LuaFunctionAttrs { metamethod: Option, } -fn get_lua_functions(imp: &mut ItemImpl) -> Result> { - let mut funcs = vec![]; +fn parse_lua_functions(imp: &mut ItemImpl) -> Result> { + let mut parsed = vec![]; for item in imp.items.iter_mut() { if let ImplItem::Fn(func) = item @@ -582,23 +579,36 @@ fn get_lua_functions(imp: &mut ItemImpl) -> Result> { params.push(parse_quote_spanned!(variadic.span() => variadic!())); // luaify builtin macro } - let attrs = parse_lua_function_attrs(&mut func.attrs)?; - attrs.metamethod.map(|mm| document_metamethod(func, mm)); - func.sig.asyncness.is_some().then(|| document_async(func)); - document_lua_function(func); - - funcs.push(LuaFunction { + parsed.push(LuaFunction { name: func.sig.ident.clone(), params, body: func.block.clone(), - attrs, + attrs: parse_lua_function_attrs(&mut func.attrs)?, }); + document_lua_function(func, parsed.last().unwrap()); stub_lua_function(func)?; } } - Ok(funcs) + Ok(parsed) +} + +fn parse_lua_function_attrs(attrs: &mut Vec) -> Result { + let mut parsed = LuaFunctionAttrs::default(); + let mut i = 0; + while let Some(attr) = attrs.get(i) { + if let Some(name) = attr.path().get_ident() + && let Ok(method) = Metamethod::try_from(&name.unraw()) + { + parsed.metamethod = Some(method); + attrs.remove(i); + } else { + i += 1; + } + } + + Ok(parsed) } fn stub_lua_function(func: &mut ImplItemFn) -> Result<()> { @@ -649,23 +659,6 @@ fn stub_lua_function(func: &mut ImplItemFn) -> Result<()> { Ok(()) } -fn parse_lua_function_attrs(attrs: &mut Vec) -> Result { - let mut parsed = LuaFunctionAttrs::default(); - let mut i = 0; - while let Some(attr) = attrs.get(i) { - if let Some(name) = attr.path().get_ident() - && let Ok(method) = Metamethod::try_from(&name.unraw()) - { - parsed.metamethod = Some(method); - attrs.remove(i); - } else { - i += 1; - } - } - - Ok(parsed) -} - fn add_lua_function(registry: &mut Registry, func: &LuaFunction) -> Result<()> { let ffi = ffi_crate(); let luaify = quote!(#ffi::__internal::luaify!); @@ -750,75 +743,176 @@ fn inject_merged_drop(registry: &mut Registry, lua: Option<&LuaFunction>) -> Res Ok(()) } -fn document_ffi_function(func: &mut ImplItemFn) { - func.attrs.insert(0, parse_quote!(#[doc = - r#"FFI"# - ])); +// the transparency makes it work in dark mode too +// const FFI_COLOR: &str = "rgba(255, 222, 118, 0.3)"; // yellow +// const LUA_COLOR: &str = "rgba(188, 222, 255, 0.3)"; // blue +const FALLIBLE_COLOR: &str = "rgba(255, 168, 168, 0.3)"; // red +const ASYNC_COLOR: &str = "rgba(188, 222, 255, 0.3)"; // blue +const METAMETHOD_COLOR: &str = "rgba(255, 222, 118, 0.3)"; // yellow + +struct StabConfig { + text: &'static str, + desc: &'static str, + color: &'static str, + strong: bool, } -fn document_lua_function(func: &mut ImplItemFn) { - func.attrs.insert(0, parse_quote!(#[doc = - r#"Lua"# - ])); +fn generate_stab( + StabConfig { + text, + desc, + color, + strong, + }: StabConfig, +) -> Attribute { + let attrs = vec![ + ("class", "stab".into()), + ("title", desc.into()), + ("aria-label", desc.into()), + ("style", { + let mut style = vec![ + ("float", "right"), + ("margin", "1px"), + ("margin-left", "4px"), + ("padding-left", "5px"), + ("padding-right", "5px"), + ("background", color), + ]; + + if strong { + style.push(("font-weight", "600")); + } + + style + .into_iter() + .map(|(k, v)| format!("{k}: {v}")) + .collect::>() + .join("; ") + }), + ]; + + let attrs = attrs + .into_iter() + .map(|(k, v)| format!(r#"{k}="{v}""#)) + .collect::>() + .join(" "); + + let tag = format!(r#"{text}"#); + parse_quote!(#[doc = #tag]) } -fn document_async(func: &mut ImplItemFn) { - func.attrs.insert(0, parse_quote!(#[doc = - r#"Async"# - ])); +fn document_ffi_function(item: &mut ImplItemFn, func: &FfiFunction) { + let mut attrs = vec![]; + + // attrs.push(generate_stab(StabConfig { + // text: "FFI", + // desc: "This function is implemented in Rust and called via FFI.", + // color: FFI_COLOR, + // strong: true, + // })); + + generate_stab_fallible(item).map(|stab| attrs.push(stab)); + generate_stab_async(item).map(|stab| attrs.push(stab)); + generate_stab_metamethod(func.attrs.metamethod).map(|(stab, help)| { + attrs.push(stab); + item.attrs.push(help); + }); + + item.attrs.splice(0..0, attrs); } -fn document_metamethod(func: &mut ImplItemFn, method: Metamethod) { - func.attrs.insert(0, parse_quote!(#[doc = - r#"Metamethod"# - ])); +fn document_lua_function(item: &mut ImplItemFn, func: &LuaFunction) { + let mut attrs = vec![]; - let doc = match method { - Metamethod::Eq => "This function is a metamethod which is called by the `==` operator.", - Metamethod::Len => "This function is a metamethod which is called by the `#` operator.", - Metamethod::Lt => "This function is a metamethod which is called by the `<` operator.", - Metamethod::Le => "This function is a metamethod which is called by the `<=` operator.", - Metamethod::Concat => "This function is a metamethod which is called by the `..` operator.", - Metamethod::Call => { - "This function is a metamethod which can be called by calling `(...)` on the value directly." - } - Metamethod::Add => "This function is a metamethod which is called by the `+` operator.", - Metamethod::Sub => "This function is a metamethod which is called by the `-` operator.", - Metamethod::Mul => "This function is a metamethod which is called by the `*` operator.", - Metamethod::Div => "This function is a metamethod which is called by the `/` operator.", - Metamethod::Mod => "This function is a metamethod which is called by the `%` operator.", - Metamethod::Pow => "This function is a metamethod which is called by the `^` operator.", - Metamethod::Unm => "This function is a metamethod which is called by the `-` operator.", - Metamethod::ToString => { - "This function is a metamethod which is called by the [`tostring(...)`](https://www.lua.org/manual/5.1/manual.html#pdf-tostring) built-in function." - } - Metamethod::Pairs => { - "This function is a metamethod which is called by the [`pairs(...)`](https://www.lua.org/manual/5.1/manual.html#pdf-pairs) built-in function." - } - Metamethod::Ipairs => { - "This function is a metamethod which is called by the [`ipairs(...)`](https://www.lua.org/manual/5.1/manual.html#pdf-ipairs) built-in function." - } - _ => "This function is a metamethod and cannot be called directly.", - }; + // attrs.push(generate_stab(StabConfig { + // text: "Lua", + // desc: "This function is implemented in Lua.", + // color: LUA_COLOR, + // strong: true, + // })); - func.attrs.push(parse_quote!(#[doc = ""])); - func.attrs.push(parse_quote!(#[doc = "# Metamethod"])); - func.attrs.push(parse_quote!(#[doc = ""])); - func.attrs.push(parse_quote!(#[doc = #doc])); + generate_stab_fallible(item).map(|stab| attrs.push(stab)); + generate_stab_async(item).map(|stab| attrs.push(stab)); + generate_stab_metamethod(func.attrs.metamethod).map(|(stab, help)| { + attrs.push(stab); + item.attrs.push(help); + }); + + item.attrs.splice(0..0, attrs); +} + +fn generate_stab_async(item: &ImplItemFn) -> Option { + item.sig.asyncness.as_ref().map(|_| { + generate_stab(StabConfig { + text: "Async", + desc: "This function is asynchronous and will yield the calling thread.", + color: ASYNC_COLOR, + strong: false, + }) + }) +} + +fn generate_stab_fallible(item: &ImplItemFn) -> Option { + match item.sig.output { + ReturnType::Default => None, + ReturnType::Type(_, ref ty) => is_resultlike(ty).map(|_| { + generate_stab(StabConfig { + text: "Fallible", + desc: "This function is fallible and may throw an error on failure.", + color: FALLIBLE_COLOR, + strong: false, + }) + }), + } +} + +fn generate_stab_metamethod(method: Option) -> Option<(Attribute, Attribute)> { + method.map(|method| { + let stab = generate_stab(StabConfig { + text: "Metamethod", + desc: "This function is a metamethod.", + color: METAMETHOD_COLOR, + strong: false, + }); + + let help = match method { + Metamethod::Eq => "This function is a metamethod which is called by the `==` operator.", + Metamethod::Len => "This function is a metamethod which is called by the `#` operator.", + Metamethod::Lt => "This function is a metamethod which is called by the `<` operator.", + Metamethod::Le => "This function is a metamethod which is called by the `<=` operator.", + Metamethod::Concat => { + "This function is a metamethod which is called by the `..` operator." + } + Metamethod::Call => { + "This function is a metamethod which can be called by calling \ + `(...)` on the value directly." + } + Metamethod::Add => "This function is a metamethod which is called by the `+` operator.", + Metamethod::Sub => "This function is a metamethod which is called by the `-` operator.", + Metamethod::Mul => "This function is a metamethod which is called by the `*` operator.", + Metamethod::Div => "This function is a metamethod which is called by the `/` operator.", + Metamethod::Mod => "This function is a metamethod which is called by the `%` operator.", + Metamethod::Pow => "This function is a metamethod which is called by the `^` operator.", + Metamethod::Unm => "This function is a metamethod which is called by the `-` operator.", + Metamethod::ToString => { + "This function is a metamethod which is called by the \ + [`tostring(...)`](https://www.lua.org/manual/5.1/manual.html#pdf-tostring) \ + built-in function." + } + Metamethod::Pairs => { + "This function is a metamethod which is called by the \ + [`pairs(...)`](https://www.lua.org/manual/5.1/manual.html#pdf-pairs) \ + built-in function." + } + Metamethod::Ipairs => { + "This function is a metamethod which is called by the \ + [`ipairs(...)`](https://www.lua.org/manual/5.1/manual.html#pdf-ipairs) \ + built-in function." + } + _ => "This function is a metamethod and cannot be called directly.", + }; + + let help = format!("\n# Metamethod\n\n{help}"); + (stab, parse_quote!(#[doc = #help])) + }) } diff --git a/crates/luaffi_impl/src/utils.rs b/crates/luaffi_impl/src/utils.rs index edc55ac..60f0ede 100644 --- a/crates/luaffi_impl/src/utils.rs +++ b/crates/luaffi_impl/src/utils.rs @@ -163,3 +163,19 @@ pub fn is_optionlike(ty: &Type) -> Option<&Type> { None } } + +pub fn is_resultlike(ty: &Type) -> Option<&Type> { + if let Type::Path(path) = ty + && path.path.leading_colon.is_none() + && path.path.segments.len() == 1 + && let Some(segment) = path.path.segments.get(0) + && segment.ident == "Result" + && let PathArguments::AngleBracketed(ref angle) = segment.arguments + && angle.args.len() <= 2 // allow Result or Result + && let Some(GenericArgument::Type(ty)) = angle.args.get(0) + { + Some(ty) + } else { + None + } +}