diff --git a/godot-codegen/src/conv/type_conversions.rs b/godot-codegen/src/conv/type_conversions.rs index e9f25465b..6138e0793 100644 --- a/godot-codegen/src/conv/type_conversions.rs +++ b/godot-codegen/src/conv/type_conversions.rs @@ -217,7 +217,9 @@ fn to_rust_type_uncached(full_ty: &GodotTy, ctx: &mut Context) -> RustTy { } else if let Some(elem_ty) = ty.strip_prefix("typedarray::") { let rust_elem_ty = to_rust_type(elem_ty, None, ctx); return if ctx.is_builtin(elem_ty) { - RustTy::BuiltinArray(quote! { Array<#rust_elem_ty> }) + RustTy::BuiltinArray { + elem_type: quote! { Array<#rust_elem_ty> }, + } } else { RustTy::EngineArray { tokens: quote! { Array<#rust_elem_ty> }, diff --git a/godot-codegen/src/models/domain.rs b/godot-codegen/src/models/domain.rs index 942f603cd..d0cbf59e6 100644 --- a/godot-codegen/src/models/domain.rs +++ b/godot-codegen/src/models/domain.rs @@ -588,11 +588,13 @@ pub struct GodotTy { #[derive(Clone, Debug)] pub enum RustTy { - /// `bool`, `Vector3i` + /// `bool`, `Vector3i`, `Array` BuiltinIdent(Ident), /// `Array` - BuiltinArray(TokenStream), + /// + /// Note that untyped arrays are mapped as `BuiltinIdent("Array")`. + BuiltinArray { elem_type: TokenStream }, /// C-style raw pointer to a `RustTy`. RawPointer { inner: Box, is_const: bool }, @@ -674,7 +676,7 @@ impl ToTokens for RustTy { fn to_tokens(&self, tokens: &mut TokenStream) { match self { RustTy::BuiltinIdent(ident) => ident.to_tokens(tokens), - RustTy::BuiltinArray(path) => path.to_tokens(tokens), + RustTy::BuiltinArray { elem_type } => elem_type.to_tokens(tokens), RustTy::RawPointer { inner, is_const: true, diff --git a/godot-codegen/src/special_cases/codegen_special_cases.rs b/godot-codegen/src/special_cases/codegen_special_cases.rs index ca111d735..a5b6fe690 100644 --- a/godot-codegen/src/special_cases/codegen_special_cases.rs +++ b/godot-codegen/src/special_cases/codegen_special_cases.rs @@ -37,7 +37,7 @@ fn is_type_excluded(ty: &str, ctx: &mut Context) -> bool { fn is_rust_type_excluded(ty: &RustTy) -> bool { match ty { RustTy::BuiltinIdent(_) => false, - RustTy::BuiltinArray(_) => false, + RustTy::BuiltinArray { .. } => false, RustTy::RawPointer { inner, .. } => is_rust_type_excluded(inner), RustTy::EngineArray { elem_class, .. } => is_class_excluded(elem_class.as_str()), RustTy::EngineEnum { diff --git a/godot-core/src/builtin/collections/array.rs b/godot-core/src/builtin/collections/array.rs index 0db571078..754220a87 100644 --- a/godot-core/src/builtin/collections/array.rs +++ b/godot-core/src/builtin/collections/array.rs @@ -34,6 +34,9 @@ use sys::{ffi_methods, interface_fn, GodotFfi}; /// `Array`, where the type `T` must implement `ArrayElement`. Some types like `Array` cannot /// be stored inside arrays, as Godot prevents nesting. /// +/// If you plan to use any integer or float types apart from `i64` and `f64`, read +/// [this documentation](../meta/trait.ArrayElement.html#integer-and-float-types). +/// /// # Reference semantics /// /// Like in GDScript, `Array` acts as a reference type: multiple `Array` instances may @@ -716,11 +719,44 @@ impl Array { /// /// Note also that any `GodotType` can be written to a `Variant` array. /// - /// In the current implementation, both cases will produce a panic rather than undefined - /// behavior, but this should not be relied upon. + /// In the current implementation, both cases will produce a panic rather than undefined behavior, but this should not be relied upon. unsafe fn assume_type(self) -> Array { - // SAFETY: The memory layout of `Array` does not depend on `T`. - unsafe { std::mem::transmute(self) } + // The memory layout of `Array` does not depend on `T`. + std::mem::transmute::, Array>(self) + } + + /// # Safety + /// See [`assume_type`](Self::assume_type). + unsafe fn assume_type_ref(&self) -> &Array { + // The memory layout of `Array` does not depend on `T`. + std::mem::transmute::<&Array, &Array>(self) + } + + #[cfg(debug_assertions)] + pub(crate) fn debug_validate_elements(&self) -> Result<(), ConvertError> { + // SAFETY: every element is internally represented as Variant. + let canonical_array = unsafe { self.assume_type_ref::() }; + + // If any element is not convertible, this will return an error. + for elem in canonical_array.iter_shared() { + elem.try_to::().map_err(|_err| { + FromGodotError::BadArrayTypeInt { + expected: self.type_info(), + value: elem + .try_to::() + .expect("origin must be i64 compatible; this is a bug"), + } + .into_error(self.clone()) + })?; + } + + Ok(()) + } + + // No-op in Release. Avoids O(n) conversion checks, but still panics on access. + #[cfg(not(debug_assertions))] + pub(crate) fn debug_validate_elements(&self) -> Result<(), ConvertError> { + Ok(()) } /// Returns the runtime type info of this array. @@ -728,7 +764,12 @@ impl Array { let variant_type = VariantType::from_sys( self.as_inner().get_typed_builtin() as sys::GDExtensionVariantType ); - let class_name = self.as_inner().get_typed_class_name(); + + let class_name = if variant_type == VariantType::OBJECT { + Some(self.as_inner().get_typed_class_name()) + } else { + None + }; ArrayTypeInfo { variant_type, @@ -765,12 +806,23 @@ impl Array { if type_info.is_typed() { let script = Variant::nil(); + // A bit contrived because empty StringName is lazy-initialized but must also remain valid. + #[allow(unused_assignments)] + let mut empty_string_name = None; + let class_name = if let Some(class_name) = &type_info.class_name { + class_name.string_sys() + } else { + empty_string_name = Some(StringName::default()); + // as_ref() crucial here -- otherwise the StringName is dropped. + empty_string_name.as_ref().unwrap().string_sys() + }; + // SAFETY: The array is a newly created empty untyped array. unsafe { interface_fn!(array_set_typed)( self.sys_mut(), type_info.variant_type.sys(), - type_info.class_name.string_sys(), + class_name, // must be empty if variant_type != OBJECT. script.var_sys(), ); } @@ -786,6 +838,19 @@ impl Array { } } +impl VariantArray { + /// # Safety + /// - Variant must have type `VariantType::ARRAY`. + /// - Subsequent operations on this array must not rely on the type of the array. + pub(crate) unsafe fn from_variant_unchecked(variant: &Variant) -> Self { + // See also ffi_from_variant(). + Self::new_with_uninit(|self_ptr| { + let array_from_variant = sys::builtin_fn!(array_from_variant); + array_from_variant(self_ptr, sys::SysPtr::force_mut(variant.var_sys())); + }) + } +} + // ---------------------------------------------------------------------------------------------------------------------------------------------- // Traits @@ -838,6 +903,7 @@ impl ToGodot for Array { impl FromGodot for Array { fn try_from_godot(via: Self::Via) -> Result { + T::debug_validate_elements(&via)?; Ok(via) } } @@ -845,7 +911,8 @@ impl FromGodot for Array { impl fmt::Debug for Array { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { // Going through `Variant` because there doesn't seem to be a direct way. - write!(f, "{:?}", self.to_variant().stringify()) + // Reuse Display. + write!(f, "{}", self.to_variant().stringify()) } } @@ -878,7 +945,7 @@ impl fmt::Display for Array { impl Clone for Array { fn clone(&self) -> Self { // SAFETY: `self` is a valid array, since we have a reference that keeps it alive. - let array = unsafe { + let copy = unsafe { Self::new_with_uninit(|self_ptr| { let ctor = sys::builtin_fn!(array_construct_copy); let args = [self.sys()]; @@ -886,9 +953,13 @@ impl Clone for Array { }) }; - array - .with_checked_type() - .expect("copied array should have same type as original array") + // Double-check copy's runtime type in Debug mode. + if cfg!(debug_assertions) { + copy.with_checked_type() + .expect("copied array should have same type as original array") + } else { + copy + } } } @@ -1000,6 +1071,7 @@ impl GodotFfiVariant for Array { } fn ffi_from_variant(variant: &Variant) -> Result { + // First check if the variant is an array. The array conversion shouldn't be called otherwise. if variant.get_type() != Self::variant_type() { return Err(FromVariantError::BadType { expected: Self::variant_type(), @@ -1015,6 +1087,7 @@ impl GodotFfiVariant for Array { }) }; + // Then, check the runtime type of the array. array.with_checked_type() } } diff --git a/godot-core/src/builtin/collections/dictionary.rs b/godot-core/src/builtin/collections/dictionary.rs index 3d682949a..a09555b33 100644 --- a/godot-core/src/builtin/collections/dictionary.rs +++ b/godot-core/src/builtin/collections/dictionary.rs @@ -606,7 +606,7 @@ impl<'a> Keys<'a> { TypedKeys::from_untyped(self) } - /// Returns an array of the keys + /// Returns an array of the keys. pub fn array(self) -> VariantArray { // Can only be called assert!(self.iter.is_first); diff --git a/godot-core/src/builtin/collections/packed_array.rs b/godot-core/src/builtin/collections/packed_array.rs index 3e75fa082..3427af0d2 100644 --- a/godot-core/src/builtin/collections/packed_array.rs +++ b/godot-core/src/builtin/collections/packed_array.rs @@ -548,7 +548,7 @@ macro_rules! impl_packed_array { fn export_hint() -> $crate::meta::PropertyHintInfo { // In 4.3 Godot can (and does) use type hint strings for packed arrays, see https://github.com/godotengine/godot/pull/82952. if sys::GdextBuild::since_api("4.3") { - $crate::meta::PropertyHintInfo::export_array_element::<$Element>() + $crate::meta::PropertyHintInfo::export_packed_array_element::<$Element>() } else { $crate::meta::PropertyHintInfo::type_name::<$PackedArray>() } diff --git a/godot-core/src/builtin/variant/mod.rs b/godot-core/src/builtin/variant/mod.rs index 2d2643844..b3936ca7a 100644 --- a/godot-core/src/builtin/variant/mod.rs +++ b/godot-core/src/builtin/variant/mod.rs @@ -5,7 +5,9 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use crate::builtin::{GString, StringName, VariantDispatch, VariantOperator, VariantType}; +use crate::builtin::{ + GString, StringName, VariantArray, VariantDispatch, VariantOperator, VariantType, +}; use crate::meta::error::ConvertError; use crate::meta::{ArrayElement, FromGodot, ToGodot}; use godot_ffi as sys; @@ -448,6 +450,16 @@ impl fmt::Display for Variant { impl fmt::Debug for Variant { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - VariantDispatch::from_variant(self).fmt(f) + // Special case for arrays: avoids converting to VariantArray (the only Array type in VariantDispatch), which fails + // for typed arrays and causes a panic. This can cause an infinite loop with Debug, or abort. + // Can be removed if there's ever a "possibly typed" Array type (e.g. OutArray) in the library. + + if self.get_type() == VariantType::ARRAY { + // SAFETY: type is checked, and only operation is print (out data flow, no covariant in access). + let array = unsafe { VariantArray::from_variant_unchecked(self) }; + array.fmt(f) + } else { + VariantDispatch::from_variant(self).fmt(f) + } } } diff --git a/godot-core/src/meta/array_type_info.rs b/godot-core/src/meta/array_type_info.rs index 3e0db7395..4404d0eb7 100644 --- a/godot-core/src/meta/array_type_info.rs +++ b/godot-core/src/meta/array_type_info.rs @@ -16,17 +16,27 @@ use std::fmt; /// We ignore the `script` parameter because it has no impact on typing in Godot. #[derive(Eq, PartialEq)] pub(crate) struct ArrayTypeInfo { + /// The builtin type; always set. pub variant_type: VariantType, + /// If [`variant_type`] is [`VariantType::OBJECT`], then the class name; otherwise `None`. + /// /// Not a `ClassName` because some values come from Godot engine API. - pub class_name: StringName, + pub class_name: Option, } impl ArrayTypeInfo { pub fn of() -> Self { + let variant_type = ::Ffi::variant_type(); + let class_name = if variant_type == VariantType::OBJECT { + Some(T::Via::class_name().to_string_name()) + } else { + None + }; + Self { - variant_type: ::Ffi::variant_type(), - class_name: T::Via::class_name().to_string_name(), + variant_type, + class_name, } } @@ -38,18 +48,17 @@ impl ArrayTypeInfo { self.variant_type } - pub fn class_name(&self) -> &StringName { - &self.class_name + pub fn class_name(&self) -> Option<&StringName> { + self.class_name.as_ref() } } impl fmt::Debug for ArrayTypeInfo { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let class = self.class_name.to_string(); - let class_str = if class.is_empty() { - String::new() - } else { + let class_str = if let Some(class) = &self.class_name { format!(" (class={class})") + } else { + String::new() }; write!(f, "{:?}{}", self.variant_type, class_str) diff --git a/godot-core/src/meta/error/convert_error.rs b/godot-core/src/meta/error/convert_error.rs index 4114b8c70..136e74589 100644 --- a/godot-core/src/meta/error/convert_error.rs +++ b/godot-core/src/meta/error/convert_error.rs @@ -169,12 +169,19 @@ impl fmt::Display for ErrorKind { /// Conversion failed during a [`FromGodot`](crate::meta::FromGodot) call. #[derive(Eq, PartialEq, Debug)] pub(crate) enum FromGodotError { + /// Destination `Array` has different type than source's runtime type. BadArrayType { expected: ArrayTypeInfo, actual: ArrayTypeInfo, }, + + /// Special case of `BadArrayType` where a custom int type such as `i8` cannot hold a dynamic `i64` value. + BadArrayTypeInt { expected: ArrayTypeInfo, value: i64 }, + /// InvalidEnum is also used by bitfields. InvalidEnum, + + /// `InstanceId` cannot be 0. ZeroInstanceId, } @@ -208,17 +215,22 @@ impl fmt::Display for FromGodotError { }; } + let exp_class = expected.class_name().expect("lhs class name present"); + let act_class = actual.class_name().expect("rhs class name present"); assert_ne!( - expected.class_name(), - actual.class_name(), + exp_class, act_class, "BadArrayType with expected == got, this is a gdext bug" ); write!( f, - "expected array of class {}, got array of class {}", - expected.class_name(), - actual.class_name() + "expected array of class {exp_class}, got array of class {act_class}" + ) + } + Self::BadArrayTypeInt { expected, value } => { + write!( + f, + "integer value {value} does not fit into Array of type {expected:?}" ) } Self::InvalidEnum => write!(f, "invalid engine enum value"), diff --git a/godot-core/src/meta/godot_convert/impls.rs b/godot-core/src/meta/godot_convert/impls.rs index fb51c576b..bea93b086 100644 --- a/godot-core/src/meta/godot_convert/impls.rs +++ b/godot-core/src/meta/godot_convert/impls.rs @@ -5,7 +5,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use crate::builtin::Variant; +use crate::builtin::{Array, Variant}; use crate::meta::error::{ConvertError, FromFfiError, FromVariantError}; use crate::meta::{ ArrayElement, ClassName, FromGodot, GodotConvert, GodotNullableFfi, GodotType, @@ -174,6 +174,13 @@ macro_rules! impl_godot_scalar { impl_godot_scalar!(@shared_fns; $Via, $param_metadata); } + // For integer types, we can validate the conversion. + impl ArrayElement for $T { + fn debug_validate_elements(array: &Array) -> Result<(), ConvertError> { + array.debug_validate_elements() + } + } + impl_godot_scalar!(@shared_traits; $T); }; @@ -196,6 +203,9 @@ macro_rules! impl_godot_scalar { impl_godot_scalar!(@shared_fns; $Via, $param_metadata); } + // For f32, conversion from f64 is lossy but will always succeed. Thus no debug validation needed. + impl ArrayElement for $T {} + impl_godot_scalar!(@shared_traits; $T); }; @@ -210,8 +220,6 @@ macro_rules! impl_godot_scalar { }; (@shared_traits; $T:ty) => { - impl ArrayElement for $T {} - impl GodotConvert for $T { type Via = $T; } diff --git a/godot-core/src/meta/godot_convert/mod.rs b/godot-core/src/meta/godot_convert/mod.rs index 6dd233c23..0998c2a15 100644 --- a/godot-core/src/meta/godot_convert/mod.rs +++ b/godot-core/src/meta/godot_convert/mod.rs @@ -91,8 +91,10 @@ pub trait FromGodot: Sized + GodotConvert { /// # Panics /// If the conversion fails. fn from_variant(variant: &Variant) -> Self { - Self::try_from_variant(variant) - .unwrap_or_else(|err| panic!("FromGodot::from_variant() failed: {err}")) + Self::try_from_variant(variant).unwrap_or_else(|err| { + eprintln!("FromGodot::from_variant() failed: {err}"); + panic!() + }) } } diff --git a/godot-core/src/meta/mod.rs b/godot-core/src/meta/mod.rs index 6a55417a0..dde9ce2b6 100644 --- a/godot-core/src/meta/mod.rs +++ b/godot-core/src/meta/mod.rs @@ -46,7 +46,7 @@ mod traits; pub mod error; pub use class_name::ClassName; pub use godot_convert::{FromGodot, GodotConvert, ToGodot}; -pub use traits::{ArrayElement, GodotType}; +pub use traits::{ArrayElement, GodotType, PackedArrayElement}; pub(crate) use crate::impl_godot_as_self; pub(crate) use array_type_info::ArrayTypeInfo; diff --git a/godot-core/src/meta/property_info.rs b/godot-core/src/meta/property_info.rs index d24d0a3be..a6a1dcec4 100644 --- a/godot-core/src/meta/property_info.rs +++ b/godot-core/src/meta/property_info.rs @@ -7,7 +7,7 @@ use crate::builtin::{GString, StringName}; use crate::global::{PropertyHint, PropertyUsageFlags}; -use crate::meta::{ArrayElement, ClassName, GodotType}; +use crate::meta::{ArrayElement, ClassName, GodotType, PackedArrayElement}; use crate::registry::property::{Export, Var}; use crate::sys; use godot_ffi::VariantType; @@ -245,4 +245,12 @@ impl PropertyHintInfo { hint_string: GString::from(T::element_type_string()), } } + + /// Use for `#[export]` properties -- [`PROPERTY_HINT_TYPE_STRING`](PropertyHint::TYPE_STRING) with the **element** type string as hint string. + pub fn export_packed_array_element() -> Self { + Self { + hint: PropertyHint::TYPE_STRING, + hint_string: GString::from(T::element_type_string()), + } + } } diff --git a/godot-core/src/meta/traits.rs b/godot-core/src/meta/traits.rs index 102eda355..68a3a49ef 100644 --- a/godot-core/src/meta/traits.rs +++ b/godot-core/src/meta/traits.rs @@ -7,7 +7,7 @@ use godot_ffi as sys; -use crate::builtin::{StringName, Variant}; +use crate::builtin::{Array, StringName, Variant}; use crate::global::PropertyUsageFlags; use crate::meta::error::ConvertError; use crate::meta::{ @@ -120,28 +120,72 @@ pub trait GodotType: /// Marker trait to identify types that can be stored in [`Array`][crate::builtin::Array]. /// -/// The types for which this trait is implemented, overlap mostly with [`GodotType`]. -/// This is done consistently what GDScript allows inside `Array[T]`. +/// The types, for which this trait is implemented, overlap mostly with [`GodotType`]. /// /// Notable differences are: /// - Only `VariantArray`, not `Array` is allowed (typed arrays cannot be nested). /// - `Option` is only supported for `Option>`, but not e.g. `Option`. +/// +/// # Integer and float types +/// `u8`, `i8`, `u16`, `i16`, `u32`, `i32` and `f32` are supported by this trait, however they don't have their own array type in Godot. +/// The engine only knows about `i64` ("int") and `f64` ("float") types. This means that when using any integer or float type, Godot +/// will treat it as the equivalent of GDScript's `Array[int]` or `Array[float]`, respectively. +/// +/// As a result, when converting from a Godot typed array to a Rust `Array`, the values stored may not actually fit into a `T`. +/// For example, you have a GDScript `Array[int]` which stores value 160, and you convert it to a Rust `Array`. This means that you may +/// end up with panics on element access (since the `Variant` storing 160 will fail to convert to `i8`). In Debug mode, we add additional +/// best-effort checks to detect such errors, however they are expensive and not bullet-proof. If you need very rigid type safety, stick to +/// `i64` and `f64`. The other types however can be extremely convenient and work well, as long as you are aware of the limitations. +/// +/// `u64` is entirely unsupported since it cannot be safely stored inside a `Variant`. +/// +/// Also, keep in mind that Godot uses `Variant` for each element. If performance matters and you have small element types such as `u8`, +/// consider using packed arrays (e.g. `PackedByteArray`) instead. #[diagnostic::on_unimplemented( message = "`Array` can only store element types supported in Godot arrays (no nesting).", - label = "does not implement `Var`", - note = "see also: https://godot-rust.github.io/docs/gdext/master/godot/builtin/meta/trait.ArrayElement.html" + label = "has invalid element type" )] pub trait ArrayElement: GodotType + sealed::Sealed { /// Returns the representation of this type as a type string. /// - /// Used for elements in arrays and packed arrays (the latter despite `ArrayElement` not having a direct relation). + /// Used for elements in arrays (the latter despite `ArrayElement` not having a direct relation). /// - /// See [`PropertyHint::TYPE_STRING`] and [upstream docs]. - /// - /// [upstream docs]: https://docs.godotengine.org/en/stable/classes/class_%40globalscope.html#enum-globalscope-propertyhint + /// See [`PropertyHint::TYPE_STRING`] and + /// [upstream docs](https://docs.godotengine.org/en/stable/classes/class_%40globalscope.html#enum-globalscope-propertyhint). #[doc(hidden)] fn element_type_string() -> String { // Most array elements and all packed array elements are builtin types, so this is a good default. builtin_type_string::() } + + fn debug_validate_elements(_array: &Array) -> Result<(), ConvertError> { + // No-op for most element types. + Ok(()) + } +} + +/// Marker trait to identify types that can be stored in `Packed*Array` types. +#[diagnostic::on_unimplemented( + message = "`Packed*Array` can only store element types supported in Godot packed arrays.", + label = "has invalid element type" +)] +pub trait PackedArrayElement: GodotType + sealed::Sealed { + /// See [`ArrayElement::element_type_string()`]. + #[doc(hidden)] + fn element_type_string() -> String { + builtin_type_string::() + } } + +// Implement all packed array element types. +impl PackedArrayElement for u8 {} +impl PackedArrayElement for i32 {} +impl PackedArrayElement for i64 {} +impl PackedArrayElement for f32 {} +impl PackedArrayElement for f64 {} +impl PackedArrayElement for crate::builtin::Vector2 {} +impl PackedArrayElement for crate::builtin::Vector3 {} +#[cfg(since_api = "4.3")] +impl PackedArrayElement for crate::builtin::Vector4 {} +impl PackedArrayElement for crate::builtin::Color {} +impl PackedArrayElement for crate::builtin::GString {} diff --git a/itest/rust/src/builtin_tests/containers/variant_test.rs b/itest/rust/src/builtin_tests/containers/variant_test.rs index 4c3a79e8e..19707d93d 100644 --- a/itest/rust/src/builtin_tests/containers/variant_test.rs +++ b/itest/rust/src/builtin_tests/containers/variant_test.rs @@ -9,7 +9,7 @@ use std::cmp::Ordering; use std::fmt::Display; use godot::builtin::{ - dict, varray, GString, NodePath, Signal, StringName, Variant, Vector2, Vector3, + array, dict, varray, Array, GString, NodePath, Signal, StringName, Variant, Vector2, Vector3, }; use godot::builtin::{Basis, Dictionary, VariantArray, VariantOperator, VariantType}; use godot::classes::{Node, Node2D}; @@ -130,6 +130,33 @@ fn variant_bad_conversions() { .expect_err("`nil` should not convert to `Dictionary`"); } +#[itest] +fn variant_array_bad_conversions() { + let i32_array: Array = array![1, 2, 160, -40]; + let i32_variant = i32_array.to_variant(); + let i8_back = i32_variant.try_to::>(); + + // In Debug mode, we expect an error upon conversion. + #[cfg(debug_assertions)] + { + let err = i8_back.expect_err("Array -> Array conversion should fail"); + assert_eq!( + err.to_string(), + "integer value 160 does not fit into Array of type INT: [1, 2, 160, -40]" + ) + } + + // In Release mode, we expect the conversion to succeed, but a panic to occur on element access. + #[cfg(not(debug_assertions))] + { + let i8_array = i8_back.expect("Array -> Array conversion should succeed"); + expect_panic("accessing element 160 as i8 should panic", || { + // Note: get() returns Err on out-of-bounds, but currently panics on bad element type, since that's always a bug. + i8_array.get(2); + }); + } +} + #[itest] fn variant_special_conversions() { // See https://github.com/godot-rust/gdext/pull/598.