Skip to content

Commit

Permalink
Merge pull request #599 from lilizoey/refactor/rewrite-convert-derives
Browse files Browse the repository at this point in the history
Rewrite the derive macros related to `GodotConvert`
  • Loading branch information
Bromeon committed Feb 11, 2024
2 parents c96f4f6 + 4b445c6 commit 535ec61
Show file tree
Hide file tree
Showing 20 changed files with 888 additions and 1,180 deletions.
150 changes: 150 additions & 0 deletions godot-macros/src/derive/data_models/c_style_enum.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*
* Copyright (c) godot-rust; Bromeon and contributors.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

use proc_macro2::{Ident, Literal, Span, TokenTree};

use crate::util::{bail, error};
use crate::ParseResult;

/// Stores info from c-style enums for use in deriving `GodotConvert` and other related traits.
#[derive(Clone, Debug)]
pub struct CStyleEnum {
/// The names of each variant.
enumerator_names: Vec<Ident>,
/// The discriminants of each variant, both explicit and implicit.
enumerator_ords: Vec<Literal>,
}

impl CStyleEnum {
/// Parses the enum.
///
/// Ensures all the variants are unit variants, and that any explicit discriminants are integer literals.
pub fn parse_enum(enum_: &venial::Enum) -> ParseResult<Self> {
let variants = enum_
.variants
.items()
.map(CStyleEnumerator::parse_enum_variant)
.collect::<ParseResult<Vec<_>>>()?;

let (names, discriminants) = Self::create_discriminant_mapping(variants)?;

Ok(Self {
enumerator_names: names,
enumerator_ords: discriminants,
})
}

fn create_discriminant_mapping(
enumerators: Vec<CStyleEnumerator>,
) -> ParseResult<(Vec<Ident>, Vec<Literal>)> {
// See here for how implicit discriminants are decided
// https://doc.rust-lang.org/reference/items/enumerations.html#implicit-discriminants
let mut names = Vec::new();
let mut discriminants = Vec::new();

let mut last_discriminant = None;
for enumerator in enumerators.into_iter() {
let discriminant_span = enumerator.discriminant_span();

let discriminant = match enumerator.discriminant_as_i64()? {
Some(discriminant) => discriminant,
None => last_discriminant.unwrap_or(0) + 1,
};
last_discriminant = Some(discriminant);

let mut discriminant = Literal::i64_unsuffixed(discriminant);
discriminant.set_span(discriminant_span);

names.push(enumerator.name);
discriminants.push(discriminant)
}

Ok((names, discriminants))
}

/// Returns the names of the variants, in order of the variants.
pub fn names(&self) -> &[Ident] {
&self.enumerator_names
}

/// Returns the discriminants of each variant, in order of the variants.
pub fn discriminants(&self) -> &[Literal] {
&self.enumerator_ords
}

/// Return a hint string for use with `PropertyHint::ENUM` where each variant has an explicit integer hint.
pub fn to_int_hint(&self) -> String {
self.enumerator_names
.iter()
.zip(self.enumerator_ords.iter())
.map(|(name, discrim)| format!("{name}:{discrim}"))
.collect::<Vec<_>>()
.join(",")
}

/// Return a hint string for use with `PropertyHint::ENUM` where the variants are just kept as strings.
pub fn to_string_hint(&self) -> String {
self.enumerator_names
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(",")
}
}

/// Each variant in a c-style enum.
#[derive(Clone, Debug)]
pub struct CStyleEnumerator {
/// The name of the variant.
name: Ident,
/// The explicit discriminant of the variant, `None` means there was no explicit discriminant.
discriminant: Option<TokenTree>,
}

impl CStyleEnumerator {
/// Parse an enum variant, erroring if it isn't a unit variant.
fn parse_enum_variant(enum_variant: &venial::EnumVariant) -> ParseResult<Self> {
match enum_variant.contents {
venial::StructFields::Unit => {}
_ => {
return bail!(
&enum_variant.contents,
"GodotConvert only supports c-style enums"
)
}
}

Ok(Self {
name: enum_variant.name.clone(),
discriminant: enum_variant.value.as_ref().map(|val| &val.value).cloned(),
})
}

/// Returns the discriminant parsed as an i64 literal.
fn discriminant_as_i64(&self) -> ParseResult<Option<i64>> {
let Some(discriminant) = self.discriminant.as_ref() else {
return Ok(None);
};

let int = discriminant
.to_string()
.parse::<i64>()
.map_err(|_| error!(discriminant, "expected i64 literal"))?;

Ok(Some(int))
}

/// Returns a span suitable for the discriminant of the variant.
///
/// If there was no explicit discriminant, this will use the span of the name instead.
fn discriminant_span(&self) -> Span {
match &self.discriminant {
Some(discriminant) => discriminant.span(),
None => self.name.span(),
}
}
}
90 changes: 90 additions & 0 deletions godot-macros/src/derive/data_models/godot_attribute.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright (c) godot-rust; Bromeon and contributors.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

use proc_macro2::{Ident, Span, TokenStream};
use quote::ToTokens;
use venial::Declaration;

use crate::util::{bail, KvParser};
use crate::ParseResult;

/// Stores data related to the `#[godot(..)]` attribute.
pub enum GodotAttribute {
/// `#[godot(transparent)]`
Transparent { span: Span },
/// `#[godot(via = via_type)]`
Via { span: Span, via_type: ViaType },
}

impl GodotAttribute {
pub fn parse_attribute(declaration: &Declaration) -> ParseResult<Self> {
let mut parser = KvParser::parse_required(declaration.attributes(), "godot", declaration)?;
let attribute = Self::parse(&mut parser)?;
parser.finish()?;

Ok(attribute)
}

fn parse(parser: &mut KvParser) -> ParseResult<Self> {
let span = parser.span();

if parser.handle_alone("transparent")? {
return Ok(Self::Transparent { span });
}

if let Some(via_type) = parser.handle_ident("via")? {
return Ok(Self::Via {
span,
via_type: ViaType::parse_ident(via_type)?,
});
}

bail!(
span,
"expected either `#[godot(transparent)]` or `#[godot(via = <via_type>)]`"
)
}

/// The span of the entire attribute.
///
/// Specifically this is the span of the `[ ]` group from a `#[godot(...)]` attribute.
pub fn span(&self) -> Span {
match self {
GodotAttribute::Transparent { span } => *span,
GodotAttribute::Via { span, .. } => *span,
}
}
}

/// The via type from a `#[godot(via = via_type)]` attribute.
pub enum ViaType {
/// The via type is `GString`
GString { gstring_ident: Ident },
/// The via type is an integer
Int { int_ident: Ident },
}

impl ViaType {
fn parse_ident(ident: Ident) -> ParseResult<Self> {
let via_type = match ident.to_string().as_str() {
"GString" => ViaType::GString { gstring_ident: ident },
"i8" |"i16" | "i32" | "i64" | "u8" | "u16" | "u32" => ViaType::Int { int_ident: ident },
other => return bail!(ident, "Via type `{other}` is not supported, expected one of: GString, i8, i16, i32, i64, u8, u16, u32")
};

Ok(via_type)
}
}

impl ToTokens for ViaType {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
ViaType::GString { gstring_ident } => gstring_ident.to_tokens(tokens),
ViaType::Int { int_ident } => int_ident.to_tokens(tokens),
}
}
}
117 changes: 117 additions & 0 deletions godot-macros/src/derive/data_models/godot_convert.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* Copyright (c) godot-rust; Bromeon and contributors.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

use proc_macro2::{Ident, TokenStream};
use quote::ToTokens;
use venial::Declaration;

use crate::util::bail;
use crate::ParseResult;

use super::c_style_enum::CStyleEnum;
use super::godot_attribute::{GodotAttribute, ViaType};
use super::newtype::NewtypeStruct;

/// Stores all relevant data to derive `GodotConvert` and other related traits.
pub struct GodotConvert {
/// The name of the type we're deriving for.
pub ty_name: Ident,
/// The data from the type and `godot` attribute.
pub convert_type: ConvertType,
}

impl GodotConvert {
pub fn parse_declaration(declaration: Declaration) -> ParseResult<Self> {
let (name, where_clause, generic_params) = match &declaration {
venial::Declaration::Struct(struct_) => (
struct_.name.clone(),
&struct_.where_clause,
&struct_.generic_params,
),
venial::Declaration::Enum(enum_) => (
enum_.name.clone(),
&enum_.where_clause,
&enum_.generic_params,
),
other => {
return bail!(
other,
"`GodotConvert` only supports structs and enums currently"
)
}
};

if let Some(where_clause) = where_clause {
return bail!(
where_clause,
"`GodotConvert` does not support where clauses"
);
}

if let Some(generic_params) = generic_params {
return bail!(generic_params, "`GodotConvert` does not support generics");
}

let data = ConvertType::parse_declaration(declaration)?;

Ok(Self {
ty_name: name,
convert_type: data,
})
}
}

/// Stores what kind of `GodotConvert` derive we're doing.
pub enum ConvertType {
/// Deriving for a newtype struct.
NewType { field: NewtypeStruct },
/// Deriving for an enum.
Enum { variants: CStyleEnum, via: ViaType },
}

impl ConvertType {
pub fn parse_declaration(declaration: Declaration) -> ParseResult<Self> {
let attribute = GodotAttribute::parse_attribute(&declaration)?;

match &declaration {
Declaration::Struct(struct_) => {
let GodotAttribute::Transparent { .. } = attribute else {
return bail!(attribute.span(), "`GodotConvert` on structs only works with `#[godot(transparent)]` currently");
};

Ok(Self::NewType {
field: NewtypeStruct::parse_struct(struct_)?,
})
}
Declaration::Enum(enum_) => {
let GodotAttribute::Via { via_type, .. } = attribute else {
return bail!(
attribute.span(),
"`GodotConvert` on enums requires `#[godot(via = ...)]` currently"
);
};

Ok(Self::Enum {
variants: CStyleEnum::parse_enum(enum_)?,
via: via_type,
})
}
_ => bail!(
declaration,
"`GodotConvert` only supports structs and enums currently"
),
}
}

/// Returns the type for use in `type Via = <type>;` in `GodotConvert` implementations.
pub fn via_type(&self) -> TokenStream {
match self {
ConvertType::NewType { field } => field.ty.to_token_stream(),
ConvertType::Enum { via, .. } => via.to_token_stream(),
}
}
}
16 changes: 16 additions & 0 deletions godot-macros/src/derive/data_models/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* Copyright (c) godot-rust; Bromeon and contributors.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

mod c_style_enum;
mod godot_attribute;
mod godot_convert;
mod newtype;

pub use c_style_enum::*;
pub use godot_attribute::*;
pub use godot_convert::*;
pub use newtype::*;

0 comments on commit 535ec61

Please sign in to comment.