Skip to content

Commit b1390df

Browse files
committed
Add StringName::transient_ord()
Also document some other traits for builtin types.
1 parent 83d245c commit b1390df

File tree

9 files changed

+129
-28
lines changed

9 files changed

+129
-28
lines changed

godot-core/src/builtin/callable.rs

+6-5
Original file line numberDiff line numberDiff line change
@@ -271,15 +271,16 @@ impl Callable {
271271

272272
impl_builtin_traits! {
273273
for Callable {
274-
// Currently no Default::default() to encourage explicit valid initialization.
275-
//Default => callable_construct_default;
274+
// Default is absent by design, to encourage explicit valid initialization.
275+
276+
Clone => callable_construct_copy;
277+
Drop => callable_destroy;
276278

277279
// Equality for custom callables depend on the equality implementation of that custom callable.
278280
// So we cannot implement `Eq` here and be confident equality will be total for all future custom callables.
279-
// Godot does not define a less-than operator, so there is no `PartialOrd`.
280281
PartialEq => callable_operator_equal;
281-
Clone => callable_construct_copy;
282-
Drop => callable_destroy;
282+
// Godot does not define a less-than operator, so there is no `PartialOrd`.
283+
// Hash could be added, but without Eq it's not that useful; wait for actual use cases.
283284
}
284285
}
285286

godot-core/src/builtin/dictionary.rs

+3
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,8 @@ impl_builtin_traits! {
297297
Default => dictionary_construct_default;
298298
Drop => dictionary_destroy;
299299
PartialEq => dictionary_operator_equal;
300+
// No < operator for dictionaries.
301+
// Hash could be added, but without Eq it's not that useful.
300302
}
301303
}
302304

@@ -305,6 +307,7 @@ impl fmt::Debug for Dictionary {
305307
write!(f, "{:?}", self.to_variant().stringify())
306308
}
307309
}
310+
308311
impl fmt::Display for Dictionary {
309312
/// Formats `Dictionary` to match Godot's string representation.
310313
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {

godot-core/src/builtin/mod.rs

+7-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ pub use rect2::*;
5151
pub use rect2i::*;
5252
pub use rid::*;
5353
pub use signal::*;
54-
pub use string::*;
54+
pub use string::{GString, NodePath, StringName};
5555
pub use transform2d::*;
5656
pub use transform3d::*;
5757
pub use variant::*;
@@ -73,6 +73,11 @@ pub mod dictionary {
7373
pub use super::dictionary_inner::{Iter, Keys, TypedIter, TypedKeys};
7474
}
7575

76+
/// Specialized types related to Godot's various string implementations.
77+
pub mod strings {
78+
pub use super::string::TransientStringNameOrd;
79+
}
80+
7681
// ----------------------------------------------------------------------------------------------------------------------------------------------
7782
// Implementation
7883

@@ -145,6 +150,7 @@ pub enum RectSide {
145150
}
146151

147152
// ----------------------------------------------------------------------------------------------------------------------------------------------
153+
// #[test] utils for serde
148154

149155
#[cfg(all(test, feature = "serde"))]
150156
pub(crate) mod test_utils {

godot-core/src/builtin/plane.rs

+3
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,9 @@ impl std::fmt::Display for Plane {
299299
}
300300
}
301301

302+
// ----------------------------------------------------------------------------------------------------------------------------------------------
303+
// Tests
304+
302305
#[cfg(test)]
303306
mod test {
304307
use crate::assert_eq_approx;

godot-core/src/builtin/string/mod.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ mod string_chars;
1414
mod string_name;
1515

1616
pub use gstring::*;
17-
pub use node_path::*;
18-
pub use string_name::*;
17+
pub use node_path::NodePath;
18+
pub use string_name::{StringName, TransientStringNameOrd};
1919

2020
use super::meta::{ConvertError, FromGodot, GodotConvert, ToGodot};
2121

godot-core/src/builtin/string/node_path.rs

+1
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ impl_builtin_traits! {
8282
Clone => node_path_construct_copy;
8383
Drop => node_path_destroy;
8484
Eq => node_path_operator_equal;
85+
// NodePath provides no < operator.
8586
Hash;
8687
}
8788
}

godot-core/src/builtin/string/string_name.rs

+78-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ use crate::builtin::{GString, NodePath};
1818
///
1919
/// StringNames are immutable strings designed for representing unique names. StringName ensures that only
2020
/// one instance of a given name exists.
21+
///
22+
/// # Ordering
23+
///
24+
/// In Godot, `StringName`s are **not** ordered lexicographically, and the ordering relation is **not** stable across multiple runs of your
25+
/// application. Therefore, this type does not implement `PartialOrd` and `Ord`, as it would be very easy to introduce bugs by accidentally
26+
/// relying on lexicographical ordering.
27+
///
28+
/// Instead, we provide [`transient_ord()`][Self::transient_ord] for ordering relations.
2129
#[repr(C)]
2230
pub struct StringName {
2331
opaque: sys::types::OpaqueStringName,
@@ -90,6 +98,19 @@ impl StringName {
9098
.expect("Godot hashes are uint32_t")
9199
}
92100

101+
/// O(1), non-lexicographic, non-stable ordering relation.
102+
///
103+
/// The result of the comparison is **not** lexicographic and **not** stable across multiple runs of your application.
104+
///
105+
/// However, it is very fast. It doesn't depend on the length of the strings, but on the memory location of string names.
106+
/// This can still be useful if you need to establish an ordering relation, but are not interested in the actual order of the strings
107+
/// (example: binary search).
108+
///
109+
/// For lexicographical ordering, convert to `GString` (significantly slower).
110+
pub fn transient_ord(&self) -> TransientStringNameOrd<'_> {
111+
TransientStringNameOrd(self)
112+
}
113+
93114
ffi_methods! {
94115
type sys::GDExtensionStringNamePtr = *mut Opaque;
95116

@@ -152,8 +173,8 @@ impl_builtin_traits! {
152173
Clone => string_name_construct_copy;
153174
Drop => string_name_destroy;
154175
Eq => string_name_operator_equal;
155-
// currently broken: https://github.com/godotengine/godot/issues/76218
156-
// Ord => string_name_operator_less;
176+
// Do not provide PartialOrd or Ord. Even though Godot provides a `operator <`, it is non-lexicographic and non-deterministic
177+
// (based on pointers). See transient_ord() method.
157178
Hash;
158179
}
159180
}
@@ -248,6 +269,61 @@ impl From<NodePath> for StringName {
248269
}
249270
}
250271

272+
// ----------------------------------------------------------------------------------------------------------------------------------------------
273+
// Ordering
274+
275+
/// Type that implements `Ord` for `StringNames`.
276+
///
277+
/// See [`StringName::transient_ord()`].
278+
pub struct TransientStringNameOrd<'a>(&'a StringName);
279+
280+
impl<'a> PartialEq for TransientStringNameOrd<'a> {
281+
fn eq(&self, other: &Self) -> bool {
282+
self.0 == other.0
283+
}
284+
}
285+
286+
impl<'a> Eq for TransientStringNameOrd<'a> {}
287+
288+
impl<'a> PartialOrd for TransientStringNameOrd<'a> {
289+
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
290+
Some(self.cmp(other))
291+
}
292+
}
293+
294+
// implement Ord like above
295+
impl<'a> Ord for TransientStringNameOrd<'a> {
296+
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
297+
// SAFETY: builtin operator provided by Godot.
298+
let op_less = |lhs, rhs| unsafe {
299+
let mut result = false;
300+
sys::builtin_call! {
301+
string_name_operator_less(lhs, rhs, result.sys_mut())
302+
}
303+
result
304+
};
305+
306+
let self_ptr = self.0.sys();
307+
let other_ptr = other.0.sys();
308+
309+
if op_less(self_ptr, other_ptr) {
310+
std::cmp::Ordering::Less
311+
} else if op_less(other_ptr, self_ptr) {
312+
std::cmp::Ordering::Greater
313+
} else if self.eq(other) {
314+
std::cmp::Ordering::Equal
315+
} else {
316+
panic!(
317+
"Godot provides inconsistent StringName ordering for \"{}\" and \"{}\"",
318+
self.0, other.0
319+
);
320+
}
321+
}
322+
}
323+
324+
// ----------------------------------------------------------------------------------------------------------------------------------------------
325+
// serde support
326+
251327
#[cfg(feature = "serde")]
252328
mod serialize {
253329
use super::*;

godot-core/src/builtin/variant/mod.rs

+10-10
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,16 @@ unsafe impl GodotFfi for Variant {
306306

307307
impl_godot_as_self!(Variant);
308308

309+
impl Default for Variant {
310+
fn default() -> Self {
311+
unsafe {
312+
Self::from_var_sys_init(|variant_ptr| {
313+
interface_fn!(variant_new_nil)(variant_ptr);
314+
})
315+
}
316+
}
317+
}
318+
309319
impl Clone for Variant {
310320
fn clone(&self) -> Self {
311321
unsafe {
@@ -324,16 +334,6 @@ impl Drop for Variant {
324334
}
325335
}
326336

327-
impl Default for Variant {
328-
fn default() -> Self {
329-
unsafe {
330-
Self::from_var_sys_init(|variant_ptr| {
331-
interface_fn!(variant_new_nil)(variant_ptr);
332-
})
333-
}
334-
}
335-
}
336-
337337
// Variant is not Eq because it can contain floats and other types composed of floats.
338338
impl PartialEq for Variant {
339339
fn eq(&self, other: &Self) -> bool {

itest/rust/src/builtin_tests/string/string_name_test.rs

+19-8
Original file line numberDiff line numberDiff line change
@@ -58,17 +58,28 @@ fn string_name_equality() {
5858
assert_ne!(string, different);
5959
}
6060

61-
// TODO: add back in when ordering StringNames is fixed
62-
#[itest(skip)]
63-
fn string_name_ordering() {
64-
let _low = StringName::from("Alpha");
65-
let _high = StringName::from("Beta");
66-
/*
61+
#[itest]
62+
fn string_name_transient_ord() {
63+
// We can't deterministically know the ordering, so this test only ensures consistency between different operators.
64+
let low = StringName::from("Alpha");
65+
let high = StringName::from("Beta");
66+
67+
let mut low = low.transient_ord();
68+
let mut high = high.transient_ord();
69+
70+
if low > high {
71+
std::mem::swap(&mut low, &mut high);
72+
}
73+
6774
assert!(low < high);
6875
assert!(low <= high);
69-
assert!(high > low);
76+
assert!(high > low); // implied.
7077
assert!(high >= low);
71-
*/
78+
79+
// Check PartialEq/Eq relation.
80+
assert_eq!(low, low);
81+
assert_ne!(low, high);
82+
assert_eq!(high, high);
7283
}
7384

7485
#[itest]

0 commit comments

Comments
 (0)