Skip to content

Commit b7ea495

Browse files
authored
Merge pull request #1096 from godot-rust/feature/collections-read-only
`Array`, `Dictionary`: add `into_read_only()` + `is_read_only()`
2 parents 97733bf + 087aabc commit b7ea495

File tree

4 files changed

+169
-2
lines changed

4 files changed

+169
-2
lines changed

godot-core/src/builtin/collections/array.rs

+79-2
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,8 @@ impl<T: ArrayElement> Array<T> {
273273

274274
/// Clears the array, removing all elements.
275275
pub fn clear(&mut self) {
276+
self.debug_ensure_mutable();
277+
276278
// SAFETY: No new values are written to the array, we only remove values from the array.
277279
unsafe { self.as_inner_mut() }.clear();
278280
}
@@ -283,6 +285,8 @@ impl<T: ArrayElement> Array<T> {
283285
///
284286
/// If `index` is out of bounds.
285287
pub fn set(&mut self, index: usize, value: impl AsArg<T>) {
288+
self.debug_ensure_mutable();
289+
286290
let ptr_mut = self.ptr_mut(index);
287291

288292
meta::arg_into_ref!(value: T);
@@ -298,6 +302,8 @@ impl<T: ArrayElement> Array<T> {
298302
#[doc(alias = "append")]
299303
#[doc(alias = "push_back")]
300304
pub fn push(&mut self, value: impl AsArg<T>) {
305+
self.debug_ensure_mutable();
306+
301307
meta::arg_into_ref!(value: T);
302308

303309
// SAFETY: The array has type `T` and we're writing a value of type `T` to it.
@@ -310,6 +316,8 @@ impl<T: ArrayElement> Array<T> {
310316
/// On large arrays, this method is much slower than [`push()`][Self::push], as it will move all the array's elements.
311317
/// The larger the array, the slower `push_front()` will be.
312318
pub fn push_front(&mut self, value: impl AsArg<T>) {
319+
self.debug_ensure_mutable();
320+
313321
meta::arg_into_ref!(value: T);
314322

315323
// SAFETY: The array has type `T` and we're writing a value of type `T` to it.
@@ -322,6 +330,8 @@ impl<T: ArrayElement> Array<T> {
322330
/// _Godot equivalent: `pop_back`_
323331
#[doc(alias = "pop_back")]
324332
pub fn pop(&mut self) -> Option<T> {
333+
self.debug_ensure_mutable();
334+
325335
(!self.is_empty()).then(|| {
326336
// SAFETY: We do not write any values to the array, we just remove one.
327337
let variant = unsafe { self.as_inner_mut() }.pop_back();
@@ -334,6 +344,8 @@ impl<T: ArrayElement> Array<T> {
334344
/// Note: On large arrays, this method is much slower than `pop()` as it will move all the
335345
/// array's elements. The larger the array, the slower `pop_front()` will be.
336346
pub fn pop_front(&mut self) -> Option<T> {
347+
self.debug_ensure_mutable();
348+
337349
(!self.is_empty()).then(|| {
338350
// SAFETY: We do not write any values to the array, we just remove one.
339351
let variant = unsafe { self.as_inner_mut() }.pop_front();
@@ -349,6 +361,8 @@ impl<T: ArrayElement> Array<T> {
349361
/// # Panics
350362
/// If `index > len()`.
351363
pub fn insert(&mut self, index: usize, value: impl AsArg<T>) {
364+
self.debug_ensure_mutable();
365+
352366
let len = self.len();
353367
assert!(
354368
index <= len,
@@ -371,6 +385,8 @@ impl<T: ArrayElement> Array<T> {
371385
/// If `index` is out of bounds.
372386
#[doc(alias = "pop_at")]
373387
pub fn remove(&mut self, index: usize) -> T {
388+
self.debug_ensure_mutable();
389+
374390
self.check_bounds(index);
375391

376392
// SAFETY: We do not write any values to the array, we just remove one.
@@ -385,6 +401,8 @@ impl<T: ArrayElement> Array<T> {
385401
/// On large arrays, this method is much slower than [`pop()`][Self::pop], as it will move all the array's
386402
/// elements after the removed element.
387403
pub fn erase(&mut self, value: impl AsArg<T>) {
404+
self.debug_ensure_mutable();
405+
388406
meta::arg_into_ref!(value: T);
389407

390408
// SAFETY: We don't write anything to the array.
@@ -394,6 +412,8 @@ impl<T: ArrayElement> Array<T> {
394412
/// Assigns the given value to all elements in the array. This can be used together with
395413
/// `resize` to create an array with a given size and initialized elements.
396414
pub fn fill(&mut self, value: impl AsArg<T>) {
415+
self.debug_ensure_mutable();
416+
397417
meta::arg_into_ref!(value: T);
398418

399419
// SAFETY: The array has type `T` and we're writing values of type `T` to it.
@@ -407,6 +427,8 @@ impl<T: ArrayElement> Array<T> {
407427
///
408428
/// If you know that the new size is smaller, then consider using [`shrink`](Array::shrink) instead.
409429
pub fn resize(&mut self, new_size: usize, value: impl AsArg<T>) {
430+
self.debug_ensure_mutable();
431+
410432
let original_size = self.len();
411433

412434
// 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<T: ArrayElement> Array<T> {
437459
/// If you want to increase the size of the array, use [`resize`](Array::resize) instead.
438460
#[doc(alias = "resize")]
439461
pub fn shrink(&mut self, new_size: usize) -> bool {
462+
self.debug_ensure_mutable();
463+
440464
if new_size >= self.len() {
441465
return false;
442466
}
@@ -449,6 +473,8 @@ impl<T: ArrayElement> Array<T> {
449473

450474
/// Appends another array at the end of this array. Equivalent of `append_array` in GDScript.
451475
pub fn extend_array(&mut self, other: &Array<T>) {
476+
self.debug_ensure_mutable();
477+
452478
// SAFETY: `append_array` will only read values from `other`, and all types can be converted to `Variant`.
453479
let other: &VariantArray = unsafe { other.assume_type_ref::<Variant>() };
454480

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

693719
/// Reverses the order of the elements in the array.
694720
pub fn reverse(&mut self) {
721+
self.debug_ensure_mutable();
722+
695723
// SAFETY: We do not write any values that don't already exist in the array, so all values have the correct type.
696724
unsafe { self.as_inner_mut() }.reverse();
697725
}
@@ -705,6 +733,8 @@ impl<T: ArrayElement> Array<T> {
705733
/// _Godot equivalent: `Array.sort()`_
706734
#[doc(alias = "sort")]
707735
pub fn sort_unstable(&mut self) {
736+
self.debug_ensure_mutable();
737+
708738
// SAFETY: We do not write any values that don't already exist in the array, so all values have the correct type.
709739
unsafe { self.as_inner_mut() }.sort();
710740
}
@@ -722,6 +752,8 @@ impl<T: ArrayElement> Array<T> {
722752
where
723753
F: FnMut(&T, &T) -> cmp::Ordering,
724754
{
755+
self.debug_ensure_mutable();
756+
725757
let godot_comparator = |args: &[&Variant]| {
726758
let lhs = T::from_variant(args[0]);
727759
let rhs = T::from_variant(args[1]);
@@ -749,6 +781,8 @@ impl<T: ArrayElement> Array<T> {
749781
/// _Godot equivalent: `Array.sort_custom()`_
750782
#[doc(alias = "sort_custom")]
751783
pub fn sort_unstable_custom(&mut self, func: &Callable) {
784+
self.debug_ensure_mutable();
785+
752786
// SAFETY: We do not write any values that don't already exist in the array, so all values have the correct type.
753787
unsafe { self.as_inner_mut() }.sort_custom(func);
754788
}
@@ -757,14 +791,58 @@ impl<T: ArrayElement> Array<T> {
757791
/// global random number generator common to methods such as `randi`. Call `randomize` to
758792
/// ensure that a new seed will be used each time if you want non-reproducible shuffling.
759793
pub fn shuffle(&mut self) {
794+
self.debug_ensure_mutable();
795+
760796
// SAFETY: We do not write any values that don't already exist in the array, so all values have the correct type.
761797
unsafe { self.as_inner_mut() }.shuffle();
762798
}
763799

764-
/// Asserts that the given index refers to an existing element.
800+
/// Turns the array into a shallow-immutable array.
801+
///
802+
/// Makes the array read-only and returns the original array. The array's elements cannot be overridden with different values, and their
803+
/// order cannot change. Does not apply to nested elements, such as dictionaries. This operation is irreversible.
804+
///
805+
/// In GDScript, arrays are automatically read-only if declared with the `const` keyword.
806+
///
807+
/// # Semantics and alternatives
808+
/// 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):
809+
/// 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
810+
/// in feedback; some may silently do nothing.
811+
///
812+
/// In Rust, you can use shared references (`&Array<T>`) to prevent mutation. Note however that `Clone` can be used to create another
813+
/// reference, through which mutation can still occur. For deep-immutable arrays, you'll need to keep your `Array` encapsulated or directly
814+
/// use Rust data structures.
815+
///
816+
/// _Godot equivalent: `make_read_only`_
817+
#[doc(alias = "make_read_only")]
818+
pub fn into_read_only(self) -> Self {
819+
// SAFETY: Changes a per-array property, no elements.
820+
unsafe { self.as_inner_mut() }.make_read_only();
821+
self
822+
}
823+
824+
/// Returns true if the array is read-only.
825+
///
826+
/// See [`into_read_only()`][Self::into_read_only].
827+
/// In GDScript, arrays are automatically read-only if declared with the `const` keyword.
828+
pub fn is_read_only(&self) -> bool {
829+
self.as_inner().is_read_only()
830+
}
831+
832+
/// Best-effort mutability check.
765833
///
766834
/// # Panics
835+
/// In Debug mode, if the array is marked as read-only.
836+
fn debug_ensure_mutable(&self) {
837+
debug_assert!(
838+
!self.is_read_only(),
839+
"mutating operation on read-only array"
840+
);
841+
}
842+
843+
/// Asserts that the given index refers to an existing element.
767844
///
845+
/// # Panics
768846
/// If `index` is out of bounds.
769847
fn check_bounds(&self, index: usize) {
770848
let len = self.len();
@@ -777,7 +855,6 @@ impl<T: ArrayElement> Array<T> {
777855
/// Returns a pointer to the element at the given index.
778856
///
779857
/// # Panics
780-
///
781858
/// If `index` is out of bounds.
782859
fn ptr(&self, index: usize) -> sys::GDExtensionConstVariantPtr {
783860
let ptr = self.ptr_or_null(index);

godot-core/src/builtin/collections/dictionary.rs

+52
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,8 @@ impl Dictionary {
188188

189189
/// Removes all key-value pairs from the dictionary.
190190
pub fn clear(&mut self) {
191+
self.debug_ensure_mutable();
192+
191193
self.as_inner().clear()
192194
}
193195

@@ -197,6 +199,8 @@ impl Dictionary {
197199
///
198200
/// _Godot equivalent: `dict[key] = value`_
199201
pub fn set<K: ToGodot, V: ToGodot>(&mut self, key: K, value: V) {
202+
self.debug_ensure_mutable();
203+
200204
let key = key.to_variant();
201205

202206
// 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 {
210214
/// If you don't need the previous value, use [`set()`][Self::set] instead.
211215
#[must_use]
212216
pub fn insert<K: ToGodot, V: ToGodot>(&mut self, key: K, value: V) -> Option<Variant> {
217+
self.debug_ensure_mutable();
218+
213219
let key = key.to_variant();
214220
let old_value = self.get(key.clone());
215221
self.set(key, value);
@@ -222,6 +228,8 @@ impl Dictionary {
222228
/// _Godot equivalent: `erase`_
223229
#[doc(alias = "erase")]
224230
pub fn remove<K: ToGodot>(&mut self, key: K) -> Option<Variant> {
231+
self.debug_ensure_mutable();
232+
225233
let key = key.to_variant();
226234
let old_value = self.get(key.clone());
227235
self.as_inner().erase(&key);
@@ -257,6 +265,8 @@ impl Dictionary {
257265
/// _Godot equivalent: `merge`_
258266
#[doc(alias = "merge")]
259267
pub fn extend_dictionary(&mut self, other: &Self, overwrite: bool) {
268+
self.debug_ensure_mutable();
269+
260270
self.as_inner().merge(other, overwrite)
261271
}
262272

@@ -312,6 +322,48 @@ impl Dictionary {
312322
Keys::new(self)
313323
}
314324

325+
/// Turns the dictionary into a shallow-immutable dictionary.
326+
///
327+
/// Makes the dictionary read-only and returns the original dictionary. Disables modification of the dictionary's contents.
328+
/// Does not apply to nested content, e.g. elements of nested dictionaries.
329+
///
330+
/// In GDScript, dictionaries are automatically read-only if declared with the `const` keyword.
331+
///
332+
/// # Semantics and alternatives
333+
/// 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):
334+
/// some methods like `set()` panic in Debug mode, when used on a read-only dictionary. There is no guarantee that any attempts to change
335+
/// result in feedback; some may silently do nothing.
336+
///
337+
/// In Rust, you can use shared references (`&Dictionary`) to prevent mutation. Note however that `Clone` can be used to create another
338+
/// reference, through which mutation can still occur. For deep-immutable dictionaries, you'll need to keep your `Dictionary` encapsulated
339+
/// or directly use Rust data structures.
340+
///
341+
/// _Godot equivalent: `make_read_only`_
342+
#[doc(alias = "make_read_only")]
343+
pub fn into_read_only(self) -> Self {
344+
self.as_inner().make_read_only();
345+
self
346+
}
347+
348+
/// Returns true if the dictionary is read-only.
349+
///
350+
/// See [`into_read_only()`][Self::into_read_only].
351+
/// In GDScript, dictionaries are automatically read-only if declared with the `const` keyword.
352+
pub fn is_read_only(&self) -> bool {
353+
self.as_inner().is_read_only()
354+
}
355+
356+
/// Best-effort mutability check.
357+
///
358+
/// # Panics
359+
/// In Debug mode, if the array is marked as read-only.
360+
fn debug_ensure_mutable(&self) {
361+
debug_assert!(
362+
!self.is_read_only(),
363+
"mutating operation on read-only dictionary"
364+
);
365+
}
366+
315367
#[doc(hidden)]
316368
pub fn as_inner(&self) -> inner::InnerDictionary {
317369
inner::InnerDictionary::from_outer(self)

itest/rust/src/builtin_tests/containers/array_test.rs

+15
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,21 @@ fn array_set() {
251251
});
252252
}
253253

254+
#[itest]
255+
fn array_set_readonly() {
256+
let mut array = array![1, 2].into_read_only();
257+
258+
#[cfg(debug_assertions)]
259+
expect_panic("Mutating read-only array in Debug mode", || {
260+
array.set(0, 3);
261+
});
262+
263+
#[cfg(not(debug_assertions))]
264+
array.set(0, 3); // silently fails.
265+
266+
assert_eq!(array.at(0), 1);
267+
}
268+
254269
#[itest]
255270
fn array_push_pop() {
256271
let mut array = array![1, 2];

itest/rust/src/builtin_tests/containers/dictionary_test.rs

+23
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,29 @@ fn dictionary_at() {
226226
});
227227
}
228228

229+
#[itest]
230+
fn dictionary_set() {
231+
let mut dictionary = dict! { "zero": 0, "one": 1 };
232+
233+
dictionary.set("zero", 2);
234+
assert_eq!(dictionary, dict! { "zero": 2, "one": 1 });
235+
}
236+
237+
#[itest]
238+
fn dictionary_set_readonly() {
239+
let mut dictionary = dict! { "zero": 0, "one": 1 }.into_read_only();
240+
241+
#[cfg(debug_assertions)]
242+
expect_panic("Mutating read-only dictionary in Debug mode", || {
243+
dictionary.set("zero", 2);
244+
});
245+
246+
#[cfg(not(debug_assertions))]
247+
dictionary.set("zero", 2); // silently fails.
248+
249+
assert_eq!(dictionary.at("zero"), 0.to_variant());
250+
}
251+
229252
#[itest]
230253
fn dictionary_insert() {
231254
let mut dictionary = dict! {

0 commit comments

Comments
 (0)