Skip to content

Commit a50057e

Browse files
authored
Merge pull request #843 from godot-rust/feature/enum-ord-expressions
`#[derive(GodotClass)]` enums can now have complex ordinal expressions
2 parents 884b5d9 + 7a37953 commit a50057e

File tree

7 files changed

+212
-64
lines changed

7 files changed

+212
-64
lines changed

examples/hot-reload/rust/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ impl Reloadable {
6262
// ----------------------------------------------------------------------------------------------------------------------------------------------
6363

6464
#[derive(GodotConvert, Var, Export)]
65-
#[godot(via=GString)]
65+
#[godot(via = GString)]
6666
enum Planet {
6767
Earth,
6868
Mars,

godot-macros/src/derive/data_models/c_style_enum.rs

Lines changed: 36 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,24 @@
55
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
66
*/
77

8-
use proc_macro2::{Ident, Literal, Span, TokenTree};
9-
10-
use crate::util::{bail, error};
8+
use crate::util::bail;
119
use crate::ParseResult;
10+
use proc_macro2::{Ident, Span, TokenStream, TokenTree};
11+
use quote::{quote, ToTokens};
1212

13-
/// Stores info from c-style enums for use in deriving `GodotConvert` and other related traits.
13+
/// Stores info from C-style enums for use in deriving `GodotConvert` and other related traits.
1414
#[derive(Clone, Debug)]
1515
pub struct CStyleEnum {
16-
/// The names of each variant.
16+
/// The names of each enumerator.
1717
enumerator_names: Vec<Ident>,
18+
1819
/// The discriminants of each variant, both explicit and implicit.
19-
enumerator_ords: Vec<Literal>,
20+
///
21+
/// Can be simple or complex expressions, the latter with parentheses:
22+
/// - `13`
23+
/// - `(1 + 2)`
24+
/// - `Enum::Variant as isize`
25+
enumerator_ords: Vec<TokenStream>,
2026
}
2127

2228
impl CStyleEnum {
@@ -30,49 +36,53 @@ impl CStyleEnum {
3036
.map(CStyleEnumerator::parse_enum_variant)
3137
.collect::<ParseResult<Vec<_>>>()?;
3238

33-
let (names, discriminants) = Self::create_discriminant_mapping(variants)?;
39+
let (names, ord_exprs) = Self::create_discriminant_mapping(variants)?;
3440

3541
Ok(Self {
3642
enumerator_names: names,
37-
enumerator_ords: discriminants,
43+
enumerator_ords: ord_exprs,
3844
})
3945
}
4046

4147
fn create_discriminant_mapping(
4248
enumerators: Vec<CStyleEnumerator>,
43-
) -> ParseResult<(Vec<Ident>, Vec<Literal>)> {
44-
// See here for how implicit discriminants are decided
49+
) -> ParseResult<(Vec<Ident>, Vec<TokenStream>)> {
50+
// See here for how implicit discriminants are decided:
4551
// https://doc.rust-lang.org/reference/items/enumerations.html#implicit-discriminants
4652
let mut names = Vec::new();
47-
let mut discriminants = Vec::new();
53+
let mut ord_exprs = Vec::new();
4854

49-
let mut last_discriminant = None;
55+
let mut last_ord = None;
5056
for enumerator in enumerators.into_iter() {
51-
let discriminant_span = enumerator.discriminant_span();
52-
53-
let discriminant = match enumerator.discriminant_as_i64()? {
54-
Some(discriminant) => discriminant,
55-
None => last_discriminant.unwrap_or(0) + 1,
57+
let span = enumerator.discriminant_span();
58+
let ord = match enumerator.discriminant {
59+
Some(mut discriminant) => {
60+
discriminant.set_span(span);
61+
discriminant.to_token_stream()
62+
}
63+
None if last_ord.is_none() => quote! { 0 },
64+
None => quote! { #last_ord + 1 },
5665
};
57-
last_discriminant = Some(discriminant);
5866

59-
let mut discriminant = Literal::i64_unsuffixed(discriminant);
60-
discriminant.set_span(discriminant_span);
67+
last_ord = Some(ord.clone());
68+
69+
// let discriminant_span = enumerator.discriminant_span();
70+
// discriminant.set_span(discriminant_span);
6171

6272
names.push(enumerator.name);
63-
discriminants.push(discriminant)
73+
ord_exprs.push(ord)
6474
}
6575

66-
Ok((names, discriminants))
76+
Ok((names, ord_exprs))
6777
}
6878

69-
/// Returns the names of the variants, in order of the variants.
70-
pub fn names(&self) -> &[Ident] {
79+
/// Returns the names of the enumerators, in order of declaration.
80+
pub fn enumerator_names(&self) -> &[Ident] {
7181
&self.enumerator_names
7282
}
7383

74-
/// Returns the discriminants of each variant, in order of the variants.
75-
pub fn discriminants(&self) -> &[Literal] {
84+
/// Returns the ordinal expression (discriminant) of each enumerator, in order of declaration.
85+
pub fn enumerator_ord_exprs(&self) -> &[TokenStream] {
7686
&self.enumerator_ords
7787
}
7888

@@ -124,20 +134,6 @@ impl CStyleEnumerator {
124134
})
125135
}
126136

127-
/// Returns the discriminant parsed as an i64 literal.
128-
fn discriminant_as_i64(&self) -> ParseResult<Option<i64>> {
129-
let Some(discriminant) = self.discriminant.as_ref() else {
130-
return Ok(None);
131-
};
132-
133-
let int = discriminant
134-
.to_string()
135-
.parse::<i64>()
136-
.map_err(|_| error!(discriminant, "expected i64 literal"))?;
137-
138-
Ok(Some(int))
139-
}
140-
141137
/// Returns a span suitable for the discriminant of the variant.
142138
///
143139
/// If there was no explicit discriminant, this will use the span of the name instead.

godot-macros/src/derive/derive_from_godot.rs

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,33 @@
55
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
66
*/
77

8+
use crate::derive::data_models::{CStyleEnum, ConvertType, GodotConvert, NewtypeStruct, ViaType};
9+
use crate::derive::derive_godot_convert::EnumeratorExprCache;
10+
use crate::util;
811
use proc_macro2::{Ident, TokenStream};
912
use quote::quote;
1013

11-
use crate::derive::data_models::{CStyleEnum, ConvertType, GodotConvert, NewtypeStruct, ViaType};
12-
1314
/// Creates a `FromGodot` impl for the given `GodotConvert`.
1415
///
1516
/// There is no dedicated `FromGodot` derive macro currently, this is instead called by the `GodotConvert` derive macro.
16-
pub fn make_fromgodot(convert: &GodotConvert) -> TokenStream {
17+
pub fn make_fromgodot(convert: &GodotConvert, cache: &mut EnumeratorExprCache) -> TokenStream {
1718
let GodotConvert {
1819
ty_name: name,
1920
convert_type: data,
2021
} = convert;
2122

2223
match data {
2324
ConvertType::NewType { field } => make_fromgodot_for_newtype_struct(name, field),
25+
2426
ConvertType::Enum {
2527
variants,
2628
via: ViaType::GString { .. },
2729
} => make_fromgodot_for_gstring_enum(name, variants),
30+
2831
ConvertType::Enum {
2932
variants,
3033
via: ViaType::Int { int_ident },
31-
} => make_fromgodot_for_int_enum(name, variants, int_ident),
34+
} => make_fromgodot_for_int_enum(name, variants, int_ident, cache),
3235
}
3336
}
3437

@@ -49,17 +52,35 @@ fn make_fromgodot_for_newtype_struct(name: &Ident, field: &NewtypeStruct) -> Tok
4952
}
5053

5154
/// Derives `FromGodot` for enums with a via type of integers.
52-
fn make_fromgodot_for_int_enum(name: &Ident, enum_: &CStyleEnum, int: &Ident) -> TokenStream {
53-
let discriminants = enum_.discriminants();
54-
let names = enum_.names();
55+
fn make_fromgodot_for_int_enum(
56+
name: &Ident,
57+
enum_: &CStyleEnum,
58+
int: &Ident,
59+
cache: &mut EnumeratorExprCache,
60+
) -> TokenStream {
61+
let discriminants =
62+
cache.map_ord_exprs(int, enum_.enumerator_names(), enum_.enumerator_ord_exprs());
63+
let names = enum_.enumerator_names();
5564
let bad_variant_error = format!("invalid {name} variant");
5665

66+
let ord_variables: Vec<Ident> = names
67+
.iter()
68+
.map(|e| util::ident(&format!("ORD_{e}")))
69+
.collect();
70+
5771
quote! {
5872
impl ::godot::meta::FromGodot for #name {
73+
#[allow(unused_parens)] // Error "unnecessary parentheses around match arm expression"; comes from ord° expressions like (1 + 2).
5974
fn try_from_godot(via: #int) -> ::std::result::Result<Self, ::godot::meta::error::ConvertError> {
75+
#(
76+
// Interesting: using let instead of const would introduce a runtime bug. Its values cannot be used in match lhs (binding).
77+
// However, bindings silently shadow variables, so the first match arm always runs; no warning in generated proc-macro code.
78+
const #ord_variables: #int = #discriminants;
79+
)*
80+
6081
match via {
6182
#(
62-
#discriminants => Ok(#name::#names),
83+
#ord_variables => Ok(#name::#names),
6384
)*
6485
// Pass `via` and not `other`, to retain debug info of original type.
6586
other => Err(::godot::meta::error::ConvertError::with_error_value(#bad_variant_error, via))
@@ -71,7 +92,7 @@ fn make_fromgodot_for_int_enum(name: &Ident, enum_: &CStyleEnum, int: &Ident) ->
7192

7293
/// Derives `FromGodot` for enums with a via type of `GString`.
7394
fn make_fromgodot_for_gstring_enum(name: &Ident, enum_: &CStyleEnum) -> TokenStream {
74-
let names = enum_.names();
95+
let names = enum_.enumerator_names();
7596
let names_str = names.iter().map(ToString::to_string).collect::<Vec<_>>();
7697
let bad_variant_error = format!("invalid {name} variant");
7798

godot-macros/src/derive/derive_godot_convert.rs

Lines changed: 88 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,12 @@
44
* License, v. 2.0. If a copy of the MPL was not distributed with this
55
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
66
*/
7-
8-
use proc_macro2::TokenStream;
9-
use quote::quote;
10-
117
use crate::derive::data_models::GodotConvert;
128
use crate::derive::{make_fromgodot, make_togodot};
139
use crate::ParseResult;
10+
use proc_macro2::{Ident, TokenStream, TokenTree};
11+
use quote::quote;
12+
use std::collections::HashMap;
1413

1514
/// Derives `GodotConvert` for the given declaration.
1615
///
@@ -20,9 +19,10 @@ pub fn derive_godot_convert(item: venial::Item) -> ParseResult<TokenStream> {
2019

2120
let name = &convert.ty_name;
2221
let via_type = convert.convert_type.via_type();
22+
let mut cache = EnumeratorExprCache::default();
2323

24-
let to_godot_impl = make_togodot(&convert);
25-
let from_godot_impl = make_fromgodot(&convert);
24+
let to_godot_impl = make_togodot(&convert, &mut cache);
25+
let from_godot_impl = make_fromgodot(&convert, &mut cache);
2626

2727
Ok(quote! {
2828
impl ::godot::meta::GodotConvert for #name {
@@ -33,3 +33,85 @@ pub fn derive_godot_convert(item: venial::Item) -> ParseResult<TokenStream> {
3333
#from_godot_impl
3434
})
3535
}
36+
37+
// ----------------------------------------------------------------------------------------------------------------------------------------------
38+
// Helpers for submodules
39+
40+
/// Caches enumerator ordinal expressions that are modified, e.g. `(1 + 2) as isize` -> `(1 + 2) as i64`.
41+
#[derive(Default)]
42+
pub struct EnumeratorExprCache {
43+
is_initialized: bool,
44+
/// Contains only overridden ones (where the default wouldn't fit). Key is enumerator name.
45+
ord_expr_by_name: HashMap<Ident, TokenStream>,
46+
}
47+
48+
impl EnumeratorExprCache {
49+
/// Returns an iterator of ord expressions, with those replaced that have been overridden.
50+
///
51+
/// Requires that parameters are the same as in previous calls.
52+
pub fn map_ord_exprs<'ords: 'cache, 'cache>(
53+
&'cache mut self,
54+
int: &'ords Ident,
55+
names: &'ords [Ident],
56+
ord_exprs: &'ords [TokenStream],
57+
) -> impl Iterator<Item = &TokenStream> + 'cache {
58+
self.ensure_initialized(int, names, ord_exprs);
59+
60+
names
61+
.iter()
62+
.zip(ord_exprs.iter())
63+
.map(|(name, ord_expr)| self.ord_expr_by_name.get(name).unwrap_or(ord_expr))
64+
}
65+
66+
/// Goes through all (name, ord_expr) pairs and builds special cases.
67+
///
68+
/// If initialized before, does nothing.
69+
fn ensure_initialized(&mut self, int: &Ident, names: &[Ident], ord_exprs: &[TokenStream]) {
70+
if self.is_initialized {
71+
return;
72+
}
73+
74+
for (enumerator_name, ord_expr) in names.iter().zip(ord_exprs) {
75+
if let Some(new_ord_expr) = adjust_ord_expr(ord_expr, int) {
76+
self.ord_expr_by_name
77+
.insert(enumerator_name.clone(), new_ord_expr);
78+
}
79+
}
80+
81+
self.is_initialized = true;
82+
}
83+
}
84+
85+
fn adjust_ord_expr(ord_expr: &TokenStream, int: &Ident) -> Option<TokenStream> {
86+
// If the token stream ends in `as isize`, this is typically a constant conversion (e.g. MyVariant = OtherEnum::Variant as isize).
87+
// Then, replace `as isize` (which is required for Rust enum) with `as #int`. This currently ignores type narrowing errors.
88+
89+
let paren_group = ord_expr
90+
.clone()
91+
.into_iter()
92+
.next()
93+
.expect("no tokens in enumerator ord expression");
94+
95+
let TokenTree::Group(paren_expr) = paren_group else {
96+
// Early exit for simple expressions (literals).
97+
return None;
98+
};
99+
100+
// Could technically save this allocation by using field + clear() + extend().
101+
let mut tokens = Vec::from_iter(paren_expr.stream());
102+
103+
match tokens.as_slice() {
104+
// Ends with `as isize` => likely using another constant. We replace it with `as #int`, so it fits the underlying Godot type.
105+
// Since this is a derive macro, we can unfortunately not change the original definition.
106+
[.., TokenTree::Ident(tk_as), TokenTree::Ident(tk_isize)]
107+
if tk_as == "as" && tk_isize == "isize" =>
108+
{
109+
tokens.pop();
110+
tokens.push(TokenTree::Ident(int.clone()));
111+
112+
let stream = TokenStream::from_iter(tokens.iter().cloned());
113+
Some(stream)
114+
}
115+
_ => None,
116+
}
117+
}

0 commit comments

Comments
 (0)