Skip to main content

moverox_codegen/move_struct/
mod.rs

1use std::collections::{HashMap, HashSet};
2
3use move_syn::ItemPath;
4use proc_macro2::{Ident, TokenStream};
5use quote::quote;
6
7mod braced;
8mod tuple;
9
10use braced::BracedStructExt as _;
11use tuple::TupleStructExt as _;
12
13use crate::generics::GenericsExt;
14use crate::iter::BoxedIter as _;
15use crate::{ItemContext, Result};
16
17pub(super) trait StructGen {
18    /// The full Rust struct declaration, its `new` constructor and potentially its `HasKey`
19    /// implementation.
20    fn to_rust(&self, otw_types: HashSet<Ident>, ctx: ItemContext<'_>) -> Result<TokenStream>;
21}
22
23impl StructGen for move_syn::Struct {
24    fn to_rust(&self, otw_types: HashSet<Ident>, ctx: ItemContext<'_>) -> Result<TokenStream> {
25        let decl = self.rust_declaration(otw_types, ctx)?;
26        let impl_new = self.impl_new(ctx.address_map);
27        let impl_has_key_maybe = self.impl_has_key(ctx.thecrate).unwrap_or_default();
28        Ok(quote! {
29            #decl
30            #impl_new
31            #impl_has_key_maybe
32        })
33    }
34}
35
36trait StructExt {
37    fn rust_declaration(
38        &self,
39        otw_types: HashSet<Ident>,
40        ctx: ItemContext<'_>,
41    ) -> Result<TokenStream>;
42
43    /// The Rust code for the struct's `new` constructor.
44    fn impl_new(&self, address_map: &HashMap<Ident, TokenStream>) -> TokenStream;
45
46    /// If this is a braced struct and `key` is one of its abilities, then return the
47    /// `moverox_traits::HasKey` implementation for it.
48    fn impl_has_key(&self, thecrate: &TokenStream) -> Option<TokenStream>;
49
50    /// Any additional derives to prepend to the standard ones.
51    ///
52    /// Currently only `Default` if this struct is empty. Avoids the `clippy::new_without_default`
53    /// lint.
54    fn extra_derives(&self) -> Option<TokenStream>;
55
56    /// If any generic types, `<T, ...>`, else nothing.
57    ///
58    /// No bounds on the types; this is usually for usage after the datatype identifier.
59    fn generics(&self) -> TokenStream;
60
61    /// Generics for the main struct declaration. These may have defaults set.
62    fn type_generics(
63        &self,
64        thecrate: &TokenStream,
65        otw_types: HashSet<Ident>,
66    ) -> Result<TokenStream>;
67
68    /// Identifiers of each phantom type of the struct.
69    fn unused_phantoms(&self) -> impl Iterator<Item = &Ident>;
70}
71
72impl StructExt for move_syn::Struct {
73    fn rust_declaration(
74        &self,
75        otw_types: HashSet<Ident>,
76        ctx: ItemContext<'_>,
77    ) -> Result<TokenStream> {
78        use move_syn::StructKind as K;
79        let Self { ident, kind, .. } = self;
80
81        let extra_attrs: TokenStream = ctx
82            .package
83            .into_iter()
84            .map(unsynn::ToTokens::to_token_stream)
85            .map(|addr| quote!(#[move_(address = #addr)]))
86            .chain(ctx.module.map(|ident| quote!(#[move_(module = #ident)])))
87            .collect();
88        let extra_derives = self.extra_derives().unwrap_or_default();
89        let type_generics = self.type_generics(ctx.thecrate, otw_types)?;
90        let contents = match kind {
91            K::Braced(braced) => braced.to_rust_contents(self.unused_phantoms(), ctx.address_map),
92            K::Tuple(tuple) => tuple.to_rust_contents(self.unused_phantoms(), ctx.address_map),
93        };
94        // NOTE: this has to be formatted as a string first, so that `quote!` will turn it into a
95        // string literal later, which is what `#[serde(crate = ...)]` accepts
96        let thecrate = ctx.thecrate;
97        let serde_crate = format!("{thecrate}::serde").replace(" ", "");
98        Ok(quote! {
99            #[derive(
100                #extra_derives
101                Clone,
102                Debug,
103                PartialEq,
104                Eq,
105                Hash,
106                #thecrate::traits::MoveDatatype,
107                #thecrate::serde::Deserialize,
108                #thecrate::serde::Serialize,
109            )]
110            #[move_(crate = #thecrate::traits)]
111            #[serde(crate = #serde_crate)]
112            #extra_attrs
113            #[allow(non_snake_case)]
114            pub struct #ident #type_generics #contents
115        })
116    }
117
118    fn impl_new(&self, address_map: &HashMap<Ident, TokenStream>) -> TokenStream {
119        use move_syn::StructKind;
120        let Self { ident, kind, .. } = self;
121        let generics = self.generics();
122        let (args, assignments) = match kind {
123            StructKind::Braced(braced) => braced.impl_new(self.unused_phantoms(), address_map),
124            StructKind::Tuple(tuple) => tuple.impl_new(self.unused_phantoms(), address_map),
125        };
126        quote! {
127            impl #generics #ident #generics {
128                #[allow(clippy::just_underscores_and_digits, clippy::too_many_arguments)]
129                pub const fn new(#args) -> Self {
130                    Self #assignments
131                }
132            }
133        }
134    }
135
136    /// Impl `moverox_traits::HasKey` if the struct has the 'key' ability.
137    ///
138    /// # Note
139    ///
140    /// This assumes the Move adapter (platform) is similar to Sui/IOTA in that:
141    /// - objects are required to have an `id: UID` field
142    /// - `UID` has a `id: ID` field
143    /// - `ID` has a `bytes: address` field
144    ///
145    /// Hence this generates code to return that innermost `bytes` field.
146    fn impl_has_key(&self, thecrate: &TokenStream) -> Option<TokenStream> {
147        use move_syn::Ability;
148        if !self.abilities().any(|a| matches!(a, Ability::Key(_))) {
149            return None;
150        }
151        let ident = &self.ident;
152        let generics = self.generics();
153        Some(quote! {
154            impl #generics #thecrate::traits::HasKey for  #ident #generics {
155                fn address(&self) -> #thecrate::types::Address {
156                    self.id.id.bytes
157                }
158            }
159        })
160    }
161
162    fn extra_derives(&self) -> Option<TokenStream> {
163        use move_syn::StructKind;
164        let is_empty = match &self.kind {
165            StructKind::Braced(braced) => braced.is_empty(),
166            StructKind::Tuple(tuple) => tuple.is_empty(),
167        };
168        is_empty.then_some(quote!(Default,))
169    }
170
171    fn generics(&self) -> TokenStream {
172        self.generics
173            .as_ref()
174            .map(GenericsExt::to_rust)
175            .unwrap_or_default()
176    }
177
178    fn type_generics(
179        &self,
180        thecrate: &TokenStream,
181        otw_types: HashSet<Ident>,
182    ) -> Result<TokenStream> {
183        self.generics
184            .as_ref()
185            .map(|g| g.type_generics(thecrate, otw_types))
186            .transpose()
187            .map(Option::unwrap_or_default)
188    }
189
190    /// Phantom types that don't appear in any field types.
191    fn unused_phantoms(&self) -> impl Iterator<Item = &Ident> {
192        unused_phantoms(self)
193    }
194}
195
196/// Collect `this` struct's phantom types that aren't used in any of its field types.
197fn unused_phantoms(this: &move_syn::Struct) -> impl Iterator<Item = &Ident> {
198    let maybe_phantom_leaf_types: HashSet<_> = struct_leaf_types(this)
199        .filter_map(|path| match path {
200            ItemPath::Ident(ident) => Some(ident),
201            _ => None,
202        })
203        .collect();
204
205    this.generics
206        .iter()
207        .flat_map(|g| g.phantoms())
208        .filter(move |&ident| !maybe_phantom_leaf_types.contains(ident))
209}
210
211/// Find all type parameters of `this` enum's fields that are 'leaf's, recursively.
212///
213/// A type parameter is a 'leaf' if it has no type parameters itself
214fn struct_leaf_types(this: &move_syn::Struct) -> Box<dyn Iterator<Item = &ItemPath> + '_> {
215    match &this.kind {
216        move_syn::StructKind::Braced(named) => {
217            leaf_types_recursive(named.fields().map(|field| &field.ty).boxed())
218        }
219        move_syn::StructKind::Tuple(positional) => {
220            leaf_types_recursive(positional.fields().map(|field| &field.ty).boxed())
221        }
222    }
223    .boxed()
224}
225
226fn leaf_types_recursive<'a>(
227    types: Box<dyn Iterator<Item = &'a move_syn::Type> + 'a>,
228) -> Box<dyn Iterator<Item = &'a ItemPath> + 'a> {
229    types
230        .into_iter()
231        .flat_map(|t| {
232            t.type_args.as_ref().map_or_else(
233                || std::iter::once(&t.path).boxed(),
234                |t_args| leaf_types_recursive(t_args.types().boxed()),
235            )
236        })
237        .boxed()
238}