moverox_codegen/move_enum/
mod.rs

1use std::collections::{HashMap, HashSet};
2
3use move_syn::{FieldsKind, ItemPath};
4use quote::quote;
5use unsynn::{Ident, LiteralString, ToTokens as _, TokenStream};
6
7use crate::generics::GenericsExt;
8use crate::{named_fields, positional_fields};
9
10/// The full Rust struct declaration and its `new` constructor.
11pub(super) fn to_rust(
12    this: &move_syn::Enum,
13    thecrate: &TokenStream,
14    package: Option<&LiteralString>,
15    module: Option<&Ident>,
16    address_map: &HashMap<Ident, TokenStream>,
17) -> TokenStream {
18    let move_syn::Enum {
19        ident, generics, ..
20    } = this;
21
22    let extra_attrs: TokenStream = package
23        .into_iter()
24        .map(unsynn::ToTokens::to_token_stream)
25        .map(|addr| quote!(#[move_(address = #addr)]))
26        .chain(module.map(|ident| quote!(#[move_(module = #ident)])))
27        .collect();
28
29    let rust_generics = generics
30        .as_ref()
31        .map(GenericsExt::to_rust)
32        .unwrap_or_default();
33
34    let mut phantoms = unused_phantoms(this);
35    let variants = this
36        .variants()
37        // HACK: pipe unused phantom parameters into the first variant to become phantom data fields
38        .map(|var| variant_to_rust(var, &std::mem::take(&mut phantoms), address_map));
39
40    // NOTE: this has to be formatted as a string first, so that `quote!` will turn it into a
41    // string literal later, which is what `#[serde(crate = ...)]` accepts
42    let serde_crate = format!("{thecrate}::serde").replace(" ", "");
43    quote! {
44        #[derive(
45            Clone,
46            Debug,
47            PartialEq,
48            Eq,
49            Hash,
50            #thecrate::traits::MoveDatatype,
51            #thecrate::serde::Deserialize,
52            #thecrate::serde::Serialize,
53        )]
54        #[move_(crate = #thecrate::traits)]
55        #[serde(crate = #serde_crate)]
56        #extra_attrs
57        #[allow(non_snake_case)]
58        pub enum #ident #rust_generics {
59            #(#variants),*
60        }
61    }
62}
63
64/// Collect `this` enum's phantom types that aren't used in any of its field types.
65fn unused_phantoms(this: &move_syn::Enum) -> Vec<Ident> {
66    let Some(generics) = this.generics.as_ref() else {
67        return Vec::new(); // short-circuit if the enum has no generics
68    };
69
70    let maybe_phantom_leaf_types: HashSet<_> = enum_leaf_types(this)
71        .filter_map(|path| match path {
72            ItemPath::Ident(ident) => Some(ident),
73            _ => None,
74        })
75        .collect();
76
77    generics
78        .phantoms()
79        .filter(|&ident| !maybe_phantom_leaf_types.contains(ident))
80        .cloned()
81        .collect()
82}
83
84/// Find all type parameters of `this` enum's fields that are 'leaf's, recursively.
85///
86/// A type parameter is a 'leaf' if it has no type parameters itself
87fn enum_leaf_types(this: &move_syn::Enum) -> Box<dyn Iterator<Item = &ItemPath> + '_> {
88    this.variants()
89        .flat_map(|var| &var.fields)
90        .flat_map(|fields| match fields {
91            FieldsKind::Positional(positional) => {
92                leaf_types_recursive(positional.fields().map(|field| &field.ty).boxed())
93            }
94            FieldsKind::Named(named) => {
95                leaf_types_recursive(named.fields().map(|field| &field.ty).boxed())
96            }
97        })
98        .boxed()
99}
100
101fn leaf_types_recursive<'a>(
102    types: Box<dyn Iterator<Item = &'a move_syn::Type> + 'a>,
103) -> Box<dyn Iterator<Item = &'a ItemPath> + 'a> {
104    types
105        .into_iter()
106        .flat_map(|t| {
107            t.type_args.as_ref().map_or_else(
108                || std::iter::once(&t.path).boxed(),
109                |t_args| leaf_types_recursive(t_args.types().boxed()),
110            )
111        })
112        .boxed()
113}
114
115fn variant_to_rust(
116    this: &move_syn::EnumVariant,
117    phantoms: &[Ident],
118    address_map: &HashMap<Ident, TokenStream>,
119) -> TokenStream {
120    use move_syn::FieldsKind as K;
121    let move_syn::EnumVariant {
122        attrs,
123        ident,
124        fields,
125    } = this;
126    let attrs = attrs
127        .iter()
128        .filter(|attr| attr.is_doc())
129        .map(|attr| attr.to_token_stream());
130
131    // Move enum variants can have empty fields
132    let bool_if_empty = false;
133    // Public enums in Rust already have all their variants' fields public
134    let visibility = false;
135
136    // If the variant is a unit (empty) one but there are phantom parameters, make it into a
137    // positional fields one with just the phantom data
138    let default_fields = (!phantoms.is_empty()).then(|| {
139        positional_fields::to_rust(
140            &Default::default(),
141            phantoms.iter(),
142            address_map,
143            bool_if_empty,
144            visibility,
145        )
146    });
147
148    let fields = fields
149        .as_ref()
150        .map(|kind| match kind {
151            K::Named(named) => {
152                named_fields::to_rust(named, phantoms.iter(), address_map, visibility)
153            }
154            K::Positional(positional) => positional_fields::to_rust(
155                positional,
156                phantoms.iter(),
157                address_map,
158                bool_if_empty,
159                visibility,
160            ),
161        })
162        .or(default_fields)
163        .unwrap_or_default();
164
165    quote! {
166        #(#attrs)*
167        #ident #fields
168    }
169}
170
171trait BoxedIter<'a>: Iterator + 'a {
172    fn boxed(self) -> Box<dyn Iterator<Item = Self::Item> + 'a>
173    where
174        Self: Sized,
175    {
176        Box::new(self)
177    }
178}
179
180impl<'a, T: Iterator + 'a> BoxedIter<'a> for T {}