Skip to content

Commit 4c06b58

Browse files
authored
Merge pull request #728 from godot-rust/qol/exhaustive-enums
Exhaustive enums
2 parents 0aa0072 + 1f37d35 commit 4c06b58

File tree

15 files changed

+262
-128
lines changed

15 files changed

+262
-128
lines changed

godot-codegen/src/generator/enums.rs

Lines changed: 99 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
//!
1010
//! See also models/domain/enums.rs for other enum-related methods.
1111
12-
use crate::models::domain::{Enum, Enumerator};
12+
use crate::models::domain::{Enum, Enumerator, EnumeratorValue};
1313
use crate::{conv, util};
1414
use proc_macro2::TokenStream;
1515
use quote::{quote, ToTokens};
@@ -34,46 +34,67 @@ pub fn make_enum_definition_with(
3434
define_enum: bool,
3535
define_traits: bool,
3636
) -> TokenStream {
37+
assert!(
38+
!(enum_.is_bitfield && enum_.is_exhaustive),
39+
"bitfields cannot be marked exhaustive"
40+
);
41+
3742
// Things needed for the type definition
3843
let derives = enum_.derives();
3944
let enum_doc = make_enum_doc(enum_);
4045
let name = &enum_.name;
4146

4247
// Values
43-
let enumerators = enum_
44-
.enumerators
45-
.iter()
46-
.map(|enumerator| make_enumerator_definition(enumerator, name.to_token_stream()));
48+
let enumerators = enum_.enumerators.iter().map(|enumerator| {
49+
make_enumerator_definition(enumerator, name.to_token_stream(), !enum_.is_exhaustive)
50+
});
4751

4852
// Various types
4953
let ord_type = enum_.ord_type();
5054
let engine_trait = enum_.engine_trait();
5155

52-
let definition = define_enum.then(|| {
53-
let debug_impl = make_enum_debug_impl(enum_);
54-
55-
// Workaround because traits are defined in separate crate, but need access to field `ord`.
56-
let vis = (!define_traits).then(|| {
56+
let definition = if define_enum {
57+
// Exhaustive enums are declared as Rust enums.
58+
if enum_.is_exhaustive {
5759
quote! {
58-
#[doc(hidden)] pub
60+
#[repr(i32)]
61+
#[derive(Debug, #( #derives ),* )]
62+
#( #[doc = #enum_doc] )*
63+
///
64+
/// This enum is exhaustive; you should not expect future Godot versions to add new enumerators.
65+
#[allow(non_camel_case_types)]
66+
pub enum #name {
67+
#( #enumerators )*
68+
}
5969
}
60-
});
70+
}
71+
//
72+
// Non-exhaustive enums are declared as newtype structs with associated constants.
73+
else {
74+
// Workaround because traits are defined in separate crate, but need access to field `ord`.
75+
let vis = (!define_traits).then(|| {
76+
quote! { #[doc(hidden)] pub }
77+
});
78+
79+
let debug_impl = make_enum_debug_impl(enum_);
80+
quote! {
81+
#[repr(transparent)]
82+
#[derive( #( #derives ),* )]
83+
#( #[doc = #enum_doc] )*
84+
pub struct #name {
85+
#vis ord: #ord_type
86+
}
6187

62-
quote! {
63-
#[repr(transparent)]
64-
#[derive( #( #derives ),* )]
65-
#( #[doc = #enum_doc] )*
66-
pub struct #name {
67-
#vis ord: #ord_type
68-
}
88+
impl #name {
89+
#( #enumerators )*
90+
}
6991

70-
impl #name {
71-
#( #enumerators )*
92+
#debug_impl
7293
}
73-
74-
#debug_impl
7594
}
76-
});
95+
} else {
96+
TokenStream::new()
97+
};
7798

7899
let traits = define_traits.then(|| {
79100
// Trait implementations
@@ -185,6 +206,36 @@ fn make_enum_engine_trait_impl(enum_: &Enum) -> TokenStream {
185206
}
186207
}
187208
}
209+
} else if enum_.is_exhaustive {
210+
let enumerators = enum_.enumerators.iter().map(|enumerator| {
211+
let Enumerator {
212+
name,
213+
value: EnumeratorValue::Enum(ord),
214+
..
215+
} = enumerator
216+
else {
217+
panic!("exhaustive enum contains bitfield enumerators")
218+
};
219+
220+
quote! {
221+
#ord => Some(Self::#name),
222+
}
223+
});
224+
225+
quote! {
226+
impl #engine_trait for #name {
227+
fn try_from_ord(ord: i32) -> Option<Self> {
228+
match ord {
229+
#( #enumerators )*
230+
_ => None,
231+
}
232+
}
233+
234+
fn ord(self) -> i32 {
235+
self as i32
236+
}
237+
}
238+
}
188239
} else {
189240
let unique_ords = enum_.unique_ords().expect("self is an enum");
190241

@@ -238,13 +289,21 @@ fn make_enum_doc(enum_: &Enum) -> Vec<String> {
238289
docs
239290
}
240291

241-
/// Creates a `const` definition for `enumerator` of the type `enum_type`.
292+
/// Creates a definition for `enumerator` of the type `enum_type`.
242293
///
243-
/// That is, it'll be a definition like
294+
/// If `as_constant` is true, it will be a `const` definition like:
295+
/// ```ignore
296+
/// pub const NAME: enum_type = ord;
297+
/// ```
298+
/// Otherwise, it will be a regular enum variant like:
244299
/// ```ignore
245-
/// pub const NAME: enum_type = ..;
300+
/// NAME = ord,
246301
/// ```
247-
fn make_enumerator_definition(enumerator: &Enumerator, enum_type: TokenStream) -> TokenStream {
302+
fn make_enumerator_definition(
303+
enumerator: &Enumerator,
304+
enum_type: TokenStream,
305+
as_constant: bool,
306+
) -> TokenStream {
248307
let Enumerator {
249308
name,
250309
godot_name,
@@ -262,11 +321,18 @@ fn make_enumerator_definition(enumerator: &Enumerator, enum_type: TokenStream) -
262321
TokenStream::new()
263322
};
264323

265-
quote! {
266-
#docs
267-
pub const #name: #enum_type = #enum_type {
268-
ord: #value
269-
};
324+
if as_constant {
325+
quote! {
326+
#docs
327+
pub const #name: #enum_type = #enum_type {
328+
ord: #value
329+
};
330+
}
331+
} else {
332+
quote! {
333+
#docs
334+
#name = #value,
335+
}
270336
}
271337
}
272338

godot-codegen/src/models/domain/enums.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub struct Enum {
1818
pub name: Ident,
1919
pub godot_name: String,
2020
pub is_bitfield: bool,
21+
pub is_exhaustive: bool,
2122
pub enumerators: Vec<Enumerator>,
2223
}
2324

godot-codegen/src/models/domain_mapping.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,7 @@ impl Enum {
517517
pub fn from_json(json_enum: &JsonEnum, surrounding_class: Option<&TyName>) -> Self {
518518
let godot_name = &json_enum.name;
519519
let is_bitfield = json_enum.is_bitfield;
520+
let is_exhaustive = special_cases::is_enum_exhaustive(surrounding_class, godot_name);
520521

521522
let rust_enum_name = conv::make_enum_name_str(godot_name);
522523
let rust_enumerator_names = {
@@ -550,6 +551,7 @@ impl Enum {
550551
name: ident(&rust_enum_name),
551552
godot_name: godot_name.clone(),
552553
is_bitfield,
554+
is_exhaustive,
553555
enumerators,
554556
}
555557
}

godot-codegen/src/special_cases/special_cases.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,3 +354,27 @@ pub fn is_class_level_server(class_name: &str) -> bool {
354354
=> true, _ => false
355355
}
356356
}
357+
358+
/// Certain enums that are extremely unlikely to get new identifiers in the future.
359+
///
360+
/// `class_name` = None for global enums.
361+
///
362+
/// Very conservative, only includes a few enums. Even `VariantType` was extended over time.
363+
/// Also does not work for any enums containing duplicate ordinals.
364+
#[rustfmt::skip]
365+
pub fn is_enum_exhaustive(class_name: Option<&TyName>, enum_name: &str) -> bool {
366+
// Adding new enums here should generally not break existing code:
367+
// * match _ patterns are still allowed, but cause a warning
368+
// * Enum::CONSTANT access looks the same for proper enum and newtype+const
369+
// Obviously, removing them will.
370+
371+
match (class_name, enum_name) {
372+
| (None, "ClockDirection")
373+
| (None, "Corner")
374+
| (None, "EulerOrder")
375+
| (None, "Side")
376+
| (None, "Orientation")
377+
378+
=> true, _ => false
379+
}
380+
}

godot-core/src/builtin/basis.rs

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use crate::builtin::math::{ApproxEq, FloatExt, GlamConv, GlamType};
1212
use crate::builtin::real_consts::FRAC_PI_2;
1313
use crate::builtin::{real, Quaternion, RMat3, RQuat, RVec2, RVec3, Vector3};
1414

15+
use crate::engine::global::EulerOrder;
1516
use std::cmp::Ordering;
1617
use std::fmt::Display;
1718
use std::ops::{Mul, MulAssign};
@@ -322,6 +323,7 @@ impl Basis {
322323
))
323324
}
324325

326+
#[allow(clippy::wrong_self_convention)]
325327
fn to_euler_inner(major: real, pair0: RVec2, pair1: RVec2, pair2: RVec2) -> RVec3 {
326328
match Self::is_between_neg1_1(major) {
327329
// It's -1
@@ -606,19 +608,6 @@ unsafe impl GodotFfi for Basis {
606608

607609
impl_godot_as_self!(Basis);
608610

609-
/// The ordering used to interpret a set of euler angles as extrinsic
610-
/// rotations.
611-
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
612-
#[repr(C)]
613-
pub enum EulerOrder {
614-
XYZ = 0,
615-
XZY = 1,
616-
YXZ = 2,
617-
YZX = 3,
618-
ZXY = 4,
619-
ZYX = 5,
620-
}
621-
622611
#[cfg(test)]
623612
mod test {
624613
use crate::builtin::real_consts::{FRAC_PI_2, PI};

godot-core/src/builtin/color.rs

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -368,31 +368,43 @@ impl ApproxEq for Color {
368368
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
369369
pub enum ColorChannelOrder {
370370
/// RGBA channel order. Godot's default.
371-
Rgba,
371+
RGBA,
372372

373373
/// ABGR channel order. Reverse of the default RGBA order.
374-
Abgr,
374+
ABGR,
375375

376376
/// ARGB channel order. More compatible with DirectX.
377-
Argb,
377+
ARGB,
378+
}
379+
380+
#[allow(non_upper_case_globals)]
381+
impl ColorChannelOrder {
382+
#[deprecated(note = "Renamed to `ColorChannelOrder::RGBA`.")]
383+
pub const Rgba: Self = Self::RGBA;
384+
385+
#[deprecated(note = "Renamed to `ColorChannelOrder::ABGR`.")]
386+
pub const Abgr: Self = Self::ABGR;
387+
388+
#[deprecated(note = "Renamed to `ColorChannelOrder::ARGB`.")]
389+
pub const Argb: Self = Self::ARGB;
378390
}
379391

380392
impl ColorChannelOrder {
381393
fn pack<T>(self, rgba: [T; 4]) -> [T; 4] {
382394
let [r, g, b, a] = rgba;
383395
match self {
384-
ColorChannelOrder::Rgba => [r, g, b, a],
385-
ColorChannelOrder::Abgr => [a, b, g, r],
386-
ColorChannelOrder::Argb => [a, r, g, b],
396+
ColorChannelOrder::RGBA => [r, g, b, a],
397+
ColorChannelOrder::ABGR => [a, b, g, r],
398+
ColorChannelOrder::ARGB => [a, r, g, b],
387399
}
388400
}
389401

390402
fn unpack<T>(self, xyzw: [T; 4]) -> [T; 4] {
391403
let [x, y, z, w] = xyzw;
392404
match self {
393-
ColorChannelOrder::Rgba => [x, y, z, w],
394-
ColorChannelOrder::Abgr => [w, z, y, x],
395-
ColorChannelOrder::Argb => [y, z, w, x],
405+
ColorChannelOrder::RGBA => [x, y, z, w],
406+
ColorChannelOrder::ABGR => [w, z, y, x],
407+
ColorChannelOrder::ARGB => [y, z, w, x],
396408
}
397409
}
398410
}

godot-core/src/builtin/mod.rs

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -151,14 +151,27 @@ pub(crate) fn u8_to_bool(u: u8) -> bool {
151151
/// The side of a [`Rect2`] or [`Rect2i`].
152152
///
153153
/// _Godot equivalent: `@GlobalScope.Side`_
154-
#[doc(alias = "Side")]
155-
#[derive(Copy, Clone, Debug)]
156-
#[repr(C)]
157-
pub enum RectSide {
158-
Left = 0,
159-
Top = 1,
160-
Right = 2,
161-
Bottom = 3,
154+
#[deprecated(note = "Merged with `godot::global::Side`.")]
155+
pub type RectSide = crate::engine::global::Side;
156+
use crate::engine::global::Side;
157+
158+
/// The ordering used to interpret a set of euler angles as extrinsic rotations.
159+
#[deprecated(note = "Merged with `godot::global::EulerOrder`.")]
160+
pub type EulerOrder = crate::engine::global::EulerOrder;
161+
162+
#[allow(non_upper_case_globals)]
163+
impl Side {
164+
#[deprecated(note = "Renamed to `Side::LEFT`.")]
165+
pub const Left: Side = Side::LEFT;
166+
167+
#[deprecated(note = "Renamed to `Side::TOP`.")]
168+
pub const Top: Side = Side::TOP;
169+
170+
#[deprecated(note = "Renamed to `Side::RIGHT`.")]
171+
pub const Right: Side = Side::RIGHT;
172+
173+
#[deprecated(note = "Renamed to `Side::BOTTOM`.")]
174+
pub const Bottom: Side = Side::BOTTOM;
162175
}
163176

164177
// ----------------------------------------------------------------------------------------------------------------------------------------------

0 commit comments

Comments
 (0)