Skip to content

Array, Dictionary: add into_read_only() + is_read_only() #1096

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 79 additions & 2 deletions godot-core/src/builtin/collections/array.rs
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,8 @@ impl<T: ArrayElement> Array<T> {

/// 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();
}
Expand All @@ -283,6 +285,8 @@ impl<T: ArrayElement> Array<T> {
///
/// If `index` is out of bounds.
pub fn set(&mut self, index: usize, value: impl AsArg<T>) {
self.debug_ensure_mutable();

let ptr_mut = self.ptr_mut(index);

meta::arg_into_ref!(value: T);
Expand All @@ -298,6 +302,8 @@ impl<T: ArrayElement> Array<T> {
#[doc(alias = "append")]
#[doc(alias = "push_back")]
pub fn push(&mut self, value: impl AsArg<T>) {
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.
Expand All @@ -310,6 +316,8 @@ impl<T: ArrayElement> Array<T> {
/// 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<T>) {
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.
Expand All @@ -322,6 +330,8 @@ impl<T: ArrayElement> Array<T> {
/// _Godot equivalent: `pop_back`_
#[doc(alias = "pop_back")]
pub fn pop(&mut self) -> Option<T> {
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();
Expand All @@ -334,6 +344,8 @@ impl<T: ArrayElement> Array<T> {
/// 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<T> {
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();
Expand All @@ -349,6 +361,8 @@ impl<T: ArrayElement> Array<T> {
/// # Panics
/// If `index > len()`.
pub fn insert(&mut self, index: usize, value: impl AsArg<T>) {
self.debug_ensure_mutable();

let len = self.len();
assert!(
index <= len,
Expand All @@ -371,6 +385,8 @@ impl<T: ArrayElement> Array<T> {
/// 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.
Expand All @@ -385,6 +401,8 @@ impl<T: ArrayElement> Array<T> {
/// 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<T>) {
self.debug_ensure_mutable();

meta::arg_into_ref!(value: T);

// SAFETY: We don't write anything to the array.
Expand All @@ -394,6 +412,8 @@ impl<T: ArrayElement> Array<T> {
/// 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<T>) {
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.
Expand All @@ -407,6 +427,8 @@ impl<T: ArrayElement> Array<T> {
///
/// 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<T>) {
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
Expand Down Expand Up @@ -437,6 +459,8 @@ impl<T: ArrayElement> Array<T> {
/// 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;
}
Expand All @@ -449,6 +473,8 @@ impl<T: ArrayElement> Array<T> {

/// Appends another array at the end of this array. Equivalent of `append_array` in GDScript.
pub fn extend_array(&mut self, other: &Array<T>) {
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::<Variant>() };

Expand Down Expand Up @@ -692,6 +718,8 @@ impl<T: ArrayElement> Array<T> {

/// 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();
}
Expand All @@ -705,6 +733,8 @@ impl<T: ArrayElement> Array<T> {
/// _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();
}
Expand All @@ -722,6 +752,8 @@ impl<T: ArrayElement> Array<T> {
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]);
Expand Down Expand Up @@ -749,6 +781,8 @@ impl<T: ArrayElement> Array<T> {
/// _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);
}
Expand All @@ -757,14 +791,58 @@ impl<T: ArrayElement> Array<T> {
/// 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<T>`) 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();
Expand All @@ -777,7 +855,6 @@ impl<T: ArrayElement> Array<T> {
/// 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);
Expand Down
52 changes: 52 additions & 0 deletions godot-core/src/builtin/collections/dictionary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

Expand All @@ -197,6 +199,8 @@ impl Dictionary {
///
/// _Godot equivalent: `dict[key] = value`_
pub fn set<K: ToGodot, V: ToGodot>(&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.
Expand All @@ -210,6 +214,8 @@ impl Dictionary {
/// If you don't need the previous value, use [`set()`][Self::set] instead.
#[must_use]
pub fn insert<K: ToGodot, V: ToGodot>(&mut self, key: K, value: V) -> Option<Variant> {
self.debug_ensure_mutable();

let key = key.to_variant();
let old_value = self.get(key.clone());
self.set(key, value);
Expand All @@ -222,6 +228,8 @@ impl Dictionary {
/// _Godot equivalent: `erase`_
#[doc(alias = "erase")]
pub fn remove<K: ToGodot>(&mut self, key: K) -> Option<Variant> {
self.debug_ensure_mutable();

let key = key.to_variant();
let old_value = self.get(key.clone());
self.as_inner().erase(&key);
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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)
Expand Down
15 changes: 15 additions & 0 deletions itest/rust/src/builtin_tests/containers/array_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
23 changes: 23 additions & 0 deletions itest/rust/src/builtin_tests/containers/dictionary_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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! {
Expand Down
Loading