diff --git a/src/lua.rs b/src/lua.rs index 8e6a8a2..ebd1260 100644 --- a/src/lua.rs +++ b/src/lua.rs @@ -1857,7 +1857,10 @@ impl Lua { pub fn scope<'lua, 'scope, R>( &'lua self, f: impl FnOnce(&Scope<'lua, 'scope>) -> Result, - ) -> Result { + ) -> Result + where + 'lua: 'scope, + { f(&Scope::new(self)) } @@ -2487,7 +2490,7 @@ impl Lua { unsafe fn register_userdata_metatable<'lua, T: 'static>( &'lua self, - registry: UserDataRegistrar<'lua, T>, + mut registry: UserDataRegistrar<'lua, T>, ) -> Result { let state = self.state(); let _sg = StackGuard::new(state); @@ -2510,7 +2513,7 @@ impl Lua { let mut has_name = false; for (k, f) in registry.meta_fields { has_name = has_name || k == "__name"; - self.push_value(f(self)?)?; + self.push_value(f(self, MultiValue::new())?.pop_front().unwrap())?; rawset_field(state, -2, MetaMethod::validate(&k)?)?; } // Set `__name` if not provided @@ -2523,6 +2526,32 @@ impl Lua { let mut extra_tables_count = 0; + let fields_nrec = registry.fields.len(); + if fields_nrec > 0 { + // If __index is a table then update it inplace + let index_type = ffi::lua_getfield(state, metatable_index, cstr!("__index")); + match index_type { + ffi::LUA_TNIL | ffi::LUA_TTABLE => { + if index_type == ffi::LUA_TNIL { + // Create a new table + ffi::lua_pop(state, 1); + push_table(state, 0, fields_nrec as c_int, true)?; + } + for (k, f) in registry.fields { + self.push_value(f(self, MultiValue::new())?.pop_front().unwrap())?; + rawset_field(state, -2, &k)?; + } + rawset_field(state, metatable_index, "__index")?; + } + _ => { + // Propagate fields to the field getters + for (k, f) in registry.fields { + registry.field_getters.push((k, f)) + } + } + } + } + let mut field_getters_index = None; let field_getters_nrec = registry.field_getters.len(); if field_getters_nrec > 0 { @@ -2552,7 +2581,16 @@ impl Lua { #[cfg(feature = "async")] let methods_nrec = methods_nrec + registry.async_methods.len(); if methods_nrec > 0 { - push_table(state, 0, methods_nrec as c_int, true)?; + // If __index is a table then update it inplace + let index_type = ffi::lua_getfield(state, metatable_index, cstr!("__index")); + match index_type { + ffi::LUA_TTABLE => {} // Update the existing table + _ => { + // Create a new table + ffi::lua_pop(state, 1); + push_table(state, 0, methods_nrec as c_int, true)?; + } + } for (k, m) in registry.methods { self.push_value(Value::Function(self.create_callback(m)?))?; rawset_field(state, -2, &k)?; @@ -2562,8 +2600,18 @@ impl Lua { self.push_value(Value::Function(self.create_async_callback(m)?))?; rawset_field(state, -2, &k)?; } - methods_index = Some(ffi::lua_absindex(state, -1)); - extra_tables_count += 1; + match index_type { + ffi::LUA_TTABLE => { + ffi::lua_pop(state, 1); // All done + } + ffi::LUA_TNIL => { + rawset_field(state, metatable_index, "__index")?; // Set the new table as __index + } + _ => { + methods_index = Some(ffi::lua_absindex(state, -1)); + extra_tables_count += 1; + } + } } init_userdata_metatable::>( diff --git a/src/scope.rs b/src/scope.rs index 7c46413..3d4cb94 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -14,6 +14,7 @@ use crate::types::{Callback, CallbackUpvalue, LuaRef, MaybeSend}; use crate::userdata::{ AnyUserData, MetaMethod, UserData, UserDataCell, UserDataFields, UserDataMethods, }; +use crate::userdata_impl::UserDataRegistrar; use crate::util::{ assert_stack, check_stack, get_userdata, init_userdata_metatable, push_table, rawset_field, take_userdata, StackGuard, @@ -32,7 +33,10 @@ use std::future::Future; /// See [`Lua::scope`] for more details. /// /// [`Lua::scope`]: crate::Lua.html::scope -pub struct Scope<'lua, 'scope> { +pub struct Scope<'lua, 'scope> +where + 'lua: 'scope, +{ lua: &'lua Lua, destructors: RefCell, DestructorCallback<'lua>)>>, _scope_invariant: PhantomData>, @@ -393,7 +397,7 @@ impl<'lua, 'scope> Scope<'lua, 'scope> { rawset_field(state, -2, MetaMethod::validate(&k)?)?; } for (k, f) in ud_fields.meta_fields { - lua.push_value(f(mem::transmute(lua))?)?; + lua.push_value(f(lua, MultiValue::new())?.pop_front().unwrap())?; rawset_field(state, -2, MetaMethod::validate(&k)?)?; } let metatable_index = ffi::lua_absindex(state, -1); @@ -734,15 +738,16 @@ impl<'lua, T: UserData> UserDataMethods<'lua, T> for NonStaticUserDataMethods<'l } struct NonStaticUserDataFields<'lua, T: UserData> { + fields: Vec<(String, Callback<'lua, 'static>)>, field_getters: Vec<(String, NonStaticMethod<'lua, T>)>, field_setters: Vec<(String, NonStaticMethod<'lua, T>)>, - #[allow(clippy::type_complexity)] - meta_fields: Vec<(String, Box Result>>)>, + meta_fields: Vec<(String, Callback<'lua, 'static>)>, } impl<'lua, T: UserData> Default for NonStaticUserDataFields<'lua, T> { fn default() -> NonStaticUserDataFields<'lua, T> { NonStaticUserDataFields { + fields: Vec::new(), field_getters: Vec::new(), field_setters: Vec::new(), meta_fields: Vec::new(), @@ -751,6 +756,17 @@ impl<'lua, T: UserData> Default for NonStaticUserDataFields<'lua, T> { } impl<'lua, T: UserData> UserDataFields<'lua, T> for NonStaticUserDataFields<'lua, T> { + fn add_field(&mut self, name: impl AsRef, value: V) + where + V: IntoLua<'lua> + Clone + 'static, + { + let name = name.as_ref().to_string(); + self.fields.push(( + name, + Box::new(move |lua, _| value.clone().into_lua_multi(lua)), + )); + } + fn add_field_method_get(&mut self, name: impl AsRef, method: M) where M: Fn(&'lua Lua, &T) -> Result + MaybeSend + 'static, @@ -796,30 +812,30 @@ impl<'lua, T: UserData> UserDataFields<'lua, T> for NonStaticUserDataFields<'lua self.field_setters.push((name.as_ref().into(), func)); } + fn add_meta_field(&mut self, name: impl AsRef, value: V) + where + V: IntoLua<'lua> + Clone + 'static, + { + let name = name.as_ref().to_string(); + let name2 = name.clone(); + self.meta_fields.push(( + name, + Box::new(move |lua, _| { + UserDataRegistrar::<()>::check_meta_field(lua, &name2, value.clone()) + }), + )); + } + fn add_meta_field_with(&mut self, name: impl AsRef, f: F) where F: Fn(&'lua Lua) -> Result + MaybeSend + 'static, R: IntoLua<'lua>, { let name = name.as_ref().to_string(); + let name2 = name.clone(); self.meta_fields.push(( - name.clone(), - Box::new(move |lua| { - let value = f(lua)?.into_lua(lua)?; - if name == MetaMethod::Index || name == MetaMethod::NewIndex { - match value { - Value::Nil | Value::Table(_) | Value::Function(_) => {} - _ => { - return Err(Error::MetaMethodTypeError { - method: name.clone(), - type_name: value.type_name(), - message: Some("expected nil, table or function".to_string()), - }) - } - } - } - Ok(value) - }), + name, + Box::new(move |lua, _| UserDataRegistrar::<()>::check_meta_field(lua, &name2, f(lua)?)), )); } } diff --git a/src/userdata.rs b/src/userdata.rs index 68edf67..8215083 100644 --- a/src/userdata.rs +++ b/src/userdata.rs @@ -21,12 +21,10 @@ use crate::function::Function; use crate::lua::Lua; use crate::string::String; use crate::table::{Table, TablePairs}; -use crate::types::{Callback, LuaRef, MaybeSend}; +use crate::types::{LuaRef, MaybeSend}; use crate::util::{check_stack, get_userdata, take_userdata, StackGuard}; use crate::value::{FromLua, FromLuaMulti, IntoLua, IntoLuaMulti, Value}; - -#[cfg(feature = "async")] -use crate::types::AsyncCallback; +use crate::UserDataRegistrar; #[cfg(feature = "lua54")] pub(crate) const USER_VALUE_MAXSLOT: usize = 8; @@ -412,29 +410,26 @@ pub trait UserDataMethods<'lua, T> { // #[doc(hidden)] - fn add_callback(&mut self, _name: StdString, _callback: Callback<'lua, 'static>) {} - - #[doc(hidden)] - #[cfg(feature = "async")] - fn add_async_callback(&mut self, _name: StdString, _callback: AsyncCallback<'lua, 'static>) {} - - #[doc(hidden)] - fn add_meta_callback(&mut self, _name: StdString, _callback: Callback<'lua, 'static>) {} - - #[doc(hidden)] - #[cfg(feature = "async")] - fn add_async_meta_callback( - &mut self, - _name: StdString, - _callback: AsyncCallback<'lua, 'static>, - ) { - } + fn append_methods_from(&mut self, _other: UserDataRegistrar<'lua, S>) {} } /// Field registry for [`UserData`] implementors. /// /// [`UserData`]: crate::UserData pub trait UserDataFields<'lua, T> { + /// Add a static field to the `UserData`. + /// + /// Static fields are implemented by updating the `__index` metamethod and returning the + /// accessed field. This allows them to be used with the expected `userdata.field` syntax. + /// + /// Static fields are usually shared between all instances of the `UserData` of the same type. + /// + /// If `add_meta_method` is used to set the `__index` metamethod, it will + /// be used as a fall-back if no regular field or method are found. + fn add_field(&mut self, name: impl AsRef, value: V) + where + V: IntoLua<'lua> + Clone + 'static; + /// Add a regular field getter as a method which accepts a `&T` as the parameter. /// /// Regular field getters are implemented by overriding the `__index` metamethod and returning the @@ -483,9 +478,21 @@ pub trait UserDataFields<'lua, T> { F: FnMut(&'lua Lua, AnyUserData<'lua>, A) -> Result<()> + MaybeSend + 'static, A: FromLua<'lua>; - /// Add a metamethod value computed from `f`. + /// Add a metatable field. /// - /// This will initialize the metamethod value from `f` on `UserData` creation. + /// This will initialize the metatable field with `value` on `UserData` creation. + /// + /// # Note + /// + /// `mlua` will trigger an error on an attempt to define a protected metamethod, + /// like `__gc` or `__metatable`. + fn add_meta_field(&mut self, name: impl AsRef, value: V) + where + V: IntoLua<'lua> + Clone + 'static; + + /// Add a metatable field computed from `f`. + /// + /// This will initialize the metatable field from `f` on `UserData` creation. /// /// # Note /// @@ -501,10 +508,7 @@ pub trait UserDataFields<'lua, T> { // #[doc(hidden)] - fn add_field_getter(&mut self, _name: StdString, _callback: Callback<'lua, 'static>) {} - - #[doc(hidden)] - fn add_field_setter(&mut self, _name: StdString, _callback: Callback<'lua, 'static>) {} + fn append_fields_from(&mut self, _other: UserDataRegistrar<'lua, S>) {} } /// Trait for custom userdata types. diff --git a/src/userdata_impl.rs b/src/userdata_impl.rs index 7f140ac..363ef63 100644 --- a/src/userdata_impl.rs +++ b/src/userdata_impl.rs @@ -11,7 +11,7 @@ use crate::userdata::{ AnyUserData, MetaMethod, UserData, UserDataCell, UserDataFields, UserDataMethods, }; use crate::util::{check_stack, get_userdata, short_type_name, StackGuard}; -use crate::value::{FromLua, FromLuaMulti, IntoLua, IntoLuaMulti, Value}; +use crate::value::{FromLua, FromLuaMulti, IntoLua, IntoLuaMulti, MultiValue, Value}; #[cfg(not(feature = "send"))] use std::rc::Rc; @@ -25,13 +25,10 @@ use { pub struct UserDataRegistrar<'lua, T: 'static> { // Fields + pub(crate) fields: Vec<(String, Callback<'lua, 'static>)>, pub(crate) field_getters: Vec<(String, Callback<'lua, 'static>)>, pub(crate) field_setters: Vec<(String, Callback<'lua, 'static>)>, - #[allow(clippy::type_complexity)] - pub(crate) meta_fields: Vec<( - String, - Box Result> + 'static>, - )>, + pub(crate) meta_fields: Vec<(String, Callback<'lua, 'static>)>, // Methods pub(crate) methods: Vec<(String, Callback<'lua, 'static>)>, @@ -47,6 +44,7 @@ pub struct UserDataRegistrar<'lua, T: 'static> { impl<'lua, T: 'static> UserDataRegistrar<'lua, T> { pub(crate) const fn new() -> Self { UserDataRegistrar { + fields: Vec::new(), field_getters: Vec::new(), field_setters: Vec::new(), meta_fields: Vec::new(), @@ -360,6 +358,30 @@ impl<'lua, T: 'static> UserDataRegistrar<'lua, T> { ) }) } + + pub(crate) fn check_meta_field( + lua: &'lua Lua, + name: &str, + value: V, + ) -> Result> + where + V: IntoLua<'lua>, + { + let value = value.into_lua(lua)?; + if name == MetaMethod::Index || name == MetaMethod::NewIndex { + match value { + Value::Nil | Value::Table(_) | Value::Function(_) => {} + _ => { + return Err(Error::MetaMethodTypeError { + method: name.to_string(), + type_name: value.type_name(), + message: Some("expected nil, table or function".to_string()), + }) + } + } + } + value.into_lua_multi(lua) + } } // Returns function name for the type `T`, without the module path @@ -368,6 +390,17 @@ fn get_function_name(name: &str) -> StdString { } impl<'lua, T: 'static> UserDataFields<'lua, T> for UserDataRegistrar<'lua, T> { + fn add_field(&mut self, name: impl AsRef, value: V) + where + V: IntoLua<'lua> + Clone + 'static, + { + let name = name.as_ref().to_string(); + self.fields.push(( + name, + Box::new(move |lua, _| value.clone().into_lua_multi(lua)), + )); + } + fn add_field_method_get(&mut self, name: impl AsRef, method: M) where M: Fn(&'lua Lua, &T) -> Result + MaybeSend + 'static, @@ -408,41 +441,38 @@ impl<'lua, T: 'static> UserDataFields<'lua, T> for UserDataRegistrar<'lua, T> { self.field_setters.push((name.into(), func)); } + fn add_meta_field(&mut self, name: impl AsRef, value: V) + where + V: IntoLua<'lua> + Clone + 'static, + { + let name = name.as_ref().to_string(); + let name2 = name.clone(); + self.meta_fields.push(( + name, + Box::new(move |lua, _| Self::check_meta_field(lua, &name2, value.clone())), + )); + } + fn add_meta_field_with(&mut self, name: impl AsRef, f: F) where F: Fn(&'lua Lua) -> Result + MaybeSend + 'static, R: IntoLua<'lua>, { let name = name.as_ref().to_string(); + let name2 = name.clone(); self.meta_fields.push(( - name.clone(), - Box::new(move |lua| { - let value = f(lua)?.into_lua(lua)?; - if name == MetaMethod::Index || name == MetaMethod::NewIndex { - match value { - Value::Nil | Value::Table(_) | Value::Function(_) => {} - _ => { - return Err(Error::MetaMethodTypeError { - method: name.clone(), - type_name: value.type_name(), - message: Some("expected nil, table or function".to_string()), - }) - } - } - } - Ok(value) - }), + name, + Box::new(move |lua, _| Self::check_meta_field(lua, &name2, f(lua)?)), )); } // Below are internal methods - fn add_field_getter(&mut self, name: String, callback: Callback<'lua, 'static>) { - self.field_getters.push((name, callback)); - } - - fn add_field_setter(&mut self, name: String, callback: Callback<'lua, 'static>) { - self.field_setters.push((name, callback)); + fn append_fields_from(&mut self, other: UserDataRegistrar<'lua, S>) { + self.fields.extend(other.fields); + self.field_getters.extend(other.field_getters); + self.field_setters.extend(other.field_setters); + self.meta_fields.extend(other.meta_fields); } } @@ -591,22 +621,13 @@ impl<'lua, T: 'static> UserDataMethods<'lua, T> for UserDataRegistrar<'lua, T> { // Below are internal methods used in generated code - fn add_callback(&mut self, name: String, callback: Callback<'lua, 'static>) { - self.methods.push((name, callback)); - } - - #[cfg(feature = "async")] - fn add_async_callback(&mut self, name: String, callback: AsyncCallback<'lua, 'static>) { - self.async_methods.push((name, callback)); - } - - fn add_meta_callback(&mut self, name: String, callback: Callback<'lua, 'static>) { - self.meta_methods.push((name, callback)); - } - - #[cfg(feature = "async")] - fn add_async_meta_callback(&mut self, meta: String, callback: AsyncCallback<'lua, 'static>) { - self.async_meta_methods.push((meta, callback)) + fn append_methods_from(&mut self, other: UserDataRegistrar<'lua, S>) { + self.methods.extend(other.methods); + #[cfg(feature = "async")] + self.async_methods.extend(other.async_methods); + self.meta_methods.extend(other.meta_methods); + #[cfg(feature = "async")] + self.async_meta_methods.extend(other.async_meta_methods); } } @@ -626,31 +647,13 @@ macro_rules! lua_userdata_impl { fn add_fields<'lua, F: UserDataFields<'lua, Self>>(fields: &mut F) { let mut orig_fields = UserDataRegistrar::new(); T::add_fields(&mut orig_fields); - for (name, callback) in orig_fields.field_getters { - fields.add_field_getter(name, callback); - } - for (name, callback) in orig_fields.field_setters { - fields.add_field_setter(name, callback); - } + fields.append_fields_from(orig_fields); } fn add_methods<'lua, M: UserDataMethods<'lua, Self>>(methods: &mut M) { let mut orig_methods = UserDataRegistrar::new(); T::add_methods(&mut orig_methods); - for (name, callback) in orig_methods.methods { - methods.add_callback(name, callback); - } - #[cfg(feature = "async")] - for (name, callback) in orig_methods.async_methods { - methods.add_async_callback(name, callback); - } - for (meta, callback) in orig_methods.meta_methods { - methods.add_meta_callback(meta, callback); - } - #[cfg(feature = "async")] - for (meta, callback) in orig_methods.async_meta_methods { - methods.add_async_meta_callback(meta, callback); - } + methods.append_methods_from(orig_methods); } } }; diff --git a/src/util/mod.rs b/src/util/mod.rs index 394ce34..97681ab 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -402,9 +402,12 @@ unsafe extern "C" fn lua_error_impl(state: *mut ffi::lua_State) -> c_int { } unsafe extern "C" fn lua_isfunction_impl(state: *mut ffi::lua_State) -> c_int { - let t = ffi::lua_type(state, -1); - ffi::lua_pop(state, 1); - ffi::lua_pushboolean(state, (t == ffi::LUA_TFUNCTION) as c_int); + ffi::lua_pushboolean(state, ffi::lua_isfunction(state, -1)); + 1 +} + +unsafe extern "C" fn lua_istable_impl(state: *mut ffi::lua_State) -> c_int { + ffi::lua_pushboolean(state, ffi::lua_istable(state, -1)); 1 } @@ -418,14 +421,19 @@ unsafe fn init_userdata_metatable_index(state: *mut ffi::lua_State) -> Result<() // Create and cache `__index` generator let code = cstr!( r#" - local error, isfunction = ... + local error, isfunction, istable = ... return function (__index, field_getters, methods) - -- Fastpath to return methods table for index access - if __index == nil and field_getters == nil then - return methods + -- Common case: has field getters and index is a table + if field_getters ~= nil and methods == nil and istable(__index) then + return function (self, key) + local field_getter = field_getters[key] + if field_getter ~= nil then + return field_getter(self) + end + return __index[key] + end end - -- Alternatively return a function for index access return function (self, key) if field_getters ~= nil then local field_getter = field_getters[key] @@ -460,7 +468,8 @@ unsafe fn init_userdata_metatable_index(state: *mut ffi::lua_State) -> Result<() } ffi::lua_pushcfunction(state, lua_error_impl); ffi::lua_pushcfunction(state, lua_isfunction_impl); - ffi::lua_call(state, 2, 1); + ffi::lua_pushcfunction(state, lua_istable_impl); + ffi::lua_call(state, 3, 1); #[cfg(feature = "luau-jit")] if ffi::luau_codegen_supported() != 0 { diff --git a/tests/userdata.rs b/tests/userdata.rs index 5045f6e..c545586 100644 --- a/tests/userdata.rs +++ b/tests/userdata.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::string::String as StdString; use std::sync::Arc; #[cfg(not(feature = "parking_lot"))] @@ -486,6 +487,7 @@ fn test_fields() -> Result<()> { impl UserData for MyUserData { fn add_fields<'lua, F: UserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field("static", "constant"); fields.add_field_method_get("val", |_, data| Ok(data.0)); fields.add_field_method_set("val", |_, data, val| { data.0 = val; @@ -497,11 +499,7 @@ fn test_fields() -> Result<()> { fields .add_field_function_set("uval", |_, ud, s| ud.set_user_value::>(s)); - fields.add_meta_field_with(MetaMethod::Index, |lua| { - let index = lua.create_table()?; - index.set("f", 321)?; - Ok(index) - }); + fields.add_meta_field(MetaMethod::Index, HashMap::from([("f", 321)])); fields.add_meta_field_with(MetaMethod::NewIndex, |lua| { lua.create_function(|lua, (_, field, val): (AnyUserData, String, Value)| { lua.globals().set(field, val)?; @@ -516,6 +514,7 @@ fn test_fields() -> Result<()> { globals.set("ud", MyUserData(7))?; lua.load( r#" + assert(ud.static == "constant") assert(ud.val == 7) ud.val = 10 assert(ud.val == 10) @@ -614,6 +613,7 @@ fn test_userdata_wrapped() -> Result<()> { impl UserData for MyUserData { fn add_fields<'lua, F: UserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field("static", "constant"); fields.add_field_method_get("data", |_, this| Ok(this.0)); fields.add_field_method_set("data", |_, this, val| { this.0 = val; @@ -631,6 +631,7 @@ fn test_userdata_wrapped() -> Result<()> { globals.set("rc_refcell_ud", ud1.clone())?; lua.load( r#" + assert(rc_refcell_ud.static == "constant") rc_refcell_ud.data = rc_refcell_ud.data + 1 assert(rc_refcell_ud.data == 2) "#, @@ -646,6 +647,7 @@ fn test_userdata_wrapped() -> Result<()> { globals.set("arc_mutex_ud", ud2.clone())?; lua.load( r#" + assert(arc_mutex_ud.static == "constant") arc_mutex_ud.data = arc_mutex_ud.data + 1 assert(arc_mutex_ud.data == 3) "#, @@ -660,6 +662,7 @@ fn test_userdata_wrapped() -> Result<()> { globals.set("arc_rwlock_ud", ud3.clone())?; lua.load( r#" + assert(arc_rwlock_ud.static == "constant") arc_rwlock_ud.data = arc_rwlock_ud.data + 1 assert(arc_rwlock_ud.data == 4) "#, @@ -686,7 +689,7 @@ fn test_userdata_proxy() -> Result<()> { impl UserData for MyUserData { fn add_fields<'lua, F: UserDataFields<'lua, Self>>(fields: &mut F) { - fields.add_field_function_get("static_field", |_, _| Ok(123)); + fields.add_field("static_field", 123); fields.add_field_method_get("n", |_, this| Ok(this.0)); } @@ -780,10 +783,7 @@ fn test_userdata_ext() -> Result<()> { assert_eq!(ud.get::<_, u32>("n")?, 123); ud.set("n", 321)?; assert_eq!(ud.get::<_, u32>("n")?, 321); - match ud.get::<_, u32>("non-existent") { - Err(Error::RuntimeError(_)) => {} - r => panic!("expected RuntimeError, got {r:?}"), - } + assert_eq!(ud.get::<_, Option>("non-existent")?, None); match ud.set::<_, u32>("non-existent", 123) { Err(Error::RuntimeError(_)) => {} r => panic!("expected RuntimeError, got {r:?}"),