Skip to content

Commit 1dac7f3

Browse files
authored
Merge pull request #1106 from godot-rust/feature/enum-bitmask-2
Masked enums can now be constructed from integers
2 parents 6e21024 + bd48902 commit 1dac7f3

File tree

3 files changed

+61
-11
lines changed

3 files changed

+61
-11
lines changed

godot-codegen/src/generator/enums.rs

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,13 @@ pub fn make_enum_definition_with(
9797
};
9898

9999
let traits = define_traits.then(|| {
100-
// Trait implementations
101-
let engine_trait_impl = make_enum_engine_trait_impl(enum_);
100+
// Check for associated bitmasks (e.g. Key -> KeyModifierMask).
101+
let enum_bitmask = special_cases::as_enum_bitmaskable(enum_);
102+
103+
// Trait implementations.
104+
let engine_trait_impl = make_enum_engine_trait_impl(enum_, enum_bitmask.as_ref());
102105
let index_enum_impl = make_enum_index_impl(enum_);
103-
let bitwise_impls = make_enum_bitwise_operators(enum_);
106+
let bitwise_impls = make_enum_bitwise_operators(enum_, enum_bitmask.as_ref());
104107

105108
quote! {
106109
#engine_trait_impl
@@ -216,11 +219,13 @@ fn make_enum_debug_impl(enum_: &Enum, use_as_str: bool) -> TokenStream {
216219
/// Creates an implementation of the engine trait for the given enum.
217220
///
218221
/// This will implement the trait returned by [`Enum::engine_trait`].
219-
fn make_enum_engine_trait_impl(enum_: &Enum) -> TokenStream {
222+
fn make_enum_engine_trait_impl(enum_: &Enum, enum_bitmask: Option<&RustTy>) -> TokenStream {
220223
let name = &enum_.name;
221224
let engine_trait = enum_.engine_trait();
222225

223226
if enum_.is_bitfield {
227+
// Bitfields: u64, assume any combination is valid.
228+
224229
quote! {
225230
// We may want to add this in the future.
226231
//
@@ -239,6 +244,8 @@ fn make_enum_engine_trait_impl(enum_: &Enum) -> TokenStream {
239244
}
240245
}
241246
} else if enum_.is_exhaustive {
247+
// Exhaustive enums: Rust representation is C-style `enum` (not `const`), no fallback enumerator.
248+
242249
let enumerators = enum_.enumerators.iter().map(|enumerator| {
243250
let Enumerator {
244251
name,
@@ -273,16 +280,36 @@ fn make_enum_engine_trait_impl(enum_: &Enum) -> TokenStream {
273280
}
274281
}
275282
} else {
283+
// Non-exhaustive enums divide into two categories:
284+
// - Those with associated mask (e.g. Key -> KeyModifierMask)
285+
// - All others
286+
// Both have a Rust representation of `struct { ord: i32 }`, with their values as `const` declarations.
287+
// However, those with masks don't have strict validation when marshalling from integers, and a Debug repr which includes the mask.
288+
276289
let unique_ords = enum_.unique_ords().expect("self is an enum");
277290
let str_functions = make_enum_str_functions(enum_);
278291

292+
// We can technically check against all possible mask values, remove each mask, and then verify it's a valid base-enum value.
293+
// However, this is not forward compatible: if a new mask is added in a future API version, it wouldn't be removed, and the
294+
// "unmasked" (all *known* masks removed) value would not match an enumerator. Thus, assume the value is valid, even if at lower
295+
// type safety.
296+
let try_from_ord_code = if let Some(_mask) = enum_bitmask {
297+
quote! {
298+
Some(Self { ord })
299+
}
300+
} else {
301+
quote! {
302+
match ord {
303+
#( ord @ #unique_ords )|* => Some(Self { ord }),
304+
_ => None,
305+
}
306+
}
307+
};
308+
279309
quote! {
280310
impl #engine_trait for #name {
281311
fn try_from_ord(ord: i32) -> Option<Self> {
282-
match ord {
283-
#( ord @ #unique_ords )|* => Some(Self { ord }),
284-
_ => None,
285-
}
312+
#try_from_ord_code
286313
}
287314

288315
fn ord(self) -> i32 {
@@ -358,7 +385,7 @@ fn make_enum_str_functions(enum_: &Enum) -> TokenStream {
358385
/// Creates implementations for bitwise operators for the given enum.
359386
///
360387
/// Currently, this is just [`BitOr`](std::ops::BitOr) for bitfields but that could be expanded in the future.
361-
fn make_enum_bitwise_operators(enum_: &Enum) -> TokenStream {
388+
fn make_enum_bitwise_operators(enum_: &Enum, enum_bitmask: Option<&RustTy>) -> TokenStream {
362389
let name = &enum_.name;
363390

364391
if enum_.is_bitfield {
@@ -380,7 +407,7 @@ fn make_enum_bitwise_operators(enum_: &Enum) -> TokenStream {
380407
}
381408
}
382409
}
383-
} else if let Some(mask_enum) = special_cases::as_enum_bitmaskable(enum_) {
410+
} else if let Some(mask_enum) = enum_bitmask {
384411
// Enum that has an accompanying bitfield for masking.
385412
let RustTy::EngineEnum { tokens: mask, .. } = mask_enum else {
386413
panic!("as_enum_bitmaskable() must return enum/bitfield type")

godot-codegen/src/special_cases/special_cases.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -884,6 +884,11 @@ pub fn is_enum_bitfield(class_name: Option<&TyName>, enum_name: &str) -> Option<
884884
///
885885
/// If a mapping is found, returns the corresponding `RustTy`.
886886
pub fn as_enum_bitmaskable(enum_: &Enum) -> Option<RustTy> {
887+
if enum_.is_bitfield {
888+
// Only enums need this treatment.
889+
return None;
890+
}
891+
887892
let class_name = enum_.surrounding_class.as_ref();
888893
let class_name_str = class_name.map(|ty| ty.godot_ty.as_str());
889894
let enum_name = enum_.godot_name.as_str();

itest/rust/src/engine_tests/engine_enum_test.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use godot::global::{Key, KeyModifierMask};
1111
use godot::obj::{EngineBitfield, EngineEnum};
1212

1313
#[itest]
14-
fn enum_with_masked_bitfield() {
14+
fn enum_with_masked_bitfield_ord() {
1515
let key = Key::A;
1616
let mask = KeyModifierMask::SHIFT;
1717

@@ -28,3 +28,21 @@ fn enum_with_masked_bitfield() {
2828
key |= KeyModifierMask::SHIFT;
2929
assert_eq!(key.ord(), 65 | (1 << 25));
3030
}
31+
32+
#[itest]
33+
fn enum_with_masked_bitfield_from_ord() {
34+
let shifted_key = Key::A | KeyModifierMask::SHIFT;
35+
let ord = shifted_key.ord();
36+
assert_eq!(ord, 65 | (1 << 25));
37+
38+
let back = Key::try_from_ord(ord);
39+
assert_eq!(back, Some(shifted_key), "deserialize bitmasked enum");
40+
41+
let back = Key::from_ord(ord);
42+
assert_eq!(back, shifted_key, "deserialize bitmasked enum");
43+
44+
// For random values that are *not* a valid enum|mask combination, try_from_ord() should fail.
45+
// Not implemented, as it's hard to achieve this without breaking forward compatibility; see make_enum_engine_trait_impl().
46+
// let back = Key::try_from_ord(31);
47+
// assert_eq!(back, None, "don't deserialize invalid bitmasked enum");
48+
}

0 commit comments

Comments
 (0)