diff --git a/godot-codegen/src/conv/type_conversions.rs b/godot-codegen/src/conv/type_conversions.rs index 93e3e9e71..80d9bb13d 100644 --- a/godot-codegen/src/conv/type_conversions.rs +++ b/godot-codegen/src/conv/type_conversions.rs @@ -61,7 +61,14 @@ fn to_hardcoded_rust_ident(full_ty: &GodotTy) -> Option { Some(FlowDirection::GodotToRust) => "VarArray", None => "_unused__Array_must_not_appear_in_idents", }, - ("Dictionary", None) => "VarDictionary", + + // For Dictionary, use AnyDictionary/VarDictionary with flow differentiation: + // * VarDictionary is always untyped (Dictionary). + // * AnyDictionary is covariant and can hold typed or untyped dictionaries. + ("Dictionary", None) => match full_ty.flow { + Some(FlowDirection::RustToGodot) => "AnyDictionary", + Some(FlowDirection::GodotToRust) | None => "VarDictionary", + }, // Types needed for native structures mapping ("uint8_t", None) => "u8", diff --git a/godot-core/src/builtin/collections/any_dictionary.rs b/godot-core/src/builtin/collections/any_dictionary.rs new file mode 100644 index 000000000..2c0e6d0c7 --- /dev/null +++ b/godot-core/src/builtin/collections/any_dictionary.rs @@ -0,0 +1,433 @@ +/* + * Copyright (c) godot-rust; Bromeon and contributors. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +//! Type-erased dictionary. + +use std::fmt; + +use godot_ffi as sys; +use sys::{GodotFfi, ffi_methods}; + +use super::dictionary::{Iter, Keys}; +use crate::builtin::*; +use crate::meta; +use crate::meta::error::ConvertError; +use crate::meta::{ + ArrayElement, AsVArg, ElementType, GodotConvert, GodotFfiVariant, GodotType, ToGodot, +}; +use crate::registry::property::SimpleVar; + +/// Covariant `Dictionary` that can be typed or untyped. +/// +/// Unlike [`Dictionary`], which carries compile-time type information, `AnyDictionary` is a type-erased version of dictionaries. +/// It can point to any `Dictionary`, for both typed and untyped dictionaries. +/// +/// # Covariance +/// In GDScript, the subtyping relationship is modeled incorrectly for typed dictionaries: +/// ```gdscript +/// var typed: Dictionary[String, int] = {"one": 1, "two": 2} +/// var untyped: Dictionary = typed # Implicit "upcast" to Dictionary[Variant, Variant]. +/// +/// untyped["hello"] = "world" # Not detected by GDScript parser (may fail at runtime). +/// ``` +/// +/// godot-rust on the other hand introduces a new type `AnyDictionary`, which can store _any_ dictionary, typed or untyped. +/// `AnyDictionary` provides operations that are valid regardless of the type, e.g. `len()`, `is_empty()` or `clear()`. +/// Methods which would return specific types on `Dictionary` exist on `AnyDictionary` but return `Variant`. +/// +/// `AnyDictionary` does not provide any operations where data flows _into_ to the dictionary, such as `set()` or `insert()`. +/// Note that this does **not** mean that `AnyDictionary` is an immutable view; mutating methods that are agnostic to element types (or where +/// data only flows _out_), are still available. Examples are `clear()` and `remove()`. +/// +/// ## Conversions +/// - Use [`try_cast_dictionary::()`][Self::try_cast_dictionary] to convert to a typed `Dictionary`. +/// - Use [`try_cast_var_dictionary()`][Self::try_cast_var_dictionary] to convert to an untyped `VarDictionary`. +#[derive(PartialEq)] +#[repr(transparent)] // Guarantees same layout as VarDictionary, enabling Deref from Dictionary (K/V have no influence on layout). +pub struct AnyDictionary { + dict: VarDictionary, +} + +impl AnyDictionary { + pub(super) fn from_typed_or_untyped( + dict: Dictionary, + ) -> Self { + // SAFETY: Dictionary is not accessed as such, but immediately wrapped in AnyDictionary. + let inner = unsafe { std::mem::transmute::, VarDictionary>(dict) }; + + Self { dict: inner } + } + + /// Creates an empty untyped `AnyDictionary`. + pub(crate) fn new_untyped() -> Self { + Self { + dict: VarDictionary::default(), + } + } + + fn from_opaque(opaque: sys::types::OpaqueDictionary) -> Self { + Self { + dict: VarDictionary::from_opaque(opaque), + } + } + + /// ⚠️ Returns the value for the given key, or panics. + /// + /// To check for presence, use [`get()`][Self::get]. + /// + /// # Panics + /// If there is no value for the given key. Note that this is distinct from a `NIL` value, which is returned as `Variant::nil()`. + pub fn at(&self, key: impl AsVArg) -> Variant { + self.dict.at(key) + } + + /// Returns the value for the given key, or `None`. + /// + /// Note that `NIL` values are returned as `Some(Variant::nil())`, while absent values are returned as `None`. + pub fn get(&self, key: impl AsVArg) -> Option { + self.dict.get(key) + } + + /// Returns `true` if the dictionary contains the given key. + /// + /// _Godot equivalent: `has`_ + #[doc(alias = "has")] + pub fn contains_key(&self, key: impl AsVArg) -> bool { + self.dict.contains_key(key) + } + + /// Returns `true` if the dictionary contains all the given keys. + /// + /// _Godot equivalent: `has_all`_ + #[doc(alias = "has_all")] + pub fn contains_all_keys(&self, keys: &VarArray) -> bool { + self.dict.contains_all_keys(keys) + } + + /// Returns the number of entries in the dictionary. + /// + /// _Godot equivalent: `size`_ + #[doc(alias = "size")] + pub fn len(&self) -> usize { + self.dict.len() + } + + /// Returns true if the dictionary is empty. + pub fn is_empty(&self) -> bool { + self.dict.is_empty() + } + + /// Returns a 32-bit integer hash value representing the dictionary and its contents. + /// + /// Dictionaries with equal content will always produce identical hash values. However, the reverse is not true: + /// Different dictionaries can have identical hash values due to hash collisions. + pub fn hash_u32(&self) -> u32 { + self.dict.hash_u32() + } + + /// Reverse-search a key by its value. + /// + /// Unlike Godot, this will return `None` if the key does not exist and `Some(key)` if found. + /// + /// This operation is rarely needed and very inefficient. If you find yourself needing it a lot, consider + /// using a `HashMap` or `Dictionary` with the inverse mapping. + /// + /// _Godot equivalent: `find_key`_ + #[doc(alias = "find_key")] + pub fn find_key_by_value(&self, value: impl AsVArg) -> Option { + self.dict.find_key_by_value(value) + } + + /// Removes all key-value pairs from the dictionary. + pub fn clear(&mut self) { + self.dict.clear() + } + + /// Removes a key from the dictionary, and returns the value associated with the key if it was present. + /// + /// This is a covariant-safe operation that removes data without adding typed data. + /// + /// _Godot equivalent: `erase`_ + #[doc(alias = "erase")] + pub fn remove(&mut self, key: impl AsVArg) -> Option { + self.dict.remove(key) + } + + /// Alias for [`remove()`][Self::remove]. + /// + /// _Godot equivalent: `erase`_ + pub fn erase(&mut self, key: impl AsVArg) -> Option { + self.remove(key) + } + + /// Creates a new `Array` containing all the keys currently in the dictionary. + /// + /// _Godot equivalent: `keys`_ + #[doc(alias = "keys")] + pub fn keys_array(&self) -> AnyArray { + // Array can still be typed; so AnyArray is the only sound return type. + // Do not use dict.keys_array() which assumes Variant typing. + self.dict.as_inner().keys().upcast_any_array() + } + + /// Creates a new `Array` containing all the values currently in the dictionary. + /// + /// _Godot equivalent: `values`_ + #[doc(alias = "values")] + pub fn values_array(&self) -> AnyArray { + // Array can still be typed; so AnyArray is the only sound return type. + // Do not use dict.values_array() which assumes Variant typing. + self.dict.as_inner().values().upcast_any_array() + } + + /// Returns a shallow copy, sharing reference types (`Array`, `Dictionary`, `Object`...) with the original dictionary. + /// + /// This operation retains the dynamic key/value types: copying `Dictionary` will yield another `Dictionary`. + /// + /// To create a deep copy, use [`duplicate_deep()`][Self::duplicate_deep] instead. + /// To create a new reference to the same dictionary data, use [`clone()`][Clone::clone]. + pub fn duplicate_shallow(&self) -> AnyDictionary { + self.dict.duplicate_shallow().upcast_any_dictionary() + } + + /// Returns a deep copy, duplicating nested `Array`/`Dictionary` elements but keeping `Object` elements shared. + /// + /// This operation retains the dynamic key/value types: copying `Dictionary` will yield another `Dictionary`. + /// + /// To create a shallow copy, use [`duplicate_shallow()`][Self::duplicate_shallow] instead. + /// To create a new reference to the same dictionary data, use [`clone()`][Clone::clone]. + pub fn duplicate_deep(&self) -> Self { + self.dict.duplicate_deep().upcast_any_dictionary() + } + + /// Returns an iterator over the key-value pairs of the `Dictionary`. + /// + /// The pairs are each of type `(Variant, Variant)`. Each pair references the original dictionary, but instead of a `&`-reference + /// to key-value pairs as you might expect, the iterator returns a (cheap, shallow) copy of each key-value pair. + /// + /// Note that it's possible to modify the dictionary through another reference while iterating over it. This will not result in + /// unsoundness or crashes, but will cause the iterator to behave in an unspecified way. + pub fn iter_shared(&self) -> Iter<'_> { + self.dict.iter_shared() + } + + /// Returns an iterator over the keys in the `Dictionary`. + /// + /// The keys are each of type `Variant`. Each key references the original `Dictionary`, but instead of a `&`-reference to keys + /// as you might expect, the iterator returns a (cheap, shallow) copy of each key. + /// + /// Note that it's possible to modify the `Dictionary` through another reference while iterating over it. This will not result in + /// unsoundness or crashes, but will cause the iterator to behave in an unspecified way. + pub fn keys_shared(&self) -> Keys<'_> { + self.dict.keys_shared() + } + + /// Turns the dictionary into a shallow-immutable dictionary. + /// + /// Makes the dictionary read-only and returns the original dictionary. Disables modification of the dictionary's contents. + /// Does not apply to nested content, e.g. elements of nested dictionaries. + /// + /// In GDScript, dictionaries are automatically read-only if declared with the `const` keyword. + /// + /// _Godot equivalent: `make_read_only`_ + #[doc(alias = "make_read_only")] + pub fn into_read_only(self) -> Self { + self.dict.as_inner().make_read_only(); + self + } + + /// Returns `true` if the dictionary is read-only. + /// + /// See [`into_read_only()`][Self::into_read_only]. + pub fn is_read_only(&self) -> bool { + self.dict.is_read_only() + } + + /// Returns the runtime element type information for keys in this dictionary. + /// + /// The result is generally cached, so feel free to call this method repeatedly. + pub fn key_element_type(&self) -> ElementType { + self.dict.key_element_type() + } + + /// Returns the runtime element type information for values in this dictionary. + /// + /// The result is generally cached, so feel free to call this method repeatedly. + pub fn value_element_type(&self) -> ElementType { + self.dict.value_element_type() + } + + // TODO(v0.5): rename to `as_inner_unchecked` for consistency; `_mut` is misleading since receiver is `&self`. + /// # Safety + /// Must not be used for any "input" operations, moving elements into the dictionary -- this would break covariance. + #[doc(hidden)] + pub unsafe fn as_inner_mut(&self) -> inner::InnerDictionary<'_> { + inner::InnerDictionary::from_outer(&self.dict) + } + + /// Converts to `Dictionary` if the runtime types match. + /// + /// If `K=Variant` and `V=Variant`, this will attempt to "downcast" to an untyped dictionary, identical to + /// [`try_cast_var_dictionary()`][Self::try_cast_var_dictionary]. + /// + /// Returns `Err(self)` if the dictionary's dynamic types differ from `K` and `V`. Check [`key_element_type()`][Self::key_element_type] + /// and [`value_element_type()`][Self::value_element_type] before calling to determine what types the dictionary actually holds. + /// + /// Consumes `self`, to avoid incrementing reference-count and to be only callable on `AnyDictionary`, not `Dictionary`. + /// Use `clone()` if you need to keep the original. + pub fn try_cast_dictionary( + self, + ) -> Result, Self> { + let from_key_type = self.dict.key_element_type(); + let from_value_type = self.dict.value_element_type(); + let to_key_type = ElementType::of::(); + let to_value_type = ElementType::of::(); + + if from_key_type == to_key_type && from_value_type == to_value_type { + // SAFETY: just checked types match. + let dict = unsafe { std::mem::transmute::>(self.dict) }; + Ok(dict) + } else { + Err(self) + } + } + + /// Converts to an untyped `VarDictionary` if the dictionary is untyped. + /// + /// This is a shorthand for [`try_cast_dictionary::()`][Self::try_cast_dictionary]. + /// + /// Consumes `self`, to avoid incrementing reference-count. Use `clone()` if you need to keep the original. + pub fn try_cast_var_dictionary(self) -> Result { + self.try_cast_dictionary::() + } +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// Traits + +// SAFETY: See VarDictionary. +// +// We cannot provide GodotConvert with Via=VarDictionary, because ToGodot::to_godot() would otherwise enable a safe conversion from AnyDictionary to +// VarDictionary, which is not sound. +unsafe impl GodotFfi for AnyDictionary { + const VARIANT_TYPE: sys::ExtVariantType = + sys::ExtVariantType::Concrete(VariantType::DICTIONARY); + + // No Default trait, thus manually defining this and ffi_methods!. + unsafe fn new_with_init(init_fn: impl FnOnce(sys::GDExtensionTypePtr)) -> Self { + let mut result = Self::new_untyped(); + init_fn(result.sys_mut()); + result + } + + // Manually forwarding these, since no Opaque. + fn sys(&self) -> sys::GDExtensionConstTypePtr { + self.dict.sys() + } + + fn sys_mut(&mut self) -> sys::GDExtensionTypePtr { + self.dict.sys_mut() + } + + unsafe fn move_return_ptr(self, dst: sys::GDExtensionTypePtr, call_type: sys::PtrcallType) { + unsafe { self.dict.move_return_ptr(dst, call_type) } + } + + ffi_methods! { type sys::GDExtensionTypePtr = *mut Opaque; + fn new_from_sys; + fn new_with_uninit; + fn from_arg_ptr; + } +} + +impl Clone for AnyDictionary { + fn clone(&self) -> Self { + Self { + dict: self.dict.clone(), + } + } +} + +impl meta::sealed::Sealed for AnyDictionary {} + +impl ArrayElement for AnyDictionary {} + +impl GodotConvert for AnyDictionary { + type Via = Self; +} + +impl ToGodot for AnyDictionary { + type Pass = meta::ByValue; + + fn to_godot(&self) -> meta::ToArg<'_, Self::Via, Self::Pass> { + self.clone() + } + + fn to_variant(&self) -> Variant { + self.ffi_to_variant() + } +} + +impl meta::FromGodot for AnyDictionary { + fn try_from_godot(via: Self::Via) -> Result { + Ok(via) + } +} + +// TODO(v0.5): reconsider whether AnyDictionary should implement SimpleVar (and thus Var + Export). +// It allows exporting AnyDictionary as a property, but VarDictionary already serves that purpose. +// AnyArray has the same pattern -- if changed, update both. +impl SimpleVar for AnyDictionary {} + +impl fmt::Debug for AnyDictionary { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.dict.fmt(f) + } +} + +impl fmt::Display for AnyDictionary { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.dict.fmt(f) + } +} + +impl GodotType for AnyDictionary { + type Ffi = Self; + + type ToFfi<'f> + = meta::RefArg<'f, AnyDictionary> + where + Self: 'f; + + fn to_ffi(&self) -> Self::ToFfi<'_> { + meta::RefArg::new(self) + } + + fn into_ffi(self) -> Self::Ffi { + self + } + + fn try_from_ffi(ffi: Self::Ffi) -> Result { + Ok(ffi) + } + + fn godot_type_name() -> String { + VarDictionary::godot_type_name() + } +} + +impl GodotFfiVariant for AnyDictionary { + fn ffi_to_variant(&self) -> Variant { + VarDictionary::ffi_to_variant(&self.dict) + } + + fn ffi_from_variant(variant: &Variant) -> Result { + // SAFETY: All element types are valid for AnyDictionary. + let result = unsafe { VarDictionary::unchecked_from_variant(variant) }; + result.map(|inner| Self { dict: inner }) + } +} diff --git a/godot-core/src/builtin/collections/array.rs b/godot-core/src/builtin/collections/array.rs index fad47c284..c28c95547 100644 --- a/godot-core/src/builtin/collections/array.rs +++ b/godot-core/src/builtin/collections/array.rs @@ -911,6 +911,8 @@ impl Array { /// - Values written to array must match runtime type. /// - Values read must be convertible to type `U`. /// - If runtime type matches `U`, both conditions hold automatically. + // TODO(v0.5): fragile manual field move + mem::forget; if a field is added, it must be moved here too. + // Consider transmute (requires #[repr(C)]) or ManuallyDrop + ptr::read. Same issue in Dictionary::assume_type. pub(super) unsafe fn assume_type(self) -> Array { let result = Array:: { opaque: self.opaque, @@ -957,31 +959,15 @@ impl Array { /// Checks that the inner array has the correct type set on it for storing elements of type `T`. fn with_checked_type(self) -> Result { - let self_ty = self.element_type(); - let target_ty = ElementType::of::(); + let actual = self.element_type(); + let expected = ElementType::of::(); - // Exact match: check successful. - if self_ty == target_ty { - return Ok(self); - } - - // Check if script class (runtime) matches its native base class (compile-time). - // This allows an Array[Enemy] from GDScript to be used as Array> in Rust. - if let (ElementType::ScriptClass(_), ElementType::Class(expected_class)) = - (&self_ty, &target_ty) - { - if let Some(actual_base_class) = self_ty.class_id() { - if actual_base_class == *expected_class { - return Ok(self); - } - } + if actual.is_compatible_with(&expected) { + Ok(self) + } else { + let mismatch = ArrayMismatch { expected, actual }; + Err(FromGodotError::BadArrayType(mismatch).into_error(self)) } - - let mismatch = ArrayMismatch { - expected: target_ty, - actual: self_ty, - }; - Err(FromGodotError::BadArrayType(mismatch).into_error(self)) } /// Sets the type of the inner array. diff --git a/godot-core/src/builtin/collections/dictionary.rs b/godot-core/src/builtin/collections/dictionary.rs index 0d60ae164..3318b3671 100644 --- a/godot-core/src/builtin/collections/dictionary.rs +++ b/godot-core/src/builtin/collections/dictionary.rs @@ -13,31 +13,30 @@ use godot_ffi as sys; use sys::types::OpaqueDictionary; use sys::{GodotFfi, ffi_methods, interface_fn}; -use crate::builtin::{VarArray, Variant, inner}; -use crate::meta::{ElementType, ExtVariantType, FromGodot, ToGodot}; - -#[deprecated = "Renamed to `VarDictionary`; `Dictionary` will be reserved for typed dictionaries in the future."] -pub type Dictionary = VarDictionary; +use super::any_dictionary::AnyDictionary; +use crate::builtin::{Array, VarArray, Variant, VariantType, inner}; +use crate::meta; +use crate::meta::{ArrayElement, AsVArg, ElementType, ExtVariantType, FromGodot, ToGodot}; /// Godot's `Dictionary` type. /// -/// Ordered associative hash-table, mapping keys to values. +/// Ordered associative hash-table, mapping keys to values. Corresponds to GDScript type `Dictionary[K, V]`. /// -/// The keys and values of the dictionary are all `Variant`s, so they can be of different types. -/// Variants are designed to be generally cheap to clone. Typed dictionaries are planned in a future godot-rust version. +/// `Dictionary` can only hold keys of type `K` and values of type `V` (except for Godot < 4.4, see below). +/// The key type `K` and value type `V` can be anything implementing the [`ArrayElement`] trait. +/// Untyped dictionaries are represented as `Dictionary`, which is aliased as [`VarDictionary`]. /// /// Check out the [book](https://godot-rust.github.io/book/godot-api/builtins.html#arrays-and-dictionaries) for a tutorial on dictionaries. /// -/// # Dictionary example -/// +/// # Untyped example /// ```no_run /// # use godot::prelude::*; -/// // Create empty dictionary and add key-values pairs. +/// // Create untyped dictionary and add key-values pairs. /// let mut dict = VarDictionary::new(); /// dict.set("str", "Hello"); /// dict.set("num", 23); /// -/// // Keys don't need to be strings. +/// // For untyped dictionaries, keys don't need to be strings. /// let coord = Vector2i::new(0, 1); /// dict.set(coord, "Tile77"); /// @@ -53,16 +52,11 @@ pub type Dictionary = VarDictionary; /// let value: GString = dict.at("str").to(); // Variant::to() extracts GString. /// let maybe: Option = dict.get("absent_key"); /// -/// // Iterate over key-value pairs as (Variant, Variant). +/// // Iterate over key-value pairs as (K, V) -- here (Variant, Variant). /// for (key, value) in dict.iter_shared() { /// println!("{key} => {value}"); /// } /// -/// // Use typed::() to get typed iterators. -/// for (key, value) in dict.iter_shared().typed::() { -/// println!("{key} => {value}"); -/// } -/// /// // Clone dictionary (shares the reference), and overwrite elements through clone. /// let mut cloned = dict.clone(); /// cloned.remove("num"); @@ -75,15 +69,46 @@ pub type Dictionary = VarDictionary; /// assert_eq!(dict.get("num"), None); /// ``` /// -/// # Thread safety +// TODO(v0.5): support enums -- https://github.com/godot-rust/gdext/issues/353. +// # Typed example +// ```no_run +// # use godot::prelude::*; +// +// // Define a Godot-exported enum. +// #[derive(GodotConvert)] +// #[godot(via = GString)] +// enum Tile { GRASS, ROCK, WATER } +// +// let mut tiles = Dictionary::::new(); +// tiles.set(Vector2i::new(1, 2), Tile::GRASS); +// tiles.set(Vector2i::new(1, 3), Tile::WATER); +// +// // Create the same dictionary in a single expression. +// let tiles = dict! { +// (Vector2i::new(1, 2)): Tile::GRASS, +// (Vector2i::new(1, 3)): Tile::WATER, +// }; +// +// // Element access is now strongly typed. +// let value = dict.at(Vector2i::new(1, 3)); // type Tile. +// ``` /// -/// The same principles apply as for [`VarArray`]. Consult its documentation for details. +/// # Compatibility +/// **Godot 4.4+**: Dictionaries are fully typed at compile time and runtime. Type information is enforced by GDScript +/// and visible in the editor. /// -/// # Godot docs +/// **Before Godot 4.4**: Type safety is enforced only on the Rust side. GDScript sees all dictionaries as untyped, and type information is not +/// available in the editor. When assigning dictionaries from GDScript to typed Rust ones, panics may occur on access if the type is incorrect. +/// For more defensive code, `VarDictionary` is recommended. /// +/// # Thread safety +/// The same principles apply as for [`crate::builtin::Array`]. Consult its documentation for details. +/// +/// # Godot docs /// [`Dictionary` (stable)](https://docs.godotengine.org/en/stable/classes/class_dictionary.html) -pub struct VarDictionary { +pub struct Dictionary { opaque: OpaqueDictionary, + _phantom: PhantomData<(K, V)>, /// Lazily computed and cached element type information for the key type. cached_key_type: OnceCell, @@ -92,67 +117,77 @@ pub struct VarDictionary { cached_value_type: OnceCell, } -impl VarDictionary { - fn from_opaque(opaque: OpaqueDictionary) -> Self { +/// Untyped Godot `Dictionary`. +/// +/// Alias for `Dictionary`. This provides an untyped dictionary that can store any key-value pairs. +/// Available on all Godot versions. +pub type VarDictionary = Dictionary; + +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// Implementation + +impl Dictionary { + pub(super) fn from_opaque(opaque: OpaqueDictionary) -> Self { Self { opaque, + _phantom: PhantomData, cached_key_type: OnceCell::new(), cached_value_type: OnceCell::new(), } } - /// Constructs an empty `Dictionary`. + /// Constructs an empty typed `Dictionary`. pub fn new() -> Self { - Self::default() + let mut dict = Self::default(); + dict.init_inner_type(); + dict } /// ⚠️ Returns the value for the given key, or panics. /// - /// If you want to check for presence, use [`get()`][Self::get] or [`get_or_nil()`][Self::get_or_nil]. + /// If you want to check for presence, use [`get()`][Self::get]. For `V=Variant`, you can additionally use [`get_or_nil()`][Self::get_or_nil]. /// /// # Panics - /// /// If there is no value for the given key. Note that this is distinct from a `NIL` value, which is returned as `Variant::nil()`. - pub fn at(&self, key: K) -> Variant { - // Code duplication with get(), to avoid third clone (since K: ToGodot takes ownership). - - let key = key.to_variant(); - if self.contains_key(key.clone()) { - self.get_or_nil(key) + pub fn at(&self, key: impl AsVArg) -> V { + meta::varg_into_ref!(key: K); + let key_variant = key.to_variant(); + if self.as_inner().has(&key_variant) { + self.get_or_panic(key_variant) } else { - panic!("key {key:?} missing in dictionary: {self:?}") + panic!("key {key_variant:?} missing in dictionary: {self:?}") } } /// Returns the value for the given key, or `None`. /// - /// Note that `NIL` values are returned as `Some(Variant::nil())`, while absent values are returned as `None`. + /// Note that `NIL` values are returned as `Some(V::from_variant(...))`, while absent values are returned as `None`. /// If you want to treat both as `NIL`, use [`get_or_nil()`][Self::get_or_nil]. /// /// When you are certain that a key is present, use [`at()`][`Self::at`] instead. /// /// This can be combined with Rust's `Option` methods, e.g. `dict.get(key).unwrap_or(default)`. - pub fn get(&self, key: K) -> Option { - // If implementation is changed, make sure to update at(). - - let key = key.to_variant(); - if self.contains_key(key.clone()) { - Some(self.get_or_nil(key)) + pub fn get(&self, key: impl AsVArg) -> Option { + meta::varg_into_ref!(key: K); + let key_variant = key.to_variant(); + if self.as_inner().has(&key_variant) { + Some(self.get_or_panic(key_variant)) } else { None } } - /// Returns the value at the key in the dictionary, or `NIL` otherwise. - /// - /// This method does not let you differentiate `NIL` values stored as values from absent keys. - /// If you need that, use [`get()`][`Self::get`] instead. - /// - /// When you are certain that a key is present, use [`at()`][`Self::at`] instead. - /// - /// _Godot equivalent: `dict.get(key, null)`_ - pub fn get_or_nil(&self, key: K) -> Variant { - self.as_inner().get(&key.to_variant(), &Variant::nil()) + /// Returns the value at the key, converted to `V`. Panics on conversion failure. + fn get_or_panic(&self, key: Variant) -> V { + V::from_variant(&self.as_inner().get(&key, &Variant::nil())) + } + + // TODO(v0.5): avoid double FFI round-trip (has + get); consider using get(key, sentinel) pattern. + /// Gets and removes the old value for a key, if it exists. + fn take_old_value(&self, key_variant: &Variant) -> Option { + self.as_inner() + .has(key_variant) + .then(|| self.get_or_panic(key_variant.clone())) } /// Gets a value and ensures the key is set, inserting default if key is absent. @@ -160,31 +195,38 @@ impl VarDictionary { /// If the `key` exists in the dictionary, this behaves like [`get()`][Self::get], and the existing value is returned. /// Otherwise, the `default` value is inserted and returned. /// - /// # Compatibility - /// This function is natively available from Godot 4.3 onwards, we provide a polyfill for older versions. - /// /// _Godot equivalent: `get_or_add`_ #[doc(alias = "get_or_add")] - pub fn get_or_insert(&mut self, key: K, default: V) -> Variant { + pub fn get_or_insert(&mut self, key: impl AsVArg, default: impl AsVArg) -> V { self.balanced_ensure_mutable(); + meta::varg_into_ref!(key: K); + meta::varg_into_ref!(default: V); + let key_variant = key.to_variant(); - let default_variant = default.to_variant(); // Godot 4.3+: delegate to native get_or_add(). #[cfg(since_api = "4.3")] { - self.as_inner().get_or_add(&key_variant, &default_variant) + let default_variant = default.to_variant(); + + let result = self.as_inner().get_or_add(&key_variant, &default_variant); + V::from_variant(&result) } // Polyfill for Godot versions before 4.3. #[cfg(before_api = "4.3")] { - if let Some(existing_value) = self.get(key_variant.clone()) { - existing_value + if self.as_inner().has(&key_variant) { + self.get_or_panic(key_variant) } else { - self.set(key_variant, default_variant.clone()); - default_variant + let default_variant = default.to_variant(); + + // SAFETY: K and V strongly typed. + unsafe { self.set_variant(key_variant, default_variant.clone()) }; + + // Variant roundtrip to avoid V: Clone bound. Inefficient but old Godot version. + V::from_variant(&default_variant) } } } @@ -193,7 +235,8 @@ impl VarDictionary { /// /// _Godot equivalent: `has`_ #[doc(alias = "has")] - pub fn contains_key(&self, key: K) -> bool { + pub fn contains_key(&self, key: impl AsVArg) -> bool { + meta::varg_into_ref!(key: K); let key = key.to_variant(); self.as_inner().has(&key) } @@ -221,18 +264,22 @@ impl VarDictionary { /// Reverse-search a key by its value. /// - /// Unlike Godot, this will return `None` if the key does not exist and `Some(Variant::nil())` the key is `NIL`. + /// Unlike Godot, this will return `None` if the key does not exist and `Some(key)` if found. /// /// This operation is rarely needed and very inefficient. If you find yourself needing it a lot, consider /// using a `HashMap` or `Dictionary` with the inverse mapping (`V` -> `K`). /// /// _Godot equivalent: `find_key`_ #[doc(alias = "find_key")] - pub fn find_key_by_value(&self, value: V) -> Option { + pub fn find_key_by_value(&self, value: impl AsVArg) -> Option + where + K: FromGodot, + { + meta::varg_into_ref!(value: V); let key = self.as_inner().find_key(&value.to_variant()); - if !key.is_nil() || self.contains_key(key.clone()) { - Some(key) + if !key.is_nil() || self.as_inner().has(&key) { + Some(K::from_variant(&key)) } else { None } @@ -241,7 +288,6 @@ impl VarDictionary { /// Removes all key-value pairs from the dictionary. pub fn clear(&mut self) { self.balanced_ensure_mutable(); - self.as_inner().clear() } @@ -249,28 +295,36 @@ impl VarDictionary { /// /// If you are interested in the previous value, use [`insert()`][Self::insert] instead. /// + /// For `VarDictionary` (or partially-typed dictionaries with `Variant` key/value), this method + /// accepts any `impl ToGodot` for the Variant positions, thanks to blanket `AsVArg` impls. + /// /// _Godot equivalent: `dict[key] = value`_ - pub fn set(&mut self, key: K, value: V) { + pub fn set(&mut self, key: impl AsVArg, value: impl AsVArg) { self.balanced_ensure_mutable(); - let key = key.to_variant(); + meta::varg_into_ref!(key: K); + meta::varg_into_ref!(value: V); - // SAFETY: `self.get_ptr_mut(key)` always returns a valid pointer to a value in the dictionary; either pre-existing or newly inserted. - unsafe { - value.to_variant().move_into_var_ptr(self.get_ptr_mut(key)); - } + // SAFETY: K and V strongly typed. + unsafe { self.set_variant(key.to_variant(), value.to_variant()) }; } /// Insert a value at the given key, returning the previous value for that key (if available). /// /// If you don't need the previous value, use [`set()`][Self::set] instead. #[must_use] - pub fn insert(&mut self, key: K, value: V) -> Option { + pub fn insert(&mut self, key: impl AsVArg, value: impl AsVArg) -> Option { self.balanced_ensure_mutable(); - let key = key.to_variant(); - let old_value = self.get(key.clone()); - self.set(key, value); + meta::varg_into_ref!(key: K); + meta::varg_into_ref!(value: V); + + let key_variant = key.to_variant(); + let old_value = self.take_old_value(&key_variant); + + // SAFETY: K and V strongly typed. + unsafe { self.set_variant(key_variant, value.to_variant()) }; + old_value } @@ -279,12 +333,14 @@ impl VarDictionary { /// /// _Godot equivalent: `erase`_ #[doc(alias = "erase")] - pub fn remove(&mut self, key: K) -> Option { + pub fn remove(&mut self, key: impl AsVArg) -> Option { self.balanced_ensure_mutable(); - let key = key.to_variant(); - let old_value = self.get(key.clone()); - self.as_inner().erase(&key); + meta::varg_into_ref!(key: K); + + let key_variant = key.to_variant(); + let old_value = self.take_old_value(&key_variant); + self.as_inner().erase(&key_variant); old_value } @@ -296,7 +352,7 @@ impl VarDictionary { /// /// _Godot equivalent: `keys`_ #[doc(alias = "keys")] - pub fn keys_array(&self) -> VarArray { + pub fn keys_array(&self) -> Array { // SAFETY: keys() returns an untyped array with element type Variant. let out_array = self.as_inner().keys(); unsafe { out_array.assume_type() } @@ -306,7 +362,7 @@ impl VarDictionary { /// /// _Godot equivalent: `values`_ #[doc(alias = "values")] - pub fn values_array(&self) -> VarArray { + pub fn values_array(&self) -> Array { // SAFETY: values() returns an untyped array with element type Variant. let out_array = self.as_inner().values(); unsafe { out_array.assume_type() } @@ -321,7 +377,9 @@ impl VarDictionary { pub fn extend_dictionary(&mut self, other: &Self, overwrite: bool) { self.balanced_ensure_mutable(); - self.as_inner().merge(other, overwrite) + // SAFETY: merge() will only write values gotten from `other` into `self`, and all values in `other` are guaranteed + // to be of type K and V respectively. + self.as_inner().merge(other.as_var_dictionary(), overwrite) } /// Deep copy, duplicating nested collections. @@ -329,12 +387,15 @@ impl VarDictionary { /// All nested arrays and dictionaries are duplicated and will not be shared with the original dictionary. /// Note that any `Object`-derived elements will still be shallow copied. /// - /// To create a shallow copy, use [`Self::duplicate_shallow()`] instead. + /// To create a shallow copy, use [`Self::duplicate_shallow()`] instead. /// To create a new reference to the same dictionary data, use [`clone()`][Clone::clone]. /// /// _Godot equivalent: `dict.duplicate(true)`_ pub fn duplicate_deep(&self) -> Self { - self.as_inner().duplicate(true).with_cache(self) + let dup = self.as_inner().duplicate(true); + // SAFETY: duplicate() returns a typed dictionary with the same type as Self, and all values are taken from `self` so have the right type. + let result = unsafe { Self::assume_type(dup) }; + result.with_cache(self) } /// Shallow copy, copying elements but sharing nested collections. @@ -347,7 +408,38 @@ impl VarDictionary { /// /// _Godot equivalent: `dict.duplicate(false)`_ pub fn duplicate_shallow(&self) -> Self { - self.as_inner().duplicate(false).with_cache(self) + let dup = self.as_inner().duplicate(false); + // SAFETY: duplicate() returns a typed dictionary with the same type as Self, and all values are taken from `self` so have the right type. + let result = unsafe { Self::assume_type(dup) }; + result.with_cache(self) + } + + /// Changes the type parameter without runtime checks, consuming the dictionary. + /// + /// # Safety + /// - Values written to dictionary must match runtime type. + /// - Values read must be convertible to types `K` and `V`. + /// - If runtime type matches `K` and `V`, both conditions hold automatically. + /// + /// This method has the same memory layout requirements as [`Array::assume_type`]. + // TODO(v0.5): fragile manual field move + mem::forget; if a field is added, it must be moved here too. + // Consider transmute (requires #[repr(C)]) or ManuallyDrop + ptr::read. Same issue in Array::assume_type. + unsafe fn assume_type(dict: VarDictionary) -> Self { + let result = Self { + opaque: dict.opaque, + _phantom: PhantomData, + cached_key_type: OnceCell::new(), + cached_value_type: OnceCell::new(), + }; + + // Transfer cached types to avoid redundant FFI calls. + ElementType::transfer_cache(&dict.cached_key_type, &result.cached_key_type); + ElementType::transfer_cache(&dict.cached_value_type, &result.cached_value_type); + + // Prevent drop of dict since we moved opaque. + std::mem::forget(dict); + + result } /// Returns an iterator over the key-value pairs of the `Dictionary`. @@ -360,7 +452,7 @@ impl VarDictionary { /// /// Use `dict.iter_shared().typed::()` to iterate over `(K, V)` pairs instead. pub fn iter_shared(&self) -> Iter<'_> { - Iter::new(self) + Iter::new(self.as_var_dictionary()) } /// Returns an iterator over the keys in a `Dictionary`. @@ -373,7 +465,7 @@ impl VarDictionary { /// /// Use `dict.keys_shared().typed::()` to iterate over `K` keys instead. pub fn keys_shared(&self) -> Keys<'_> { - Keys::new(self) + Keys::new(self.as_var_dictionary()) } /// Turns the dictionary into a shallow-immutable dictionary. @@ -383,15 +475,6 @@ impl VarDictionary { /// /// In GDScript, dictionaries are automatically read-only if declared with the `const` keyword. /// - /// # Semantics and alternatives - /// You can use this in Rust, but the behavior of mutating methods is only validated in a best-effort manner (more than in GDScript though): - /// some methods like `set()` panic in Debug mode, when used on a read-only dictionary. There is no guarantee that any attempts to change - /// result in feedback; some may silently do nothing. - /// - /// In Rust, you can use shared references (`&Dictionary`) to prevent mutation. Note however that `Clone` can be used to create another - /// reference, through which mutation can still occur. For deep-immutable dictionaries, you'll need to keep your `Dictionary` encapsulated - /// or directly use Rust data structures. - /// /// _Godot equivalent: `make_read_only`_ #[doc(alias = "make_read_only")] pub fn into_read_only(self) -> Self { @@ -407,6 +490,15 @@ impl VarDictionary { self.as_inner().is_read_only() } + /// Converts this typed `Dictionary` into an `AnyDictionary`. + /// + /// Typically, you can use deref coercion to convert `&Dictionary` to `&AnyDictionary`. + /// This method is useful if you need `AnyDictionary` by value. + /// It consumes `self` to avoid incrementing the reference count; use `clone()` if you use the original dictionary further. + pub fn upcast_any_dictionary(self) -> AnyDictionary { + AnyDictionary::from_typed_or_untyped(self) + } + /// Best-effort mutability check. /// /// # Panics (safeguards-balanced) @@ -420,110 +512,319 @@ impl VarDictionary { /// Returns the runtime element type information for keys in this dictionary. /// - /// Provides information about Godot typed dictionaries, even though godot-rust currently doesn't implement generics for those. + /// # Compatibility /// - /// The result is generally cached, so feel free to call this method repeatedly. + /// **Godot 4.4+**: Returns the type information stored in the Godot engine. /// - /// # Panics (Debug) - /// In the astronomically rare case where another extension in Godot modifies a dictionary's key type (which godot-rust already cached as `Untyped`) - /// via C function `dictionary_set_typed`, thus leading to incorrect cache values. Such bad practice of not typing dictionaries immediately on - /// construction is not supported, and will not be checked in Release mode. - #[cfg(since_api = "4.4")] + /// **Before Godot 4.4**: Returns the Rust-side compile-time type `K` as `ElementType::Untyped` for `Variant`, + /// or the appropriate typed `ElementType` for other types. Since typed dictionaries are not supported by the + /// engine before 4.4, all dictionaries appear untyped to Godot regardless of this value. pub fn key_element_type(&self) -> ElementType { - ElementType::get_or_compute_cached( - &self.cached_key_type, - || self.as_inner().get_typed_key_builtin(), - || self.as_inner().get_typed_key_class_name(), - || self.as_inner().get_typed_key_script(), - ) + #[cfg(since_api = "4.4")] + { + ElementType::get_or_compute_cached( + &self.cached_key_type, + || self.as_inner().get_typed_key_builtin(), + || self.as_inner().get_typed_key_class_name(), + || self.as_inner().get_typed_key_script(), + ) + } + + #[cfg(before_api = "4.4")] + { + // Return Rust's compile-time type info (cached). + self.cached_key_type + .get_or_init(|| ElementType::of::()) + .clone() + } } /// Returns the runtime element type information for values in this dictionary. /// - /// Provides information about Godot typed dictionaries, even though godot-rust currently doesn't implement generics for those. + /// # Compatibility /// - /// The result is generally cached, so feel free to call this method repeatedly. + /// **Godot 4.4+**: Returns the type information stored in the Godot engine. /// - /// # Panics (Debug) - /// In the astronomically rare case where another extension in Godot modifies a dictionary's value type (which godot-rust already cached as `Untyped`) - /// via C function `dictionary_set_typed`, thus leading to incorrect cache values. Such bad practice of not typing dictionaries immediately on - /// construction is not supported, and will not be checked in Release mode. - #[cfg(since_api = "4.4")] + /// **Before Godot 4.4**: Returns the Rust-side compile-time type `V` as `ElementType::Untyped` for `Variant`, + /// or the appropriate typed `ElementType` for other types. Since typed dictionaries are not supported by the + /// engine before 4.4, all dictionaries appear untyped to Godot regardless of this value. pub fn value_element_type(&self) -> ElementType { - ElementType::get_or_compute_cached( - &self.cached_value_type, - || self.as_inner().get_typed_value_builtin(), - || self.as_inner().get_typed_value_class_name(), - || self.as_inner().get_typed_value_script(), - ) + #[cfg(since_api = "4.4")] + { + ElementType::get_or_compute_cached( + &self.cached_value_type, + || self.as_inner().get_typed_value_builtin(), + || self.as_inner().get_typed_value_class_name(), + || self.as_inner().get_typed_value_script(), + ) + } + + #[cfg(before_api = "4.4")] + { + // Return Rust's compile-time type info (cached). + self.cached_value_type + .get_or_init(|| ElementType::of::()) + .clone() + } } #[doc(hidden)] pub fn as_inner(&self) -> inner::InnerDictionary<'_> { - inner::InnerDictionary::from_outer(self) + inner::InnerDictionary::from_outer(self.as_var_dictionary()) + } + + /// Casts this dictionary to a reference to an untyped `VarDictionary`. + /// + /// # Safety + /// This method performs a simple pointer cast. The memory layout of `Dictionary` is identical to `VarDictionary`. + /// However, operations that write to the resulting `VarDictionary` must ensure they write values compatible with `K` and `V`. + // FIXME(v0.5): unsound due to layout-incompatibility; no #[repr(C)]. Remove once engine APIs are migrated to accept &AnyDictionary instead of &VarDictionary. + fn as_var_dictionary(&self) -> &VarDictionary { + unsafe { &*(self as *const Self as *const VarDictionary) } } /// Get the pointer corresponding to the given key in the dictionary. /// /// If there exists no value at the given key, a `NIL` variant will be inserted for that key. - fn get_ptr_mut(&mut self, key: K) -> sys::GDExtensionVariantPtr { - let key = key.to_variant(); - + fn get_ptr_mut(&mut self, key: Variant) -> sys::GDExtensionVariantPtr { // Never a null pointer, since entry either existed already or was inserted above. // SAFETY: accessing an unknown key _mutably_ creates that entry in the dictionary, with value `NIL`. unsafe { interface_fn!(dictionary_operator_index)(self.sys_mut(), key.var_sys()) } } - /// Execute a function that creates a new dictionary, transferring cached element types if available. + /// Sets a key-value pair at the variant level. /// - /// This is a convenience helper for methods that create new dictionary instances and want to preserve - /// cached type information to avoid redundant FFI calls. + /// # Safety + /// `key` must hold type `K` and `value` must hold type `V`. + unsafe fn set_variant(&mut self, key: Variant, value: Variant) { + let ptr = self.get_ptr_mut(key); + + // SAFETY: `get_ptr_mut` always returns a valid pointer (creates entry if key is absent). + unsafe { value.move_into_var_ptr(ptr) }; + } + + /// Execute a function that creates a new dictionary, transferring cached element types if available. fn with_cache(self, source: &Self) -> Self { - // Transfer both key and value type caches independently ElementType::transfer_cache(&source.cached_key_type, &self.cached_key_type); ElementType::transfer_cache(&source.cached_value_type, &self.cached_value_type); self } + + /// Checks that the inner dictionary has the correct types set for storing keys of type `K` and values of type `V`. + /// + /// Only performs runtime checks on Godot 4.4+, where typed dictionaries are supported by the engine. + /// Before 4.4, this always succeeds since there are no engine-side types to check against. + #[cfg(since_api = "4.4")] + fn with_checked_type(self) -> Result { + use crate::meta::error::{DictionaryMismatch, FromGodotError}; + + let actual_key = self.key_element_type(); + let actual_value = self.value_element_type(); + let expected_key = ElementType::of::(); + let expected_value = ElementType::of::(); + + if actual_key.is_compatible_with(&expected_key) + && actual_value.is_compatible_with(&expected_value) + { + Ok(self) + } else { + let mismatch = DictionaryMismatch { + expected_key, + expected_value, + actual_key, + actual_value, + }; + Err(FromGodotError::BadDictionaryType(mismatch).into_error(self)) + } + } + + fn as_any_ref(&self) -> &AnyDictionary { + // SAFETY: + // - Dictionary and VarDictionary have identical memory layout. + // - AnyDictionary provides no "in" operations (moving data in) that could violate covariance. + unsafe { std::mem::transmute::<&Dictionary, &AnyDictionary>(self) } + } + + fn as_any_mut(&mut self) -> &mut AnyDictionary { + // SAFETY: + // - Dictionary and VarDictionary have identical memory layout. + // - AnyDictionary is #[repr(transparent)] around VarDictionary. + // - Mutable operations on AnyDictionary work with Variant values, maintaining type safety through balanced_ensure_mutable() checks. + unsafe { std::mem::transmute::<&mut Dictionary, &mut AnyDictionary>(self) } + } + + /// # Safety + /// Does not validate the dictionary key/value types; `with_checked_type()` should be called afterward. + // Visibility: shared with AnyDictionary. + pub(super) unsafe fn unchecked_from_variant( + variant: &Variant, + ) -> Result { + use crate::builtin::VariantType; + use crate::meta::error::FromVariantError; + + let variant_type = variant.get_type(); + if variant_type != VariantType::DICTIONARY { + return Err(FromVariantError::BadType { + expected: VariantType::DICTIONARY, + actual: variant_type, + } + .into_error(variant.clone())); + } + + let result = unsafe { + Self::new_with_uninit(|self_ptr| { + let converter = sys::builtin_fn!(dictionary_from_variant); + converter(self_ptr, sys::SysPtr::force_mut(variant.var_sys())); + }) + }; + + Ok(result) + } + + /// Initialize the typed dictionary with key and value type information. + /// + /// On Godot 4.4+, this calls `dictionary_set_typed()` to inform the engine about types. + /// On earlier versions, this only initializes the Rust-side type cache. + fn init_inner_type(&mut self) { + let key_elem_ty = ElementType::of::(); + let value_elem_ty = ElementType::of::(); + + // Cache types on Rust side (for all versions) -- they are Copy. + self.cached_key_type.get_or_init(|| key_elem_ty); + self.cached_value_type.get_or_init(|| value_elem_ty); + + // If both are untyped (Variant), skip initialization. + if !key_elem_ty.is_typed() && !value_elem_ty.is_typed() { + return; + } + + // Godot 4.4+: Set type information in the engine. + #[cfg(since_api = "4.4")] + { + // Script is always nil for compile-time types (only relevant for GDScript class_name types). + let script = Variant::nil(); + + let empty_string_name = crate::builtin::StringName::default(); + let key_class_name = key_elem_ty.class_name_sys_or(&empty_string_name); + let value_class_name = value_elem_ty.class_name_sys_or(&empty_string_name); + + // SAFETY: Valid pointers are passed in. + // Relevant for correctness, not safety: the dictionary is a newly created, empty, untyped dictionary. + unsafe { + interface_fn!(dictionary_set_typed)( + self.sys_mut(), + key_elem_ty.variant_type().sys(), + key_class_name, + script.var_sys(), + value_elem_ty.variant_type().sys(), + value_class_name, + script.var_sys(), + ); + } + } + + // Before Godot 4.4: No engine-side typing, only Rust-side (already cached above). + #[cfg(before_api = "4.4")] + { + // Types are already cached at the beginning of this function. + // No additional work needed - Rust-only type safety. + } + } +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// V=Variant specialization + +impl Dictionary { + /// Returns the value for the given key, or `Variant::nil()` if the key is absent. + /// + /// This does _not_ distinguish between absent keys and keys mapped to `NIL` -- both return `Variant::nil()`. + /// Use [`get()`][Self::get] if you need to tell them apart. + /// + /// _Godot equivalent: `dict.get(key)` (1-arg overload)_ + /// + /// # `AnyDictionary` + /// This method is deliberately absent from [`AnyDictionary`][super::AnyDictionary]. Because `Dictionary` implements + /// `Deref`, any method on `AnyDictionary` is inherited by _all_ dictionaries -- including typed ones + /// like `Dictionary`, where a `Variant` return would be surprising. + pub fn get_or_nil(&self, key: impl AsVArg) -> Variant { + meta::varg_into_ref!(key: K); + let key_variant = key.to_variant(); + self.as_inner().get(&key_variant, &Variant::nil()) + } } // ---------------------------------------------------------------------------------------------------------------------------------------------- // Traits -// SAFETY: -// - `move_return_ptr` -// Nothing special needs to be done beyond a `std::mem::swap` when returning a dictionary. -// So we can just use `ffi_methods`. -// -// - `from_arg_ptr` -// Dictionaries are properly initialized through a `from_sys` call, but the ref-count should be -// incremented as that is the callee's responsibility. Which we do by calling -// `std::mem::forget(dictionary.clone())`. -unsafe impl GodotFfi for VarDictionary { +unsafe impl GodotFfi for Dictionary { const VARIANT_TYPE: ExtVariantType = ExtVariantType::Concrete(sys::VariantType::DICTIONARY); ffi_methods! { type sys::GDExtensionTypePtr = *mut Opaque; .. } } -crate::meta::impl_godot_as_self!(VarDictionary: ByRef); +impl std::ops::Deref for Dictionary { + type Target = AnyDictionary; + + fn deref(&self) -> &Self::Target { + self.as_any_ref() + } +} + +impl std::ops::DerefMut for Dictionary { + fn deref_mut(&mut self) -> &mut Self::Target { + self.as_any_mut() + } +} + +// Compile-time validation of layout compatibility. +sys::static_assert_eq_size_align!(Dictionary, VarDictionary); +sys::static_assert_eq_size_align!(Dictionary, VarDictionary); +sys::static_assert_eq_size_align!(VarDictionary, AnyDictionary); + +impl Default for Dictionary { + #[inline] + fn default() -> Self { + // Create an empty untyped dictionary first (typing happens in new()). + unsafe { + Self::new_with_uninit(|self_ptr| { + let ctor = sys::builtin_fn!(dictionary_construct_default); + ctor(self_ptr, ptr::null_mut()) + }) + } + } +} -impl_builtin_traits! { - for VarDictionary { - Default => dictionary_construct_default; - Drop => dictionary_destroy; - PartialEq => dictionary_operator_equal; - // No < operator for dictionaries. - // Hash could be added, but without Eq it's not that useful. +impl Drop for Dictionary { + fn drop(&mut self) { + // SAFETY: destructor is valid for self. + unsafe { sys::builtin_fn!(dictionary_destroy)(self.sys_mut()) } } } -impl fmt::Debug for VarDictionary { +impl PartialEq for Dictionary { + fn eq(&self, other: &Self) -> bool { + // SAFETY: equality check is valid. + unsafe { + let mut result = false; + sys::builtin_call! { + dictionary_operator_equal(self.sys(), other.sys(), result.sys_mut()) + } + result + } + } +} + +// Note: PartialOrd is intentionally NOT implemented for Dictionary. +// Unlike arrays, dictionaries do not have a natural ordering in Godot (no dictionary_operator_less). + +impl fmt::Debug for Dictionary { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:?}", self.to_variant().stringify()) + write!(f, "{:?}", self.as_var_dictionary().to_variant().stringify()) } } -impl fmt::Display for VarDictionary { - /// Formats `Dictionary` to match Godot's string representation. +impl fmt::Display for Dictionary { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{{ ")?; for (count, (key, value)) in self.iter_shared().enumerate() { @@ -536,12 +837,7 @@ impl fmt::Display for VarDictionary { } } -/// Creates a new reference to the data in this dictionary. Changes to the original dictionary will be -/// reflected in the copy and vice versa. -/// -/// To create a (mostly) independent copy instead, see [`VarDictionary::duplicate_shallow()`] and -/// [`VarDictionary::duplicate_deep()`]. -impl Clone for VarDictionary { +impl Clone for Dictionary { fn clone(&self) -> Self { // SAFETY: `self` is a valid dictionary, since we have a reference that keeps it alive. let result = unsafe { @@ -558,43 +854,187 @@ impl Clone for VarDictionary { // ---------------------------------------------------------------------------------------------------------------------------------------------- // Conversion traits -/// Creates a dictionary from the given iterator `I` over a `(&K, &V)` key-value pair. -/// -/// Each key and value are converted to a `Variant`. -impl<'a, 'b, K, V, I> From for VarDictionary -where - I: IntoIterator, - K: ToGodot + 'a, - V: ToGodot + 'b, -{ - fn from(iterable: I) -> Self { - iterable - .into_iter() - .map(|(key, value)| (key.to_variant(), value.to_variant())) - .collect() - } -} - /// Insert iterator range into dictionary. /// /// Inserts all key-value pairs from the iterator into the dictionary. Previous values for keys appearing /// in `iter` will be overwritten. -impl Extend<(K, V)> for VarDictionary { +impl Extend<(K, V)> for Dictionary { fn extend>(&mut self, iter: I) { for (k, v) in iter.into_iter() { - self.set(k.to_variant(), v.to_variant()) + // Inline set logic to avoid generic owned_into_varg() (which can't resolve T::Pass). + self.balanced_ensure_mutable(); + + // SAFETY: K and V strongly typed. + unsafe { self.set_variant(k.to_variant(), v.to_variant()) }; } } } -impl FromIterator<(K, V)> for VarDictionary { +/// Creates a `Dictionary` from an iterator over key-value pairs. +impl FromIterator<(K, V)> for Dictionary { fn from_iter>(iter: I) -> Self { - let mut dict = VarDictionary::new(); + let mut dict = Dictionary::new(); dict.extend(iter); dict } } +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// GodotConvert/ToGodot/FromGodot for Dictionary + +impl meta::sealed::Sealed for Dictionary {} + +impl meta::GodotConvert for Dictionary { + type Via = Self; +} + +impl ToGodot for Dictionary { + type Pass = meta::ByRef; + + fn to_godot(&self) -> &Self::Via { + self + } +} + +impl FromGodot for Dictionary { + fn try_from_godot(via: Self::Via) -> Result { + // For typed dictionaries, we should validate that the types match. + // VarDictionary (K=V=Variant) always matches. + Ok(via) + } +} + +impl meta::GodotFfiVariant for Dictionary { + fn ffi_to_variant(&self) -> Variant { + unsafe { + Variant::new_with_var_uninit(|variant_ptr| { + let converter = sys::builtin_fn!(dictionary_to_variant); + converter(variant_ptr, sys::SysPtr::force_mut(self.sys())); + }) + } + } + + fn ffi_from_variant(variant: &Variant) -> Result { + // SAFETY: if conversion succeeds, we call with_checked_type() afterwards. + let result = unsafe { Self::unchecked_from_variant(variant) }?; + + // On Godot 4.4+, check that the runtime types match the compile-time types. + #[cfg(since_api = "4.4")] + { + result.with_checked_type() + } + + #[cfg(before_api = "4.4")] + Ok(result) + } +} + +impl meta::GodotType for Dictionary { + type Ffi = Self; + + type ToFfi<'f> + = meta::RefArg<'f, Dictionary> + where + Self: 'f; + + fn to_ffi(&self) -> Self::ToFfi<'_> { + meta::RefArg::new(self) + } + + fn into_ffi(self) -> Self::Ffi { + self + } + + fn try_from_ffi(ffi: Self::Ffi) -> Result { + Ok(ffi) + } + + fn godot_type_name() -> String { + "Dictionary".to_string() + } + + fn property_hint_info() -> meta::PropertyHintInfo { + // On Godot 4.4+, typed dictionaries use DICTIONARY_TYPE hint. + #[cfg(since_api = "4.4")] + if is_dictionary_typed::() { + return meta::PropertyHintInfo::var_dictionary_element::(); + } + + // Untyped dictionary or before 4.4: no hints. + meta::PropertyHintInfo::none() + } +} + +impl ArrayElement for Dictionary {} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// Var/Export implementations for Dictionary + +/// Check if Dictionary is typed (at least one of K or V is not Variant). +#[inline] +fn is_dictionary_typed() -> bool { + // Nil means "untyped" or "Variant" in Godot. + meta::element_variant_type::() != VariantType::NIL + || meta::element_variant_type::() != VariantType::NIL +} + +impl crate::registry::property::Var for Dictionary { + type PubType = Self; + + fn var_get(field: &Self) -> Self::Via { + field.clone() + } + + fn var_set(field: &mut Self, value: Self::Via) { + *field = value; + } + + fn var_pub_get(field: &Self) -> Self::PubType { + field.clone() + } + + fn var_pub_set(field: &mut Self, value: Self::PubType) { + *field = value; + } + + fn var_hint() -> meta::PropertyHintInfo { + // On Godot 4.4+, typed dictionaries use DICTIONARY_TYPE hint. + #[cfg(since_api = "4.4")] + if is_dictionary_typed::() { + return meta::PropertyHintInfo::var_dictionary_element::(); + } + + // Untyped dictionary or before 4.4: no hints. + meta::PropertyHintInfo::none() + } +} + +impl crate::registry::property::Export for Dictionary +where + K: ArrayElement + crate::registry::property::Export, + V: ArrayElement + crate::registry::property::Export, +{ + fn export_hint() -> meta::PropertyHintInfo { + // VarDictionary: use "Dictionary". + if !is_dictionary_typed::() { + return meta::PropertyHintInfo::type_name::(); + } + + // On Godot 4.4+, typed dictionaries use DICTIONARY_TYPE hint for export. + #[cfg(since_api = "4.4")] + return meta::PropertyHintInfo::export_dictionary_element::(); + + // Before 4.4, no engine-side typed dictionary hints. + #[cfg(before_api = "4.4")] + meta::PropertyHintInfo::none() + } +} + +impl crate::registry::property::BuiltinExport + for Dictionary +{ +} + // ---------------------------------------------------------------------------------------------------------------------------------------------- /// Internal helper for different iterator impls -- not an iterator itself @@ -633,7 +1073,7 @@ impl<'a> DictionaryIter<'a> { fn next_key_value(&mut self) -> Option<(Variant, Variant)> { let key = self.next_key()?; - if !self.dictionary.contains_key(key.clone()) { + if !self.dictionary.as_inner().has(&key) { return None; } @@ -642,8 +1082,7 @@ impl<'a> DictionaryIter<'a> { } fn size_hint(&self) -> (usize, Option) { - // Need to check for underflow in case any entry was removed while - // iterating (i.e. next_index > dicitonary.len()) + // Check for underflow in case any entry was removed while iterating; i.e. next_index > dicitonary.len(). let remaining = usize::saturating_sub(self.dictionary.len(), self.next_idx); (remaining, Some(remaining)) @@ -720,7 +1159,7 @@ impl<'a> Iter<'a> { iter: DictionaryIter::new(dictionary), } } - + // /// Creates an iterator that converts each `(Variant, Variant)` key-value pair into a `(K, V)` key-value /// pair, panicking upon conversion failure. pub fn typed(self) -> TypedIter<'a, K, V> { @@ -861,6 +1300,51 @@ fn u8_to_bool(u: u8) -> bool { // ---------------------------------------------------------------------------------------------------------------------------------------------- +/// Constructs typed [`Dictionary`] literals, close to Godot's own syntax. +/// +/// Any value can be used as a key, but to use an expression you need to surround it +/// in `()` or `{}`. +/// +/// # Type annotation +/// The macro creates a typed `Dictionary`. You must provide an explicit type annotation +/// to specify `K` and `V`. Keys must implement `AsArg` and values must implement `AsArg`. +/// +/// # Example +/// ```no_run +/// use godot::builtin::{dict, Dictionary, GString, Variant}; +/// +/// // Type annotation required +/// let d: Dictionary = dict! { +/// "key1": 10, +/// "key2": 20, +/// }; +/// +/// // Works with Variant values too +/// let d: Dictionary = dict! { +/// "str": "Hello", +/// "num": 23, +/// }; +/// ``` +/// +/// # See also +/// +/// For untyped dictionaries, use [`vdict!`][macro@crate::builtin::vdict]. +/// For arrays, similar macros [`array!`][macro@crate::builtin::array] and [`varray!`][macro@crate::builtin::varray] exist. +#[macro_export] +macro_rules! dict { + ($($key:tt: $value:expr),* $(,)?) => { + { + let mut d = $crate::builtin::Dictionary::new(); + $( + // `cargo check` complains that `(1 + 2): true` has unused parentheses, even though it's not possible to omit those. + #[allow(unused_parens)] + d.set($key, $value); + )* + d + } + }; +} + /// Constructs [`VarDictionary`] literals, close to Godot's own syntax. /// /// Any value can be used as a key, but to use an expression you need to surround it @@ -881,29 +1365,22 @@ fn u8_to_bool(u: u8) -> bool { /// /// # See also /// +/// For typed dictionaries, use [`dict!`][macro@crate::builtin::dict]. /// For arrays, similar macros [`array!`][macro@crate::builtin::array] and [`varray!`][macro@crate::builtin::varray] exist. +// TODO(v0.5): unify vdict!/dict! macro implementations; vdict! manually calls to_variant() while dict! uses AsVArg. #[macro_export] macro_rules! vdict { ($($key:tt: $value:expr_2021),* $(,)?) => { { - let mut d = $crate::builtin::VarDictionary::new(); + use $crate::meta::ToGodot as _; + let mut dict = $crate::builtin::VarDictionary::new(); $( // `cargo check` complains that `(1 + 2): true` has unused parens, even though it's not // possible to omit the parens. #[allow(unused_parens)] - d.set($key, $value); + dict.set(&$key.to_variant(), &$value.to_variant()); )* - d + dict } }; } - -#[macro_export] -#[deprecated = "Migrate to `vdict!`. The name `dict!` will be used in the future for typed dictionaries."] -macro_rules! dict { - ($($key:tt: $value:expr_2021),* $(,)?) => { - $crate::vdict!( - $($key: $value),* - ) - }; -} diff --git a/godot-core/src/builtin/collections/mod.rs b/godot-core/src/builtin/collections/mod.rs index 9cf79e220..d927ef37a 100644 --- a/godot-core/src/builtin/collections/mod.rs +++ b/godot-core/src/builtin/collections/mod.rs @@ -6,6 +6,7 @@ */ mod any_array; +mod any_dictionary; mod array; mod array_functional_ops; mod dictionary; @@ -16,6 +17,7 @@ mod packed_array_element; // Re-export in godot::builtin. pub(crate) mod containers { pub use super::any_array::AnyArray; + pub use super::any_dictionary::AnyDictionary; pub use super::array::{Array, VarArray}; #[allow(deprecated)] pub use super::dictionary::Dictionary; diff --git a/godot-core/src/builtin/variant/impls.rs b/godot-core/src/builtin/variant/impls.rs index ea641e6f9..7130c3fc4 100644 --- a/godot-core/src/builtin/variant/impls.rs +++ b/godot-core/src/builtin/variant/impls.rs @@ -150,7 +150,6 @@ mod impls { impl_ffi_variant!(ref GString, string_to_variant, string_from_variant; String); impl_ffi_variant!(ref StringName, string_name_to_variant, string_name_from_variant); impl_ffi_variant!(ref NodePath, node_path_to_variant, node_path_from_variant); - impl_ffi_variant!(ref VarDictionary, dictionary_to_variant, dictionary_from_variant; Dictionary); impl_ffi_variant!(ref Signal, signal_to_variant, signal_from_variant); impl_ffi_variant!(ref Callable, callable_to_variant, callable_from_variant); } diff --git a/godot-core/src/builtin/variant/mod.rs b/godot-core/src/builtin/variant/mod.rs index 624f6756e..7258ca2e6 100644 --- a/godot-core/src/builtin/variant/mod.rs +++ b/godot-core/src/builtin/variant/mod.rs @@ -531,7 +531,7 @@ unsafe impl GodotFfi for Variant { ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } } -crate::meta::impl_godot_as_self!(Variant: ByRef); +crate::meta::impl_godot_as_self!(Variant: ByVariant); impl Default for Variant { fn default() -> Self { diff --git a/godot-core/src/meta/args/as_arg.rs b/godot-core/src/meta/args/as_arg.rs index a386c53f6..74404eb25 100644 --- a/godot-core/src/meta/args/as_arg.rs +++ b/godot-core/src/meta/args/as_arg.rs @@ -5,7 +5,8 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use crate::builtin::{GString, NodePath, StringName, Variant}; +use crate::builtin::{Callable, GString, NodePath, Signal, StringName, Variant}; +use crate::meta; use crate::meta::sealed::Sealed; use crate::meta::traits::{GodotFfiVariant, GodotNullableFfi}; use crate::meta::{CowArg, EngineToGodot, FfiArg, GodotType, ObjectArg, ToGodot}; @@ -118,9 +119,19 @@ where } } +// Variant has `Pass = ByVariant` (not ByRef), so it's not covered by the ByRef blanket above. +impl AsArg for &Variant { + fn into_arg<'arg>(self) -> CowArg<'arg, Variant> + where + Self: 'arg, + { + CowArg::Borrowed(self) + } +} + impl AsArg for T where - T: ToGodot, + T: ToGodot + Sized, // Sized may rule out some coherence issues. { fn into_arg<'arg>(self) -> CowArg<'arg, T> where @@ -488,6 +499,18 @@ macro_rules! arg_into_owned { }; } +/// Converts `impl AsVArg` into a locally valid `&T`. +/// +/// Analogous to [`arg_into_ref`][crate::arg_into_ref], but for [`AsVArg`][meta::AsVArg]. +#[macro_export] +#[doc(hidden)] +macro_rules! varg_into_ref { + ($arg_variable:ident: $T:ty) => { + let $arg_variable = $arg_variable.into_varg(); + let $arg_variable: &$T = $arg_variable.cow_as_ref(); + }; +} + #[macro_export] macro_rules! declare_arg_method { ($ ($docs:tt)+ ) => { @@ -529,49 +552,24 @@ where // ---------------------------------------------------------------------------------------------------------------------------------------------- // GString -// Note: for all string types S, `impl AsArg for &mut String` is not yet provided, but we can add them if needed. - -impl AsArg for &str { - fn into_arg<'arg>(self) -> CowArg<'arg, GString> { - CowArg::Owned(GString::from(self)) - } -} - -impl AsArg for &String { - fn into_arg<'arg>(self) -> CowArg<'arg, GString> { - CowArg::Owned(GString::from(self)) - } -} - -// ---------------------------------------------------------------------------------------------------------------------------------------------- -// StringName - -impl AsArg for &str { - fn into_arg<'arg>(self) -> CowArg<'arg, StringName> { - CowArg::Owned(StringName::from(self)) - } -} - -impl AsArg for &String { - fn into_arg<'arg>(self) -> CowArg<'arg, StringName> { - CowArg::Owned(StringName::from(self)) - } -} - -// ---------------------------------------------------------------------------------------------------------------------------------------------- -// NodePath - -impl AsArg for &str { - fn into_arg<'arg>(self) -> CowArg<'arg, NodePath> { - CowArg::Owned(NodePath::from(self)) - } +macro_rules! impl_asarg_string { + ($Target:ty) => { + impl AsArg<$Target> for &str { + fn into_arg<'arg>(self) -> CowArg<'arg, $Target> { + CowArg::Owned(<$Target>::from(self)) + } + } + impl AsArg<$Target> for &String { + fn into_arg<'arg>(self) -> CowArg<'arg, $Target> { + CowArg::Owned(<$Target>::from(self.as_str())) + } + } + }; } -impl AsArg for &String { - fn into_arg<'arg>(self) -> CowArg<'arg, NodePath> { - CowArg::Owned(NodePath::from(self)) - } -} +impl_asarg_string!(GString); +impl_asarg_string!(StringName); +impl_asarg_string!(NodePath); // ---------------------------------------------------------------------------------------------------------------------------------------------- // Argument passing (mutually exclusive by-val or by-ref). @@ -679,6 +677,39 @@ impl ArgPassing for ByRef { } } +/// Pass `Variant` arguments to Godot by reference. +/// +/// This is semantically identical to [`ByRef`], but exists as a separate type so that `Variant` has its own `Pass` type. +/// This decoupling enables blanket `AsVArg` impls for all `ByRef` types without coherence conflicts at `T = Variant`. +/// +/// See [`ToGodot::Pass`]. +pub enum ByVariant {} +impl Sealed for ByVariant {} +impl ArgPassing for ByVariant { + type Output<'r, T: 'r> = &'r T; + + type FfiOutput<'f, T> + = T::ToFfi<'f> + where + T: GodotType + 'f; + + fn ref_to_owned_via(value: &T) -> T::Via + where + T: EngineToGodot, + T::Via: Clone, + { + value.engine_to_godot().clone() + } + + fn ref_to_ffi(value: &T) -> ::ToFfi<'_> + where + T: EngineToGodot, + T::Via: GodotType, + { + GodotType::to_ffi(value.engine_to_godot()) + } +} + /// Pass arguments to Godot by object pointer (for objects only). /// /// Currently distinct from [`ByRef`] to not interfere with the blanket impl for `&T` for all `ByRef` types. Semantics are largely the same. @@ -796,3 +827,145 @@ pub type ToArg<'r, Via, Pass> = ::Output<'r, Via>; /// #[allow(dead_code)] struct PhantomAsArgDoctests; + +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// AsVArg trait + internal DisjointVArg helper trait for blanket impls + +/// Implicit conversions for arguments passed to collection APIs (e.g. `Dictionary`, `Array`). +/// +/// `AsVArg` is a superset of [`AsArg`]: all types accepted by `AsArg` are also accepted by `AsVArg`. +/// +/// Additionally, `AsVArg` is implemented for most types that have a `ToGodot` implementation. This enables implicit conversion to +/// `Variant` when passing to collection APIs, e.g. `dict.set("key", 42)` for `Dictionary`. +// +// Design: a direct blanket `impl, T: ToGodot> AsVArg for S` would conflict with the ByValue blanket impl, because the +// compiler can't prove the intersection is empty. We solve this using a helper trait `DisjointVArg` parameterized by `Pass`, exploiting the fact +// that impls differing by generic type parameters (rather than associated types) are always disjoint. +// See also "helper trait" coherence workaround: https://github.com/rust-lang/rfcs/pull/1672#issuecomment-1405377983 +pub trait AsVArg +where + Self: Sized, +{ + #[doc(hidden)] + fn into_varg<'arg>(self) -> CowArg<'arg, T> + where + Self: 'arg; +} + +// Bridge: single blanket connecting DisjointVArg to public AsVArg. +impl AsVArg for Arg +where + T: ToGodot, + Arg: DisjointVArg, +{ + fn into_varg<'arg>(self) -> CowArg<'arg, T> + where + Self: 'arg, + { + DisjointVArg::__disjoint_to_cow(self) + } +} + +/// Not part of the public API; used internally for [`AsVArg`] coherence dispatch. +/// +/// Provides the following impls: +/// * Blanket for all types supporting `ByRef`, `ByValue`, `ByObject` and `ByOption` pass strategies, directly forwarding to `AsArg`. +/// * Per-type for `&T` -> `Variant` for `ByVariant` pass strategy. +/// +/// `DisjointVArg` moves [`ToGodot::Pass`] (an associated type) into a generic parameter, so the trait solver sees impls keyed by different +/// `Pass` values as non-overlapping. This lets us write blanket forwarding impls (`AsArg` -> `AsVArg`) for most `By*` strategies without +/// individual `AsVArg` impls for every type. +/// +/// The one case it *doesn't* help with is non-variant `T` -> `Variant` conversions: since the bridge pins `Pass = T::Pass = ByVariant`, +/// all source types compete in the same `DisjointVArg` bucket. A blanket impl `for &A where A: ToGodot` would +/// overlap with `for B where B: ToGodot` (compiler can't prove `&A` isn't such a `B`). So, those require per-type impls, +/// macroified away. +#[doc(hidden)] +pub trait DisjointVArg { + #[doc(hidden)] + fn __disjoint_to_cow<'arg>(self) -> CowArg<'arg, T> + where + Self: 'arg; +} + +// ByValue: T -> Variant. +impl> DisjointVArg for T { + fn __disjoint_to_cow<'arg>(self) -> CowArg<'arg, Variant> + where + Self: 'arg, + { + CowArg::Owned(self.to_variant()) + } +} + +// ByVariant: &Variant -> Variant. +impl DisjointVArg for &Variant { + fn __disjoint_to_cow<'arg>(self) -> CowArg<'arg, Variant> + where + Self: 'arg, + { + CowArg::Borrowed(self) + } +} + +// CowArg helper (ByVariant, needed because CowArg is not AsArg). +impl DisjointVArg for CowArg<'_, Variant> { + fn __disjoint_to_cow<'arg>(self) -> CowArg<'arg, Variant> + where + Self: 'arg, + { + self + } +} + +// Unified macro for DisjointVArg impls following a common scheme. +macro_rules! impl_varg_variant { + // Blanket forward: any AsArg automatically works as AsVArg for a given pass strategy. + (forward [$($generic_params:tt)*] $Pass:ty $(where $($extra_bound:tt)*)?) => { + impl<$($generic_params)* S, T> DisjointVArg<$Pass, T> for S + where + S: AsArg, + T: ToGodot, + $($($extra_bound)*)? + { + fn __disjoint_to_cow<'arg>(self) -> CowArg<'arg, T> + where + Self: 'arg, + { + AsArg::into_arg(self) + } + } + }; + + // Per-type &T -> Variant for ByRef types (coherence workaround, see above). + (ref_to_variant [$($generic_params:tt)*] $T:ty) => { + impl<$($generic_params)*> DisjointVArg for &$T { + fn __disjoint_to_cow<'arg>(self) -> CowArg<'arg, Variant> + where + Self: 'arg, + { + CowArg::Owned(self.to_variant()) + } + } + }; +} + +// Blanket forwards for pass strategies except ByVariant, delegating to AsArg internally. +impl_varg_variant!(forward [] ByRef); // &T. +impl_varg_variant!(forward [] ByValue); // T. +impl_varg_variant!(forward [] ByObject); // &Gd, &DynGd. +impl_varg_variant!(forward [Via,] ByOption + where Via: GodotType, + for<'f> Via::ToFfi<'f>: GodotNullableFfi, +); + +// ByRef: &T -> Variant conversions. +impl_varg_variant!(ref_to_variant [] GString); +impl_varg_variant!(ref_to_variant [] StringName); +impl_varg_variant!(ref_to_variant [] NodePath); +impl_varg_variant!(ref_to_variant [] Callable); +impl_varg_variant!(ref_to_variant [] Signal); +impl_varg_variant!(ref_to_variant [T: meta::ArrayElement] crate::builtin::Array); +impl_varg_variant!(ref_to_variant [K: meta::ArrayElement, V: meta::ArrayElement] crate::builtin::Dictionary); +impl_varg_variant!(ref_to_variant [T: meta::PackedArrayElement] crate::builtin::PackedArray); +impl_varg_variant!(ref_to_variant [T: GodotClass] Gd); diff --git a/godot-core/src/meta/args/mod.rs b/godot-core/src/meta/args/mod.rs index 95b26db9a..42ff237b0 100644 --- a/godot-core/src/meta/args/mod.rs +++ b/godot-core/src/meta/args/mod.rs @@ -17,9 +17,12 @@ mod ref_arg; // Internal APIs // Solely public for itest/convert_test.rs. +#[doc(hidden)] +pub use as_arg::DisjointVArg; pub(crate) use as_arg::NullArg; pub use as_arg::{ - ArgPassing, AsArg, ByObject, ByOption, ByRef, ByValue, ToArg, owned_into_arg, ref_to_arg, + ArgPassing, AsArg, AsVArg, ByObject, ByOption, ByRef, ByValue, ByVariant, ToArg, + owned_into_arg, ref_to_arg, }; #[cfg(not(feature = "trace"))] pub(crate) use cow_arg::{CowArg, FfiArg}; diff --git a/godot-core/src/meta/element_type.rs b/godot-core/src/meta/element_type.rs index 488438c2d..6de547038 100644 --- a/godot-core/src/meta/element_type.rs +++ b/godot-core/src/meta/element_type.rs @@ -79,6 +79,43 @@ impl ElementType { } } + /// Returns the class name sys pointer for FFI calls like `array_set_typed` / `dictionary_set_typed`. + /// + /// If `self` has a class ID, returns its `string_sys()`. Otherwise, returns `fallback.string_sys()`. + /// The caller must keep `fallback` (typically `StringName::default()`) alive while the returned pointer is in use. + pub(crate) fn class_name_sys_or( + &self, + fallback: &crate::builtin::StringName, + ) -> crate::sys::GDExtensionConstStringNamePtr { + if let Some(class_id) = self.class_id() { + class_id.string_sys() + } else { + fallback.string_sys() + } + } + + /// Checks if `self` (runtime type) is compatible with `expected` (compile-time type). + /// + /// Returns `true` if: + /// - The types match exactly, OR + /// - `self` is a `ScriptClass` and `expected` is its native base `Class` + /// + /// This allows an `Array[Enemy]` from GDScript (where `Enemy extends RefCounted`) to be used as `Array>` in Rust. + /// TODO(v0.6): this breaks covariance -- consider using generic AnyArray> instead. + pub(crate) fn is_compatible_with(&self, expected: &ElementType) -> bool { + // Exact match. + if self == expected { + return true; + } + + // Script class (runtime) matches its native base class (compile-time). + matches!( + (self, expected), + (ElementType::ScriptClass(_), ElementType::Class(expected_class)) + if self.class_id().is_some_and(|id| id == *expected_class) + ) + } + /// Transfer cached element type from source to destination, preserving type info. /// /// Used by clone-like operations like `duplicate()`, `slice()`, etc. where we want to preserve cached type information to avoid diff --git a/godot-core/src/meta/error/convert_error.rs b/godot-core/src/meta/error/convert_error.rs index f15d09495..5bdeea71d 100644 --- a/godot-core/src/meta/error/convert_error.rs +++ b/godot-core/src/meta/error/convert_error.rs @@ -187,6 +187,10 @@ pub(crate) enum FromGodotError { /// Destination `Array` has different type than source's runtime type. BadArrayType(ArrayMismatch), + /// Destination `Dictionary` has different types than source's runtime types. + #[cfg(since_api = "4.4")] + BadDictionaryType(DictionaryMismatch), + /// Special case of `BadArrayType` where a custom int type such as `i8` cannot hold a dynamic `i64` value. #[cfg(safeguards_strict)] BadArrayTypeInt { @@ -224,6 +228,9 @@ impl fmt::Display for FromGodotError { match self { Self::BadArrayType(mismatch) => write!(f, "{mismatch}"), + #[cfg(since_api = "4.4")] + Self::BadDictionaryType(mismatch) => write!(f, "{mismatch}"), + #[cfg(safeguards_strict)] Self::BadArrayTypeInt { expected_int_type, @@ -280,6 +287,32 @@ impl fmt::Display for ArrayMismatch { } } +#[cfg(since_api = "4.4")] +#[derive(Eq, PartialEq, Debug)] +pub(crate) struct DictionaryMismatch { + pub expected_key: ElementType, + pub expected_value: ElementType, + pub actual_key: ElementType, + pub actual_value: ElementType, +} + +#[cfg(since_api = "4.4")] +impl fmt::Display for DictionaryMismatch { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let DictionaryMismatch { + expected_key, + expected_value, + actual_key, + actual_value, + } = self; + + write!( + f, + "expected dictionary of type Dictionary<{expected_key:?}, {expected_value:?}>, got Dictionary<{actual_key:?}, {actual_value:?}>" + ) + } +} + /// Conversion failed during a [`GodotType::try_from_ffi()`](crate::meta::GodotType::try_from_ffi()) call. #[derive(Eq, PartialEq, Debug)] #[non_exhaustive] diff --git a/godot-core/src/meta/godot_convert/mod.rs b/godot-core/src/meta/godot_convert/mod.rs index 6cc96a448..ed6535abe 100644 --- a/godot-core/src/meta/godot_convert/mod.rs +++ b/godot-core/src/meta/godot_convert/mod.rs @@ -265,4 +265,16 @@ macro_rules! impl_godot_as_self { } } }; + + (@to_godot $T:ty: ByVariant) => { + impl $crate::meta::ToGodot for $T { + type Pass = $crate::meta::ByVariant; + + #[inline] + fn to_godot(&self) -> &Self::Via { + self + } + } + }; + } diff --git a/godot-core/src/meta/mod.rs b/godot-core/src/meta/mod.rs index 4670ad8ed..4e446101c 100644 --- a/godot-core/src/meta/mod.rs +++ b/godot-core/src/meta/mod.rs @@ -91,7 +91,7 @@ mod reexport_crate { }; // Private imports for this module only. pub(super) use crate::registry::method::MethodParamOrReturnInfo; - pub(crate) use crate::{arg_into_ref, declare_arg_method, impl_godot_as_self}; + pub(crate) use crate::{arg_into_ref, declare_arg_method, impl_godot_as_self, varg_into_ref}; } pub(crate) use reexport_crate::*; diff --git a/godot-core/src/meta/property_info.rs b/godot-core/src/meta/property_info.rs index 94fca5163..ae4f7f407 100644 --- a/godot-core/src/meta/property_info.rs +++ b/godot-core/src/meta/property_info.rs @@ -13,7 +13,7 @@ use crate::meta::{ArrayElement, ClassId, GodotType, PackedArrayElement, element_ use crate::obj::{Bounds, EngineBitfield, EngineEnum, GodotClass, bounds}; use crate::registry::class::get_dyn_property_hint_string; use crate::registry::property::{Export, Var}; -use crate::{classes, sys}; +use crate::{classes, godot_str, sys}; /// Describes a property's type, name and metadata for Godot. /// @@ -374,6 +374,30 @@ impl PropertyHintInfo { } } + /// Use for `#[var]` properties on Godot 4.4+ -- [`PROPERTY_HINT_DICTIONARY_TYPE`](PropertyHint::DICTIONARY_TYPE) with + /// `"key_type;value_type"` as hint string. + #[cfg(since_api = "4.4")] + pub fn var_dictionary_element() -> Self { + Self { + hint: PropertyHint::DICTIONARY_TYPE, + hint_string: godot_str!( + "{};{}", + element_godot_type_name::(), + element_godot_type_name::() + ), + } + } + + /// Use for `#[export]` properties on Godot 4.4+ -- [`PROPERTY_HINT_TYPE_STRING`](PropertyHint::TYPE_STRING) with + /// `"key_type_string;value_type_string"` as hint string. + #[cfg(since_api = "4.4")] + pub fn export_dictionary_element() -> Self { + Self { + hint: PropertyHint::TYPE_STRING, + hint_string: godot_str!("{};{}", K::element_type_string(), V::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 { diff --git a/godot-core/src/meta/sealed.rs b/godot-core/src/meta/sealed.rs index ddbe4ab40..e620e40e6 100644 --- a/godot-core/src/meta/sealed.rs +++ b/godot-core/src/meta/sealed.rs @@ -44,7 +44,6 @@ impl Sealed for Rect2i {} impl Sealed for Signal {} impl Sealed for Transform2D {} impl Sealed for Transform3D {} -impl Sealed for VarDictionary {} impl Sealed for bool {} impl Sealed for i64 {} impl Sealed for i32 {} diff --git a/godot-core/src/registry/property/mod.rs b/godot-core/src/registry/property/mod.rs index caae94f84..756acf8ee 100644 --- a/godot-core/src/registry/property/mod.rs +++ b/godot-core/src/registry/property/mod.rs @@ -710,9 +710,6 @@ mod export_impls { impl_property_by_godot_convert!(NodePath); impl_property_by_godot_convert!(Color); - - // Dictionary: will need to be done manually once they become typed. - impl_property_by_godot_convert!(VarDictionary); impl_property_by_godot_convert!(Variant); // Primitives diff --git a/itest/rust/build.rs b/itest/rust/build.rs index 28196b84d..aff06a198 100644 --- a/itest/rust/build.rs +++ b/itest/rust/build.rs @@ -517,12 +517,26 @@ fn generate_property_template(inputs: &[Input]) -> PropertyTests { } }; + // Only available in Godot 4.4+. + let rust_exports_4_4 = if godot_bindings::before_api("4.4") { + TokenStream::new() + } else { + quote! { + #[var] + var_dict_string_int: Dictionary, + + #[export] + export_dict_string_int: Dictionary, + } + }; + let rust = quote! { #[derive(GodotClass)] #[class(base = Node, init)] pub struct PropertyTestsRust { #(#rust,)* #rust_exports_4_3 + #rust_exports_4_4 // All the @export_file/dir variants, with GString, Array and PackedStringArray. #[export(file)] @@ -625,11 +639,21 @@ fn generate_property_template(inputs: &[Input]) -> PropertyTests { @export_global_dir var export_global_dir_parray: PackedStringArray "#; + // Only available in Godot 4.4+. + let advanced_exports_4_4 = r#" +var var_dict_string_int: Dictionary[String, int] +@export var export_dict_string_int: Dictionary[String, int] + "#; + let mut gdscript = format!("{basic_exports}\n{advanced_exports}"); if godot_bindings::since_api("4.3") { gdscript.push('\n'); gdscript.push_str(advanced_exports_4_3); } + if godot_bindings::since_api("4.4") { + gdscript.push('\n'); + gdscript.push_str(advanced_exports_4_4); + } PropertyTests { rust, gdscript } } diff --git a/itest/rust/src/builtin_tests/containers/callable_test.rs b/itest/rust/src/builtin_tests/containers/callable_test.rs index 1b876831c..29fddb86d 100644 --- a/itest/rust/src/builtin_tests/containers/callable_test.rs +++ b/itest/rust/src/builtin_tests/containers/callable_test.rs @@ -560,11 +560,11 @@ pub mod custom_callable { let mut dict = VarDictionary::new(); - dict.set(a, "hello"); + dict.set(&a, "hello"); assert_eq!(hash_count(&at), 1, "hash needed for a dict key"); assert_eq!(eq_count(&at), 0, "eq not needed if dict bucket is empty"); - dict.set(b, "hi"); + dict.set(&b, "hi"); assert_eq!(hash_count(&at), 1, "hash for a untouched if b is inserted"); assert_eq!(hash_count(&bt), 1, "hash needed for b dict key"); diff --git a/itest/rust/src/builtin_tests/containers/dictionary_test.rs b/itest/rust/src/builtin_tests/containers/dictionary_test.rs index f8e05120a..c4731b888 100644 --- a/itest/rust/src/builtin_tests/containers/dictionary_test.rs +++ b/itest/rust/src/builtin_tests/containers/dictionary_test.rs @@ -7,7 +7,9 @@ use std::collections::{HashMap, HashSet}; -use godot::builtin::{VarDictionary, Variant, VariantType, varray, vdict}; +use godot::builtin::{ + AnyDictionary, Dictionary, GString, VarDictionary, Variant, VariantType, varray, vdict, +}; use godot::classes::RefCounted; use godot::meta::{ElementType, FromGodot, ToGodot}; use godot::obj::NewGd; @@ -28,13 +30,19 @@ fn dictionary_new() { #[itest] fn dictionary_from_iterator() { - let dictionary = VarDictionary::from_iter([("foo", 1), ("bar", 2)]); + let dictionary: VarDictionary = [("foo", 1), ("bar", 2)] + .into_iter() + .map(|(k, v)| (k.to_variant(), v.to_variant())) + .collect(); assert_eq!(dictionary.len(), 2); assert_eq!(dictionary.get("foo"), Some(1.to_variant()), "key = \"foo\""); assert_eq!(dictionary.get("bar"), Some(2.to_variant()), "key = \"bar\""); - let dictionary = VarDictionary::from_iter([(1, "foo"), (2, "bar")]); + let dictionary: VarDictionary = [(1, "foo"), (2, "bar")] + .into_iter() + .map(|(k, v)| (k.to_variant(), v.to_variant())) + .collect(); assert_eq!(dictionary.len(), 2); assert_eq!(dictionary.get(1), Some("foo".to_variant()), "key = 1"); @@ -42,18 +50,28 @@ fn dictionary_from_iterator() { } #[itest] -fn dictionary_from() { - let dictionary = VarDictionary::from(&HashMap::from([("foo", 1), ("bar", 2)])); +fn dictionary_extend_from_hashmap() { + let mut dictionary = VarDictionary::new(); + dictionary.extend( + HashMap::from([("foo", 1), ("bar", 2)]) + .into_iter() + .map(|(k, v)| (k.to_variant(), v.to_variant())), + ); assert_eq!(dictionary.len(), 2); assert_eq!(dictionary.get("foo"), Some(1.to_variant()), "key = \"foo\""); assert_eq!(dictionary.get("bar"), Some(2.to_variant()), "key = \"bar\""); - let dictionary = VarDictionary::from(&HashMap::from([(1, "foo"), (2, "bar")])); + let mut dictionary = VarDictionary::new(); + dictionary.extend( + HashMap::from([(1, "foo"), (2, "bar")]) + .into_iter() + .map(|(k, v)| (k.to_variant(), v.to_variant())), + ); assert_eq!(dictionary.len(), 2); - assert_eq!(dictionary.get(1), Some("foo".to_variant()), "key = \"foo\""); - assert_eq!(dictionary.get(2), Some("bar".to_variant()), "key = \"bar\""); + assert_eq!(dictionary.get(1), Some("foo".to_variant()), "key = 1"); + assert_eq!(dictionary.get(2), Some("bar".to_variant()), "key = 2"); } #[itest] @@ -249,7 +267,7 @@ fn dictionary_get_or_insert() { assert_eq!(dict.at("existing"), 11.to_variant()); // New key -> insert + return new value. - let result = dict.get_or_insert("new_key", Variant::nil()); + let result = dict.get_or_insert("new_key", &Variant::nil()); assert_eq!(result, Variant::nil()); assert_eq!(dict.at("new_key"), Variant::nil()); @@ -259,14 +277,14 @@ fn dictionary_get_or_insert() { assert_eq!(dict.at("existing_nil"), Variant::nil()); // New NIL key -> insert + return new value. - let result = dict.get_or_insert(Variant::nil(), 11); + let result = dict.get_or_insert(&Variant::nil(), 11); assert_eq!(result, 11.to_variant()); - assert_eq!(dict.at(Variant::nil()), 11.to_variant()); + assert_eq!(dict.at(&Variant::nil()), 11.to_variant()); // Existing NIL key -> return old value. - let result = dict.get_or_insert(Variant::nil(), 22); + let result = dict.get_or_insert(&Variant::nil(), 22); assert_eq!(result, 11.to_variant()); - assert_eq!(dict.at(Variant::nil()), 11.to_variant()); + assert_eq!(dict.at(&Variant::nil()), 11.to_variant()); } #[itest] @@ -502,11 +520,17 @@ fn dictionary_iter_size_hint() { #[itest] fn dictionary_iter_equals_big() { - let dictionary: VarDictionary = (0..1000).zip(0..1000).collect(); + let dictionary: VarDictionary = (0..1000) + .zip(0..1000) + .map(|(k, v)| (k.to_variant(), v.to_variant())) + .collect(); let map: HashMap = (0..1000).zip(0..1000).collect(); let collected_map: HashMap = dictionary.iter_shared().typed::().collect(); assert_eq!(map, collected_map); - let collected_dictionary: VarDictionary = collected_map.into_iter().collect(); + let collected_dictionary: VarDictionary = collected_map + .into_iter() + .map(|(k, v)| (k.to_variant(), v.to_variant())) + .collect(); assert_eq!(dictionary, collected_dictionary); } @@ -559,7 +583,10 @@ fn dictionary_iter_insert_after_completion() { #[itest] fn dictionary_iter_big() { - let dictionary: VarDictionary = (0..256).zip(0..256).collect(); + let dictionary: VarDictionary = (0..256) + .zip(0..256) + .map(|(k, v)| (k.to_variant(), v.to_variant())) + .collect(); let mut dictionary2 = dictionary.clone(); let mut iter = dictionary.iter_shared(); @@ -571,9 +598,19 @@ fn dictionary_iter_big() { dictionary2.set("a", "b"); } dictionary2.clear(); - dictionary2.extend((0..64).zip(0..64)); + dictionary2.extend( + (0..64) + .zip(0..64) + .map(|(k, v)| (k.to_variant(), v.to_variant())), + ); } - assert_eq!(dictionary2, (0..64).zip(0..64).collect()); + assert_eq!( + dictionary2, + (0..64) + .zip(0..64) + .map(|(k, v)| (k.to_variant(), v.to_variant())) + .collect() + ); } #[itest] @@ -638,7 +675,10 @@ fn dictionary_iter_panics() { expect_panic( "VarDictionary containing integer keys should not be convertible to a HashSet", || { - let dictionary: VarDictionary = (0..10).zip(0..).collect(); + let dictionary: VarDictionary = (0..10) + .zip(0..) + .map(|(k, v)| (k.to_variant(), v.to_variant())) + .collect(); let _set: HashSet = dictionary.keys_shared().typed::().collect(); }, ); @@ -646,7 +686,10 @@ fn dictionary_iter_panics() { expect_panic( "VarDictionary containing integer entries should not be convertible to a HashMap", || { - let dictionary: VarDictionary = (0..10).zip(0..).collect(); + let dictionary: VarDictionary = (0..10) + .zip(0..) + .map(|(k, v)| (k.to_variant(), v.to_variant())) + .collect(); let _set: HashMap = dictionary.iter_shared().typed::().collect(); }, @@ -814,7 +857,7 @@ func variant_script_dict() -> Dictionary[Variant, CustomScriptForDictionaries]: // 2) Dictionary[String, Variant]. let dict = object .call("builtin_variant_dict", &[]) - .to::(); + .to::(); // typed is not compatible with VarDictionary. assert_match!( dict.key_element_type(), ElementType::Builtin(VariantType::STRING) @@ -822,7 +865,7 @@ func variant_script_dict() -> Dictionary[Variant, CustomScriptForDictionaries]: assert_match!(dict.value_element_type(), ElementType::Untyped); // 3) Dictionary[Color, RefCounted]. - let dict = object.call("builtin_class_dict", &[]).to::(); + let dict = object.call("builtin_class_dict", &[]).to::(); assert_match!( dict.key_element_type(), ElementType::Builtin(VariantType::COLOR) @@ -833,10 +876,85 @@ func variant_script_dict() -> Dictionary[Variant, CustomScriptForDictionaries]: // 4) Dictionary[Variant, CustomScriptForDictionaries]. let dict = object .call("variant_script_dict", &[]) - .to::(); + .to::(); assert_match!(dict.key_element_type(), ElementType::Untyped); assert_match!(dict.value_element_type(), ElementType::ScriptClass(script)); let script = script.script().expect("script object should be alive"); assert_eq!(script, gdscript.upcast()); assert_eq!(script.get_global_name(), "CustomScriptForDictionaries"); } + +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// Typed dictionary tests (4.4+) + +#[cfg(since_api = "4.4")] +mod typed_dictionary_tests { + use godot::builtin::dict; + + use super::*; + + #[itest] + fn dictionary_typed() { + // Value type needs to be specified for now, due to GString/StringName/NodePath ambiguity. + let dict: Dictionary = dict! { + "key1": 10, + "key2": 20, + }; + + assert_eq!(dict.len(), 2); + assert_eq!(dict.get("key1"), Some(10)); + assert_eq!(dict.get("key2"), Some(20)); + assert_eq!(dict.get("key3"), None); + + assert_match!( + dict.key_element_type(), + ElementType::Builtin(VariantType::STRING) + ); + assert_match!( + dict.value_element_type(), + ElementType::Builtin(VariantType::INT) + ); + + assert_eq!(dict.at("key1"), 10); + + let mut dict = dict; + assert_eq!(dict.remove("key1"), Some(10)); + assert_eq!(dict.get("key1"), None); + assert_eq!(dict.len(), 1); + } + + #[itest] + fn dictionary_typed_empty() { + let d: Dictionary = dict! {}; + assert_eq!(d.len(), 0); + assert!(d.is_empty()); + } + + #[itest] + fn dictionary_typed_half() { + // "Half-typed" with heterogeneous values. + let d: Dictionary = dict! { + "str": "Hello", + "num": 23, + }; + + assert_eq!(d.len(), 2); + assert_eq!(d.get("str"), Some("Hello".to_variant())); + assert_eq!(d.get("num"), Some(23.to_variant())); + } + + #[itest(skip)] // TODO(v0.5): fix {keys,values}_array and re-enable. + #[expect(clippy::dbg_macro)] + fn dictionary_typed_kv_array() { + // Value type needs to be specified for now, due to GString/StringName/NodePath ambiguity. + let dict: Dictionary = dict! { + "key1": 10, + "key2": 20, + }; + + dbg!(dict.keys_array()); + dbg!(dict.keys_array().element_type()); + dbg!(dict.values_array()); + dbg!(dict.values_array().element_type()); + } +}