Fix __eq metamethods throwing on type mismatch

This commit is contained in:
lumi 2025-06-28 11:10:29 +10:00
parent db9d611ff7
commit 36300b07d3
Signed by: luaneko
GPG Key ID: 406809B8763FF07A
2 changed files with 131 additions and 63 deletions

View File

@ -458,6 +458,7 @@ pub struct MetatypeFunctionBuilder<'r, 'm> {
cargs: String, // C call arguments
prelude: String, // lua function body prelude
postlude: String, // lua function body postlude
is_eqmm: bool,
}
impl<'r, 'm> MetatypeFunctionBuilder<'r, 'm> {
@ -469,9 +470,15 @@ impl<'r, 'm> MetatypeFunctionBuilder<'r, 'm> {
cargs: String::new(),
prelude: String::new(),
postlude: String::new(),
is_eqmm: false,
}
}
pub fn set_eqmm(&mut self, eq: bool) -> &mut Self {
self.is_eqmm = eq; // are we building an __eq metamethod?
self
}
pub fn param<T: FromFfi>(&mut self, name: impl Display) -> &mut Self {
let Self {
metatype: MetatypeBuilder { reg, .. },
@ -510,6 +517,26 @@ impl<'r, 'm> MetatypeFunctionBuilder<'r, 'm> {
self
}
pub fn param_ctchecked<T: FromFfi, U: Type + Metatype<Target = U>>(
&mut self,
name: impl Display,
) -> &mut Self {
let ct = U::name();
if self.is_eqmm {
// special case for building __eq metamethods which are expected to return false if the
// arguments aren't of the expected type, because otherwise the `==` and `~=` operators
// could throw instead of returning false.
write!(
self.prelude,
r#"if not __istype(__ct.{ct}, {name}) then return false; end; "#
)
} else {
write!(self.prelude, r#"assert(__istype(__ct.{ct}, {name})); "#)
}
.unwrap();
self.param::<T>(name)
}
pub fn param_str(
&mut self,
name: impl Display,
@ -520,7 +547,9 @@ impl<'r, 'm> MetatypeFunctionBuilder<'r, 'm> {
//
// this passes one lua `string` argument as two C `const uint8_t *ptr` and `uintptr_t len`
// arguments, bypassing the slower generic `&[u8]: FromFfi` path which constructs a
// temporary cdata to pass the string and its length in one argument.
// temporary cdata to pass the string and its length in one argument. this is used when the
// #[metatype] macro detects a string-like parameter, currently defined to be &[u8], &str,
// &BStr (from the bstr crate), or those types wrapped in Option<T>.
let Self {
lparams,
cparams,
@ -780,8 +809,7 @@ macro_rules! impl_bigint_intoabi {
fn postlude(ret: &str) -> impl Display {
// this isn't "correct" per se, but it's much more ergonomic to work with numbers in
// lua than with long longs wrapped in cdata. we gracefully accept the loss of
// precision here and that 53 bits of precision for big integers are enough. (the
// vain of Lua 5.3 integer subtype ;D )
// precision here and that 53 bits of precision for big integers are enough.
display!("{ret} = tonumber({ret}); ")
}
}
@ -837,22 +865,6 @@ impl_ptr!(*mut T, true);
impl_ptr!(Option<&T>, false);
impl_ptr!(Option<&mut T>, true);
macro_rules! impl_ptr_annotation {
($ty:ty) => {
impl<T> Annotation for $ty
where
T: Annotation,
{
fn annotation() -> impl Display {
"lightuserdata"
}
}
};
}
impl_ptr_annotation!(*const T);
impl_ptr_annotation!(*mut T);
macro_rules! impl_ref_annotation {
($ty:ty) => {
impl<T> Annotation for $ty

View File

@ -87,7 +87,7 @@ fn generate_impls(_args: &Args, imp: &mut ItemImpl) -> Result<TokenStream> {
);
}
add_ffi_function(&mut registry, &func)?;
generate_ffi_function(&mut registry, &func)?;
}
// process extern "Lua" lua functions
@ -100,10 +100,10 @@ fn generate_impls(_args: &Args, imp: &mut ItemImpl) -> Result<TokenStream> {
);
}
if let Some(Metamethod::Gc) = func.attrs.metamethod {
if func.attrs.metamethod == Some(Metamethod::Gc) {
lua_drop = Some(func);
} else {
add_lua_function(&mut registry, &func)?;
generate_lua_function(&mut registry, &func)?;
}
}
@ -155,6 +155,25 @@ enum Metamethod {
Ipairs,
}
impl Metamethod {
fn narg(&self) -> Option<usize> {
Some(match *self {
Self::Eq
| Self::Lt
| Self::Le
| Self::Concat
| Self::Add
| Self::Sub
| Self::Mul
| Self::Div
| Self::Mod
| Self::Pow => 2,
Self::Gc | Self::Len | Self::Unm | Self::ToString | Self::Pairs | Self::Ipairs => 1,
Self::Call | Self::New => return None,
})
}
}
impl TryFrom<&Ident> for Metamethod {
type Error = Error;
@ -284,11 +303,23 @@ fn parse_ffi_functions(imp: &mut ItemImpl) -> Result<Vec<FfiFunction>> {
}
}
let attrs = parse_ffi_function_attrs(&mut func.attrs)?;
if let Some(mm) = attrs.metamethod
&& let Some(narg) = mm.narg()
{
syn_assert!(
params.len() == narg,
func.sig.inputs,
"expected {narg} parameters for `{mm}` metamethod"
);
}
parsed.push(FfiFunction {
name: func.sig.ident.clone(),
params,
ret,
attrs: parse_ffi_function_attrs(&mut func.attrs)?,
attrs,
is_async: func.sig.asyncness.is_some(),
});
@ -304,14 +335,10 @@ fn parse_ffi_function_attrs(attrs: &mut Vec<Attribute>) -> Result<FfiFunctionAtt
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())
&& let Ok(mm) = Metamethod::try_from(&name.unraw())
{
match method {
Metamethod::Gc => syn_error!(attr, "implement `Drop` instead"),
_ => {}
}
parsed.metamethod = Some(method);
syn_assert!(mm != Metamethod::Gc, attr, "implement `Drop` instead");
parsed.metamethod = Some(mm);
attrs.remove(i);
} else {
i += 1;
@ -370,7 +397,7 @@ fn get_ffi_ret_type(ty: &Type) -> FfiReturnType {
}
}
fn add_ffi_function(registry: &mut Registry, func: &FfiFunction) -> Result<()> {
fn generate_ffi_function(registry: &mut Registry, func: &FfiFunction) -> Result<()> {
let ffi = ffi_crate();
let ty = &registry.ty;
let func_name = &func.name;
@ -389,13 +416,23 @@ fn add_ffi_function(registry: &mut Registry, func: &FfiFunction) -> Result<()> {
let mut asserts = vec![]; // compile-time asserts
let mut build = vec![]; // ffi builder body
// for __new metamethods, ignore the first argument (ctype of self, for which there is no
// equivalent in C)
if func.attrs.metamethod == Some(Metamethod::New) {
match func.attrs.metamethod {
Some(Metamethod::New) => {
// for __new metamethods, ignore the first argument (ctype of self, for which there is
// no equivalent in C)
build.push(quote!(
b.param_ignored();
));
}
Some(Metamethod::Eq) => {
// for __eq metamethods, set special eq mode to return false on argument type mismatch
// instead of throwing
build.push(quote!(
b.set_eqmm(true);
));
}
_ => {}
}
for (i, (name, func_param)) in func_params.iter().enumerate() {
let span = func_param.span();
@ -410,9 +447,18 @@ fn add_ffi_function(registry: &mut Registry, func: &FfiFunction) -> Result<()> {
func_args.push(quote_spanned!(span =>
<#func_param as #ffi::FromFfi>::convert(#shim_param)
));
build.push(quote_spanned!(span =>
build.push(if func.attrs.metamethod == Some(Metamethod::Eq) {
quote_spanned!(span =>
// preliminary `return false` for type mismatch in __eq mode. we just check
// against the ctype of Self because that's what __eq should be comparing
// anyway.
b.param_ctchecked::<#func_param, Self>(#name);
)
} else {
quote_spanned!(span =>
b.param::<#func_param>(#name);
));
)
});
}
ty @ (FfiParameterType::StringLike(str) | FfiParameterType::OptionStringLike(str)) => {
let shim_param_len = format_ident!("arg{i}_len");
@ -494,19 +540,21 @@ fn add_ffi_function(registry: &mut Registry, func: &FfiFunction) -> Result<()> {
}
};
build.push({
build.push(if func.is_async {
// we can't name the future from an async function, so instead we provide a closure with a
// "dummy call" that never gets called so that the builder can infer the return type from
// the closure
let infer_args = iter::repeat_n(quote!(::std::unreachable!()), func_params.len());
let infer = if func.is_async {
quote!(|| #ffi::future::lua_future::new(Self::#func_name(#(#infer_args),*)))
quote!(b.call_inferred(#c_name, || {
#ffi::future::lua_future::new(Self::#func_name(#(#infer_args),*))
});)
} else {
quote!(|| Self::#func_name(#(#infer_args),*))
};
quote!(b.call_inferred(#c_name, #infer);)
quote!(b.call::<#func_ret>(#c_name);)
});
registry.build.push(quote!(#(::std::assert!(#asserts);)*));
registry.build.push(match func.attrs.metamethod {
Some(ref mm) => quote!(b.metatable(#mm, |b| { #(#build)* });),
Some(mm) => quote!(b.metatable(#mm, |b| { #(#build)* });),
None => quote!(b.index(#lua_name, |b| { #(#build)* });),
});
@ -576,14 +624,28 @@ fn parse_lua_functions(imp: &mut ItemImpl) -> Result<Vec<LuaFunction>> {
.collect();
if let Some(ref variadic) = func.sig.variadic {
params.push(parse_quote_spanned!(variadic.span() => variadic!())); // luaify builtin macro
// need to transform variadic to the `varidic!()` luaify builtin macro because we
// luaify a closure below, and closures don't have variadics.
params.push(parse_quote_spanned!(variadic.span() => variadic!()));
}
let attrs = parse_lua_function_attrs(&mut func.attrs)?;
if let Some(mm) = attrs.metamethod
&& let Some(narg) = mm.narg()
{
syn_assert!(
params.len() == narg,
func.sig.inputs,
"expected {narg} parameters for `{mm}` metamethod"
);
}
parsed.push(LuaFunction {
name: func.sig.ident.clone(),
params,
body: func.block.clone(),
attrs: parse_lua_function_attrs(&mut func.attrs)?,
attrs,
});
document_lua_function(func, parsed.last().unwrap());
@ -599,9 +661,9 @@ fn parse_lua_function_attrs(attrs: &mut Vec<Attribute>) -> Result<LuaFunctionAtt
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())
&& let Ok(mm) = Metamethod::try_from(&name.unraw())
{
parsed.metamethod = Some(method);
parsed.metamethod = Some(mm);
attrs.remove(i);
} else {
i += 1;
@ -659,7 +721,7 @@ fn stub_lua_function(func: &mut ImplItemFn) -> Result<()> {
Ok(())
}
fn add_lua_function(registry: &mut Registry, func: &LuaFunction) -> Result<()> {
fn generate_lua_function(registry: &mut Registry, func: &LuaFunction) -> Result<()> {
let ffi = ffi_crate();
let luaify = quote!(#ffi::__internal::luaify!);
let func_name = &func.name;
@ -668,7 +730,7 @@ fn add_lua_function(registry: &mut Registry, func: &LuaFunction) -> Result<()> {
let name = func_name.unraw().to_string();
registry.build.push(match func.attrs.metamethod {
Some(ref mm) => quote!(b.metatable_raw(#mm, #luaify(|#(#params),*| #body));),
Some(mm) => quote!(b.metatable_raw(#mm, #luaify(|#(#params),*| #body));),
None => quote!(b.index_raw(#name, #luaify(|#(#params),*| #body));),
});
@ -698,12 +760,6 @@ fn inject_merged_drop(registry: &mut Registry, lua: Option<&LuaFunction>) -> Res
let c_name_str = c_name.to_string();
if let Some(lua) = lua {
syn_assert!(
lua.params.len() == 1,
lua.name,
"finaliser must take exactly one parameter"
);
let param = pat_ident(&lua.params[0])?;
let body = &lua.body;
@ -866,8 +922,8 @@ fn generate_stab_fallible(item: &ImplItemFn) -> Option<Attribute> {
}
}
fn generate_stab_metamethod(method: Option<Metamethod>) -> Option<(Attribute, Attribute)> {
method.map(|method| {
fn generate_stab_metamethod(mm: Option<Metamethod>) -> Option<(Attribute, Attribute)> {
mm.map(|mm| {
let stab = generate_stab(StabConfig {
text: "Metamethod",
desc: "This function is a metamethod.",
@ -875,7 +931,7 @@ fn generate_stab_metamethod(method: Option<Metamethod>) -> Option<(Attribute, At
strong: false,
});
let help = match method {
let help = match mm {
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.",