diff --git a/godot-core/src/builtin/collections/array.rs b/godot-core/src/builtin/collections/array.rs index 3f5510670..5698260f0 100644 --- a/godot-core/src/builtin/collections/array.rs +++ b/godot-core/src/builtin/collections/array.rs @@ -273,6 +273,8 @@ impl Array { /// Clears the array, removing all elements. pub fn clear(&mut self) { + self.debug_ensure_mutable(); + // SAFETY: No new values are written to the array, we only remove values from the array. unsafe { self.as_inner_mut() }.clear(); } @@ -283,6 +285,8 @@ impl Array { /// /// If `index` is out of bounds. pub fn set(&mut self, index: usize, value: impl AsArg) { + self.debug_ensure_mutable(); + let ptr_mut = self.ptr_mut(index); meta::arg_into_ref!(value: T); @@ -298,6 +302,8 @@ impl Array { #[doc(alias = "append")] #[doc(alias = "push_back")] pub fn push(&mut self, value: impl AsArg) { + self.debug_ensure_mutable(); + meta::arg_into_ref!(value: T); // SAFETY: The array has type `T` and we're writing a value of type `T` to it. @@ -310,6 +316,8 @@ impl Array { /// On large arrays, this method is much slower than [`push()`][Self::push], as it will move all the array's elements. /// The larger the array, the slower `push_front()` will be. pub fn push_front(&mut self, value: impl AsArg) { + self.debug_ensure_mutable(); + meta::arg_into_ref!(value: T); // SAFETY: The array has type `T` and we're writing a value of type `T` to it. @@ -322,6 +330,8 @@ impl Array { /// _Godot equivalent: `pop_back`_ #[doc(alias = "pop_back")] pub fn pop(&mut self) -> Option { + self.debug_ensure_mutable(); + (!self.is_empty()).then(|| { // SAFETY: We do not write any values to the array, we just remove one. let variant = unsafe { self.as_inner_mut() }.pop_back(); @@ -334,6 +344,8 @@ impl Array { /// Note: On large arrays, this method is much slower than `pop()` as it will move all the /// array's elements. The larger the array, the slower `pop_front()` will be. pub fn pop_front(&mut self) -> Option { + self.debug_ensure_mutable(); + (!self.is_empty()).then(|| { // SAFETY: We do not write any values to the array, we just remove one. let variant = unsafe { self.as_inner_mut() }.pop_front(); @@ -349,6 +361,8 @@ impl Array { /// # Panics /// If `index > len()`. pub fn insert(&mut self, index: usize, value: impl AsArg) { + self.debug_ensure_mutable(); + let len = self.len(); assert!( index <= len, @@ -371,6 +385,8 @@ impl Array { /// If `index` is out of bounds. #[doc(alias = "pop_at")] pub fn remove(&mut self, index: usize) -> T { + self.debug_ensure_mutable(); + self.check_bounds(index); // SAFETY: We do not write any values to the array, we just remove one. @@ -385,6 +401,8 @@ impl Array { /// On large arrays, this method is much slower than [`pop()`][Self::pop], as it will move all the array's /// elements after the removed element. pub fn erase(&mut self, value: impl AsArg) { + self.debug_ensure_mutable(); + meta::arg_into_ref!(value: T); // SAFETY: We don't write anything to the array. @@ -394,6 +412,8 @@ impl Array { /// Assigns the given value to all elements in the array. This can be used together with /// `resize` to create an array with a given size and initialized elements. pub fn fill(&mut self, value: impl AsArg) { + self.debug_ensure_mutable(); + meta::arg_into_ref!(value: T); // SAFETY: The array has type `T` and we're writing values of type `T` to it. @@ -407,6 +427,8 @@ impl Array { /// /// If you know that the new size is smaller, then consider using [`shrink`](Array::shrink) instead. pub fn resize(&mut self, new_size: usize, value: impl AsArg) { + self.debug_ensure_mutable(); + let original_size = self.len(); // SAFETY: While we do insert `Variant::nil()` if the new size is larger, we then fill it with `value` ensuring that all values in the @@ -437,6 +459,8 @@ impl Array { /// If you want to increase the size of the array, use [`resize`](Array::resize) instead. #[doc(alias = "resize")] pub fn shrink(&mut self, new_size: usize) -> bool { + self.debug_ensure_mutable(); + if new_size >= self.len() { return false; } @@ -449,6 +473,8 @@ impl Array { /// Appends another array at the end of this array. Equivalent of `append_array` in GDScript. pub fn extend_array(&mut self, other: &Array) { + self.debug_ensure_mutable(); + // SAFETY: `append_array` will only read values from `other`, and all types can be converted to `Variant`. let other: &VariantArray = unsafe { other.assume_type_ref::() }; @@ -692,6 +718,8 @@ impl Array { /// Reverses the order of the elements in the array. pub fn reverse(&mut self) { + self.debug_ensure_mutable(); + // SAFETY: We do not write any values that don't already exist in the array, so all values have the correct type. unsafe { self.as_inner_mut() }.reverse(); } @@ -705,6 +733,8 @@ impl Array { /// _Godot equivalent: `Array.sort()`_ #[doc(alias = "sort")] pub fn sort_unstable(&mut self) { + self.debug_ensure_mutable(); + // SAFETY: We do not write any values that don't already exist in the array, so all values have the correct type. unsafe { self.as_inner_mut() }.sort(); } @@ -722,6 +752,8 @@ impl Array { where F: FnMut(&T, &T) -> cmp::Ordering, { + self.debug_ensure_mutable(); + let godot_comparator = |args: &[&Variant]| { let lhs = T::from_variant(args[0]); let rhs = T::from_variant(args[1]); @@ -749,6 +781,8 @@ impl Array { /// _Godot equivalent: `Array.sort_custom()`_ #[doc(alias = "sort_custom")] pub fn sort_unstable_custom(&mut self, func: &Callable) { + self.debug_ensure_mutable(); + // SAFETY: We do not write any values that don't already exist in the array, so all values have the correct type. unsafe { self.as_inner_mut() }.sort_custom(func); } @@ -757,14 +791,58 @@ impl Array { /// global random number generator common to methods such as `randi`. Call `randomize` to /// ensure that a new seed will be used each time if you want non-reproducible shuffling. pub fn shuffle(&mut self) { + self.debug_ensure_mutable(); + // SAFETY: We do not write any values that don't already exist in the array, so all values have the correct type. unsafe { self.as_inner_mut() }.shuffle(); } - /// Asserts that the given index refers to an existing element. + /// Turns the array into a shallow-immutable array. + /// + /// Makes the array read-only and returns the original array. The array's elements cannot be overridden with different values, and their + /// order cannot change. Does not apply to nested elements, such as dictionaries. This operation is irreversible. + /// + /// In GDScript, arrays 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 array. There is no guarantee that any attempts to change result + /// in feedback; some may silently do nothing. + /// + /// In Rust, you can use shared references (`&Array`) to prevent mutation. Note however that `Clone` can be used to create another + /// reference, through which mutation can still occur. For deep-immutable arrays, you'll need to keep your `Array` encapsulated or directly + /// use Rust data structures. + /// + /// _Godot equivalent: `make_read_only`_ + #[doc(alias = "make_read_only")] + pub fn into_read_only(self) -> Self { + // SAFETY: Changes a per-array property, no elements. + unsafe { self.as_inner_mut() }.make_read_only(); + self + } + + /// Returns true if the array is read-only. + /// + /// See [`into_read_only()`][Self::into_read_only]. + /// In GDScript, arrays are automatically read-only if declared with the `const` keyword. + pub fn is_read_only(&self) -> bool { + self.as_inner().is_read_only() + } + + /// Best-effort mutability check. /// /// # Panics + /// In Debug mode, if the array is marked as read-only. + fn debug_ensure_mutable(&self) { + debug_assert!( + !self.is_read_only(), + "mutating operation on read-only array" + ); + } + + /// Asserts that the given index refers to an existing element. /// + /// # Panics /// If `index` is out of bounds. fn check_bounds(&self, index: usize) { let len = self.len(); @@ -777,7 +855,6 @@ impl Array { /// Returns a pointer to the element at the given index. /// /// # Panics - /// /// If `index` is out of bounds. fn ptr(&self, index: usize) -> sys::GDExtensionConstVariantPtr { let ptr = self.ptr_or_null(index); diff --git a/godot-core/src/builtin/collections/dictionary.rs b/godot-core/src/builtin/collections/dictionary.rs index 71649886c..29c50192b 100644 --- a/godot-core/src/builtin/collections/dictionary.rs +++ b/godot-core/src/builtin/collections/dictionary.rs @@ -188,6 +188,8 @@ impl Dictionary { /// Removes all key-value pairs from the dictionary. pub fn clear(&mut self) { + self.debug_ensure_mutable(); + self.as_inner().clear() } @@ -197,6 +199,8 @@ impl Dictionary { /// /// _Godot equivalent: `dict[key] = value`_ pub fn set(&mut self, key: K, value: V) { + self.debug_ensure_mutable(); + let key = key.to_variant(); // SAFETY: `self.get_ptr_mut(key)` always returns a valid pointer to a value in the dictionary; either pre-existing or newly inserted. @@ -210,6 +214,8 @@ impl Dictionary { /// 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 { + self.debug_ensure_mutable(); + let key = key.to_variant(); let old_value = self.get(key.clone()); self.set(key, value); @@ -222,6 +228,8 @@ impl Dictionary { /// _Godot equivalent: `erase`_ #[doc(alias = "erase")] pub fn remove(&mut self, key: K) -> Option { + self.debug_ensure_mutable(); + let key = key.to_variant(); let old_value = self.get(key.clone()); self.as_inner().erase(&key); @@ -257,6 +265,8 @@ impl Dictionary { /// _Godot equivalent: `merge`_ #[doc(alias = "merge")] pub fn extend_dictionary(&mut self, other: &Self, overwrite: bool) { + self.debug_ensure_mutable(); + self.as_inner().merge(other, overwrite) } @@ -312,6 +322,48 @@ impl Dictionary { Keys::new(self) } + /// 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. + /// + /// # 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 { + self.as_inner().make_read_only(); + self + } + + /// Returns true if the dictionary is read-only. + /// + /// See [`into_read_only()`][Self::into_read_only]. + /// In GDScript, dictionaries are automatically read-only if declared with the `const` keyword. + pub fn is_read_only(&self) -> bool { + self.as_inner().is_read_only() + } + + /// Best-effort mutability check. + /// + /// # Panics + /// In Debug mode, if the array is marked as read-only. + fn debug_ensure_mutable(&self) { + debug_assert!( + !self.is_read_only(), + "mutating operation on read-only dictionary" + ); + } + #[doc(hidden)] pub fn as_inner(&self) -> inner::InnerDictionary { inner::InnerDictionary::from_outer(self) diff --git a/itest/rust/src/builtin_tests/containers/array_test.rs b/itest/rust/src/builtin_tests/containers/array_test.rs index 7b7e777af..a7100407e 100644 --- a/itest/rust/src/builtin_tests/containers/array_test.rs +++ b/itest/rust/src/builtin_tests/containers/array_test.rs @@ -251,6 +251,21 @@ fn array_set() { }); } +#[itest] +fn array_set_readonly() { + let mut array = array![1, 2].into_read_only(); + + #[cfg(debug_assertions)] + expect_panic("Mutating read-only array in Debug mode", || { + array.set(0, 3); + }); + + #[cfg(not(debug_assertions))] + array.set(0, 3); // silently fails. + + assert_eq!(array.at(0), 1); +} + #[itest] fn array_push_pop() { let mut array = array![1, 2]; diff --git a/itest/rust/src/builtin_tests/containers/dictionary_test.rs b/itest/rust/src/builtin_tests/containers/dictionary_test.rs index 487cf9660..ccb89b36c 100644 --- a/itest/rust/src/builtin_tests/containers/dictionary_test.rs +++ b/itest/rust/src/builtin_tests/containers/dictionary_test.rs @@ -226,6 +226,29 @@ fn dictionary_at() { }); } +#[itest] +fn dictionary_set() { + let mut dictionary = dict! { "zero": 0, "one": 1 }; + + dictionary.set("zero", 2); + assert_eq!(dictionary, dict! { "zero": 2, "one": 1 }); +} + +#[itest] +fn dictionary_set_readonly() { + let mut dictionary = dict! { "zero": 0, "one": 1 }.into_read_only(); + + #[cfg(debug_assertions)] + expect_panic("Mutating read-only dictionary in Debug mode", || { + dictionary.set("zero", 2); + }); + + #[cfg(not(debug_assertions))] + dictionary.set("zero", 2); // silently fails. + + assert_eq!(dictionary.at("zero"), 0.to_variant()); +} + #[itest] fn dictionary_insert() { let mut dictionary = dict! {