Skip to main content

miden_note_schema_codegen/
lib.rs

1//! Shared Rust code generation for resolved note storage schemas.
2
3#![deny(missing_docs)]
4
5use std::{
6    collections::{BTreeMap, BTreeSet, HashMap},
7    fmt,
8};
9
10use heck::{ToSnakeCase, ToUpperCamelCase};
11use miden_note_schema::{
12    NoteStorageSchema, PrimitiveType, SchemaCase, SchemaField, SchemaType, SchemaTypeKind,
13    StandardLeaf,
14};
15use proc_macro2::{Ident, Literal, Span, TokenStream};
16use quote::{format_ident, quote};
17
18/// An error reported while generating Rust bindings.
19#[derive(Clone, Debug, Eq, PartialEq)]
20pub struct CodegenError {
21    message: String,
22}
23
24impl CodegenError {
25    /// Creates a code-generation error.
26    fn new(message: impl Into<String>) -> Self {
27        Self {
28            message: message.into(),
29        }
30    }
31}
32
33impl fmt::Display for CodegenError {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        f.write_str(&self.message)
36    }
37}
38
39impl std::error::Error for CodegenError {}
40
41/// Rust host types and metadata produced from one note storage schema.
42pub struct GeneratedTypes {
43    tokens: TokenStream,
44    root_ident: Ident,
45    type_idents: Vec<Ident>,
46    has_nested_named_types: bool,
47}
48
49impl GeneratedTypes {
50    /// Returns the generated Rust items.
51    pub const fn tokens(&self) -> &TokenStream {
52        &self.tokens
53    }
54
55    /// Returns the Rust identifier for the root storage type.
56    pub const fn root_ident(&self) -> &Ident {
57        &self.root_ident
58    }
59
60    /// Returns every generated Rust type identifier in emission order.
61    pub fn type_idents(&self) -> &[Ident] {
62        &self.type_idents
63    }
64
65    /// Returns true when the schema has a generated named type below its storage root.
66    pub const fn has_nested_named_types(&self) -> bool {
67        self.has_nested_named_types
68    }
69}
70
71/// Runtime crate paths used by generated host-profile code.
72#[derive(Clone)]
73pub struct RuntimePaths {
74    miden_field: TokenStream,
75    miden_field_repr: TokenStream,
76    miden_note_schema: TokenStream,
77    miden_protocol: TokenStream,
78}
79
80impl RuntimePaths {
81    /// Creates a runtime path set from four crate or module paths.
82    pub fn new(
83        miden_field: TokenStream,
84        miden_field_repr: TokenStream,
85        miden_note_schema: TokenStream,
86        miden_protocol: TokenStream,
87    ) -> Self {
88        Self {
89            miden_field,
90            miden_field_repr,
91            miden_note_schema,
92            miden_protocol,
93        }
94    }
95
96    /// Creates paths through a facade crate's hidden runtime re-exports.
97    pub fn through_facade(facade: TokenStream) -> Self {
98        Self::new(
99            quote!(#facade::__private::miden_field),
100            quote!(#facade::__private::miden_field_repr),
101            quote!(#facade::__private::miden_note_schema),
102            quote!(#facade::__private::miden_protocol),
103        )
104    }
105}
106
107impl Default for RuntimePaths {
108    fn default() -> Self {
109        Self::new(
110            quote!(::miden_field),
111            quote!(::miden_field_repr),
112            quote!(::miden_note_schema),
113            quote!(::miden_protocol),
114        )
115    }
116}
117
118/// Generates Rust host-profile types and structural felt conversion helpers.
119pub fn generate_host_types(
120    schema: &NoteStorageSchema,
121    runtime: &RuntimePaths,
122) -> Result<GeneratedTypes, CodegenError> {
123    schema
124        .validate_native_leaf_shapes()
125        .map_err(|error| CodegenError::new(error.to_string()))?;
126    let root = schema.root();
127    let root_fqn = root
128        .fqn()
129        .ok_or_else(|| CodegenError::new("the note storage root does not have a WIT FQN"))?;
130    let root_name = root
131        .name()
132        .ok_or_else(|| CodegenError::new("the note storage root does not have a WIT name"))?;
133
134    let mut definitions = Vec::new();
135    let mut seen = BTreeSet::new();
136    collect_named_types(root, &mut seen, &mut definitions)?;
137
138    let mut rust_names = BTreeMap::new();
139    let mut used_names = BTreeMap::<String, String>::new();
140    for definition in &definitions {
141        let fqn = definition.fqn().expect("collected named types always have a FQN");
142        let name = definition.name().expect("collected named types always have a name");
143        let ident = type_ident(name);
144        if let Some(existing) = used_names.insert(ident.to_string(), fqn.to_owned())
145            && existing != fqn
146        {
147            return Err(CodegenError::new(format!(
148                "WIT types `{existing}` and `{fqn}` both map to Rust type `{ident}`"
149            )));
150        }
151        rust_names.insert(fqn.to_owned(), ident);
152    }
153
154    let root_ident = rust_names.get(root_fqn).cloned().unwrap_or_else(|| type_ident(root_name));
155    let helper_traits = generate_helper_traits(runtime);
156    let mut felt_repr_support = FeltReprSupport::default();
157    let items = definitions
158        .iter()
159        .map(|definition| generate_type(definition, &rust_names, runtime, &mut felt_repr_support))
160        .collect::<Result<Vec<_>, _>>()?;
161    let type_idents = definitions
162        .iter()
163        .map(|definition| {
164            rust_names
165                .get(definition.fqn().expect("generated types have an FQN"))
166                .expect("generated types have a Rust name")
167                .clone()
168        })
169        .collect();
170    let has_nested_named_types = definitions
171        .iter()
172        .any(|definition| definition.fqn().is_some_and(|fqn| fqn != root_fqn));
173
174    Ok(GeneratedTypes {
175        tokens: quote! {
176            #helper_traits
177            #(#items)*
178        },
179        root_ident,
180        type_idents,
181        has_nested_named_types,
182    })
183}
184
185/// Collects reachable named records and variants, excluding mapped protocol leaves.
186fn collect_named_types<'a>(
187    ty: &'a SchemaType,
188    seen: &mut BTreeSet<String>,
189    definitions: &mut Vec<&'a SchemaType>,
190) -> Result<(), CodegenError> {
191    if ty.standard_leaf().is_some() {
192        return Ok(());
193    }
194
195    let is_definition = matches!(ty.kind(), SchemaTypeKind::Record(_) | SchemaTypeKind::Variant(_));
196    if is_definition {
197        let fqn = ty.fqn().ok_or_else(|| {
198            CodegenError::new("anonymous WIT records and variants cannot be emitted as Rust types")
199        })?;
200        ty.name().ok_or_else(|| {
201            CodegenError::new(format!("WIT type `{fqn}` does not have a local name"))
202        })?;
203        if !seen.insert(fqn.to_owned()) {
204            return Ok(());
205        }
206        definitions.push(ty);
207    }
208
209    match ty.kind() {
210        SchemaTypeKind::Record(fields) => {
211            for field in fields {
212                collect_named_types(field.ty(), seen, definitions)?;
213            }
214        }
215        SchemaTypeKind::Option(payload) => collect_named_types(payload, seen, definitions)?,
216        SchemaTypeKind::Variant(cases) => {
217            for payload in cases.iter().filter_map(SchemaCase::payload) {
218                collect_named_types(payload, seen, definitions)?;
219            }
220        }
221        SchemaTypeKind::Felt | SchemaTypeKind::Primitive(_) => {}
222    }
223    Ok(())
224}
225
226/// Generates the private traits that keep protocol-leaf order separate from foreign trait impls.
227fn generate_helper_traits(runtime: &RuntimePaths) -> TokenStream {
228    let RuntimePaths {
229        miden_field,
230        miden_field_repr,
231        miden_note_schema,
232        miden_protocol,
233    } = runtime;
234    let primitive_impls = [
235        quote!(u64),
236        quote!(u32),
237        quote!(u8),
238        quote!(bool),
239        quote!(#miden_field::Felt),
240        quote!(#miden_field::Word),
241    ]
242    .into_iter()
243    .map(|ty| {
244        quote! {
245            impl __MidenNoteEncode for #ty {
246                fn __write_note_felts(
247                    &self,
248                    writer: &mut #miden_field_repr::FeltWriter<'_>,
249                ) -> #miden_note_schema::Result<()> {
250                    #miden_field_repr::ToFeltRepr::write_felt_repr(self, writer);
251                    Ok(())
252                }
253            }
254
255            impl __MidenNoteDecode for #ty {
256                fn __read_note_felts(
257                    reader: &mut #miden_field_repr::FeltReader<'_>,
258                ) -> #miden_note_schema::Result<Self> {
259                    #miden_field_repr::FromFeltRepr::from_felt_repr(reader).map_err(|error| {
260                        #miden_note_schema::Error::new(format!(
261                            "failed to decode {} from note storage: {error}",
262                            stringify!(#ty),
263                        ))
264                    })
265                }
266            }
267        }
268    });
269
270    quote! {
271        #[doc(hidden)]
272        trait __MidenNoteEncode {
273            fn __write_note_felts(
274                &self,
275                writer: &mut #miden_field_repr::FeltWriter<'_>,
276            ) -> #miden_note_schema::Result<()>;
277        }
278
279        #[doc(hidden)]
280        trait __MidenNoteDecode: ::core::marker::Sized {
281            fn __read_note_felts(
282                reader: &mut #miden_field_repr::FeltReader<'_>,
283            ) -> #miden_note_schema::Result<Self>;
284        }
285
286        #(#primitive_impls)*
287
288        impl __MidenNoteEncode for #miden_protocol::account::AccountId {
289            fn __write_note_felts(
290                &self,
291                writer: &mut #miden_field_repr::FeltWriter<'_>,
292            ) -> #miden_note_schema::Result<()> {
293                // The WIT record declares prefix before suffix.
294                writer.write(self.prefix().as_felt());
295                writer.write(self.suffix());
296                Ok(())
297            }
298        }
299
300        impl __MidenNoteDecode for #miden_protocol::account::AccountId {
301            fn __read_note_felts(
302                reader: &mut #miden_field_repr::FeltReader<'_>,
303            ) -> #miden_note_schema::Result<Self> {
304                let prefix = reader.read().map_err(|error| {
305                    #miden_note_schema::Error::new(format!(
306                        "failed to decode account-id prefix: {error}"
307                    ))
308                })?;
309                let suffix = reader.read().map_err(|error| {
310                    #miden_note_schema::Error::new(format!(
311                        "failed to decode account-id suffix: {error}"
312                    ))
313                })?;
314                // WIT declares prefix before suffix, but the constructor takes suffix first.
315                #miden_protocol::account::AccountId::try_from_elements(suffix, prefix).map_err(
316                    |error| {
317                        #miden_note_schema::Error::new(format!(
318                            "invalid account-id in note storage: {error}"
319                        ))
320                    },
321                )
322            }
323        }
324
325        impl __MidenNoteEncode for #miden_protocol::asset::AssetAmount {
326            fn __write_note_felts(
327                &self,
328                writer: &mut #miden_field_repr::FeltWriter<'_>,
329            ) -> #miden_note_schema::Result<()> {
330                writer.write(#miden_field::Felt::from(*self));
331                Ok(())
332            }
333        }
334
335        impl __MidenNoteDecode for #miden_protocol::asset::AssetAmount {
336            fn __read_note_felts(
337                reader: &mut #miden_field_repr::FeltReader<'_>,
338            ) -> #miden_note_schema::Result<Self> {
339                let value = reader.read().map_err(|error| {
340                    #miden_note_schema::Error::new(format!(
341                        "failed to decode asset-amount: {error}"
342                    ))
343                })?;
344                #miden_protocol::asset::AssetAmount::try_from(value).map_err(|error| {
345                    #miden_note_schema::Error::new(format!(
346                        "invalid asset-amount in note storage: {error}"
347                    ))
348                })
349            }
350        }
351
352        impl<T> __MidenNoteEncode for ::core::option::Option<T>
353        where
354            T: __MidenNoteEncode,
355        {
356            fn __write_note_felts(
357                &self,
358                writer: &mut #miden_field_repr::FeltWriter<'_>,
359            ) -> #miden_note_schema::Result<()> {
360                match self {
361                    None => writer.write(#miden_field::Felt::ZERO),
362                    Some(value) => {
363                        writer.write(#miden_field::Felt::ONE);
364                        value.__write_note_felts(writer)?;
365                    }
366                }
367                Ok(())
368            }
369        }
370
371        impl<T> __MidenNoteDecode for ::core::option::Option<T>
372        where
373            T: __MidenNoteDecode,
374        {
375            fn __read_note_felts(
376                reader: &mut #miden_field_repr::FeltReader<'_>,
377            ) -> #miden_note_schema::Result<Self> {
378                let tag = reader.read().map_err(|error| {
379                    #miden_note_schema::Error::new(format!(
380                        "failed to decode option tag: {error}"
381                    ))
382                })?;
383                match tag.as_canonical_u64() {
384                    0 => Ok(None),
385                    1 => Ok(Some(T::__read_note_felts(reader)?)),
386                    tag => Err(#miden_note_schema::Error::new(format!(
387                        "invalid option tag {tag}; expected 0 or 1"
388                    ))),
389                }
390            }
391        }
392    }
393}
394
395/// Generates one Rust record or variant and its structural conversion helpers.
396fn generate_type(
397    definition: &SchemaType,
398    rust_names: &BTreeMap<String, Ident>,
399    runtime: &RuntimePaths,
400    felt_repr_support: &mut FeltReprSupport,
401) -> Result<TokenStream, CodegenError> {
402    let fqn = definition.fqn().expect("generated type definitions always have a FQN");
403    let ident = rust_names.get(fqn).expect("every generated type has a Rust identifier");
404    let docs = type_docs(definition, fqn);
405    // Every trait is named through its absolute path. A generated type takes the name of its
406    // WIT type, so a schema may declare a type called `Clone`, `Debug`, or `Sized`.
407    let derives = if felt_repr_support.supports_native_felt_repr(definition) {
408        let miden_field_repr = &runtime.miden_field_repr;
409        // `quote` prints `::` paths with spaces around each separator. The attribute value must
410        // be one path the derive macro can parse, so the spaces come out.
411        let crate_path = Literal::string(&miden_field_repr.to_string().replace(' ', ""));
412        quote! {
413            #[derive(
414                ::core::clone::Clone,
415                ::core::fmt::Debug,
416                ::core::cmp::PartialEq,
417                ::core::cmp::Eq,
418                #miden_field_repr::ToFeltRepr,
419                #miden_field_repr::FromFeltRepr,
420            )]
421            #[felt_repr(crate_path = #crate_path)]
422        }
423    } else {
424        quote! {
425            #[derive(
426                ::core::clone::Clone,
427                ::core::fmt::Debug,
428                ::core::cmp::PartialEq,
429                ::core::cmp::Eq,
430            )]
431        }
432    };
433
434    let (item, encode_impl, decode_impl) = match definition.kind() {
435        SchemaTypeKind::Record(fields) => {
436            generate_record(ident, fields, rust_names, &docs, &derives, runtime)?
437        }
438        SchemaTypeKind::Variant(cases) => {
439            generate_variant(ident, cases, rust_names, &docs, &derives, runtime)?
440        }
441        _ => {
442            return Err(CodegenError::new(format!(
443                "named WIT type `{fqn}` is not a record or variant"
444            )));
445        }
446    };
447
448    Ok(quote! {
449        #item
450
451        impl #ident {
452            /// The canonical fully-qualified WIT name for this type.
453            pub const WIT_FQN: &'static str = #fqn;
454        }
455
456        #encode_impl
457        #decode_impl
458    })
459}
460
461/// Generates a Rust struct in WIT declaration order.
462fn generate_record(
463    ident: &Ident,
464    fields: &[SchemaField],
465    rust_names: &BTreeMap<String, Ident>,
466    docs: &TokenStream,
467    derives: &TokenStream,
468    runtime: &RuntimePaths,
469) -> Result<(TokenStream, TokenStream, TokenStream), CodegenError> {
470    let miden_field_repr = &runtime.miden_field_repr;
471    let miden_note_schema = &runtime.miden_note_schema;
472    let rust_fields = fields
473        .iter()
474        .map(|field| {
475            let ident = value_ident(field.name());
476            let ty = rust_type(field.ty(), rust_names, runtime)?;
477            let docs = field_docs(field);
478            Ok((ident, ty, docs))
479        })
480        .collect::<Result<Vec<_>, CodegenError>>()?;
481    let field_idents = rust_fields.iter().map(|(ident, ..)| ident).collect::<Vec<_>>();
482    let field_types = rust_fields.iter().map(|(_, ty, _)| ty).collect::<Vec<_>>();
483    let field_docs = rust_fields.iter().map(|(_, _, docs)| docs).collect::<Vec<_>>();
484
485    let item = quote! {
486        #docs
487        #derives
488        pub struct #ident {
489            #(
490                #field_docs
491                pub #field_idents: #field_types,
492            )*
493        }
494    };
495    let encode_impl = quote! {
496        impl __MidenNoteEncode for #ident {
497            fn __write_note_felts(
498                &self,
499                writer: &mut #miden_field_repr::FeltWriter<'_>,
500            ) -> #miden_note_schema::Result<()> {
501                #(self.#field_idents.__write_note_felts(writer)?;)*
502                Ok(())
503            }
504        }
505    };
506    let decode_impl = quote! {
507        impl __MidenNoteDecode for #ident {
508            fn __read_note_felts(
509                reader: &mut #miden_field_repr::FeltReader<'_>,
510            ) -> #miden_note_schema::Result<Self> {
511                Ok(Self {
512                    #(#field_idents: <#field_types as __MidenNoteDecode>::__read_note_felts(reader)?,)*
513                })
514            }
515        }
516    };
517    Ok((item, encode_impl, decode_impl))
518}
519
520/// Generates a Rust enum with declaration-ordinal tags.
521fn generate_variant(
522    ident: &Ident,
523    cases: &[SchemaCase],
524    rust_names: &BTreeMap<String, Ident>,
525    docs: &TokenStream,
526    derives: &TokenStream,
527    runtime: &RuntimePaths,
528) -> Result<(TokenStream, TokenStream, TokenStream), CodegenError> {
529    let miden_field = &runtime.miden_field;
530    let miden_field_repr = &runtime.miden_field_repr;
531    let miden_note_schema = &runtime.miden_note_schema;
532    let rust_cases = cases
533        .iter()
534        .map(|case| {
535            let ident = type_ident(case.name());
536            let payload =
537                case.payload().map(|ty| rust_type(ty, rust_names, runtime)).transpose()?;
538            let docs = case_docs(case);
539            Ok((ident, payload, docs))
540        })
541        .collect::<Result<Vec<_>, CodegenError>>()?;
542
543    let declarations = rust_cases.iter().map(|(case, payload, docs)| match payload {
544        Some(payload) => quote! {
545            #docs
546            #case(#payload),
547        },
548        None => quote! {
549            #docs
550            #case,
551        },
552    });
553    let encode_arms = rust_cases.iter().enumerate().map(|(ordinal, (case, payload, _))| {
554        let ordinal = ordinal as u32;
555        match payload {
556            Some(_) => quote! {
557                Self::#case(value) => {
558                    writer.write(#miden_field::Felt::from_u32(#ordinal));
559                    value.__write_note_felts(writer)?;
560                }
561            },
562            None => quote! {
563                Self::#case => {
564                    writer.write(#miden_field::Felt::from_u32(#ordinal));
565                }
566            },
567        }
568    });
569    let decode_arms = rust_cases.iter().enumerate().map(|(ordinal, (case, payload, _))| {
570        let ordinal = ordinal as u32;
571        match payload {
572            Some(payload) => quote! {
573                #ordinal => Ok(Self::#case(
574                    <#payload as __MidenNoteDecode>::__read_note_felts(reader)?,
575                )),
576            },
577            None => quote! { #ordinal => Ok(Self::#case), },
578        }
579    });
580    let case_count = cases.len();
581
582    let item = quote! {
583        #docs
584        #derives
585        pub enum #ident {
586            #(#declarations)*
587        }
588    };
589    let encode_impl = quote! {
590        impl __MidenNoteEncode for #ident {
591            fn __write_note_felts(
592                &self,
593                writer: &mut #miden_field_repr::FeltWriter<'_>,
594            ) -> #miden_note_schema::Result<()> {
595                match self {
596                    #(#encode_arms)*
597                }
598                Ok(())
599            }
600        }
601    };
602    let decode_impl = quote! {
603        impl __MidenNoteDecode for #ident {
604            fn __read_note_felts(
605                reader: &mut #miden_field_repr::FeltReader<'_>,
606            ) -> #miden_note_schema::Result<Self> {
607                let tag = reader.read_u32().map_err(|error| {
608                    #miden_note_schema::Error::new(format!(
609                        "failed to decode {} tag: {error}",
610                        stringify!(#ident),
611                    ))
612                })?;
613                match tag {
614                    #(#decode_arms)*
615                    tag => Err(#miden_note_schema::Error::new(format!(
616                        "invalid {} tag {tag}; expected a declaration ordinal below {}",
617                        stringify!(#ident),
618                        #case_count,
619                    ))),
620                }
621            }
622        }
623    };
624    Ok((item, encode_impl, decode_impl))
625}
626
627/// Maps one schema type to its host-profile Rust type.
628fn rust_type(
629    ty: &SchemaType,
630    rust_names: &BTreeMap<String, Ident>,
631    runtime: &RuntimePaths,
632) -> Result<TokenStream, CodegenError> {
633    if let Some(mapped) = mapped_leaf(ty, runtime) {
634        return Ok(mapped);
635    }
636    if let Some(fqn) = ty.fqn()
637        && let Some(ident) = rust_names.get(fqn)
638    {
639        return Ok(quote!(#ident));
640    }
641
642    match ty.kind() {
643        SchemaTypeKind::Primitive(PrimitiveType::U64) => Ok(quote!(u64)),
644        SchemaTypeKind::Primitive(PrimitiveType::U32) => Ok(quote!(u32)),
645        SchemaTypeKind::Primitive(PrimitiveType::U8) => Ok(quote!(u8)),
646        SchemaTypeKind::Primitive(PrimitiveType::Bool) => Ok(quote!(bool)),
647        SchemaTypeKind::Option(payload) => {
648            let payload = rust_type(payload, rust_names, runtime)?;
649            Ok(quote!(::core::option::Option<#payload>))
650        }
651        SchemaTypeKind::Record(_) | SchemaTypeKind::Variant(_) => Err(CodegenError::new(format!(
652            "named WIT type `{}` was not collected for Rust generation",
653            ty.fqn().or(ty.name()).unwrap_or("<anonymous>")
654        ))),
655        SchemaTypeKind::Felt => {
656            let miden_field = &runtime.miden_field;
657            Ok(quote!(#miden_field::Felt))
658        }
659    }
660}
661
662/// Returns the native Rust type for one standard WIT leaf.
663fn mapped_leaf(ty: &SchemaType, runtime: &RuntimePaths) -> Option<TokenStream> {
664    let RuntimePaths {
665        miden_field,
666        miden_protocol,
667        ..
668    } = runtime;
669    match ty.standard_leaf()? {
670        StandardLeaf::Felt => Some(quote!(#miden_field::Felt)),
671        StandardLeaf::Word => Some(quote!(#miden_field::Word)),
672        StandardLeaf::AccountId => Some(quote!(#miden_protocol::account::AccountId)),
673        StandardLeaf::AssetAmount => Some(quote!(#miden_protocol::asset::AssetAmount)),
674    }
675}
676
677/// Memoized answers for the felt-repr support walk, keyed by resolved node identity.
678#[derive(Default)]
679struct FeltReprSupport {
680    answers: HashMap<*const SchemaType, bool>,
681}
682
683impl FeltReprSupport {
684    /// Returns true when all fields implement the native felt-repr traits without protocol
685    /// adapters.
686    ///
687    /// The schema model is a DAG that shares one node per resolved WIT type, so the answers are
688    /// memoized by node identity. The walk is then linear in the number of distinct types instead
689    /// of the size of the expanded tree.
690    fn supports_native_felt_repr(&mut self, ty: &SchemaType) -> bool {
691        let key = ty as *const SchemaType;
692        if let Some(answer) = self.answers.get(&key) {
693            return *answer;
694        }
695        let answer = self.compute(ty);
696        self.answers.insert(key, answer);
697        answer
698    }
699
700    /// Answers the felt-repr question for one node from its children.
701    fn compute(&mut self, ty: &SchemaType) -> bool {
702        if matches!(ty.standard_leaf(), Some(StandardLeaf::AccountId | StandardLeaf::AssetAmount)) {
703            return false;
704        }
705        match ty.kind() {
706            SchemaTypeKind::Felt | SchemaTypeKind::Primitive(_) => true,
707            SchemaTypeKind::Record(fields) => {
708                fields.iter().all(|field| self.supports_native_felt_repr(field.ty()))
709            }
710            SchemaTypeKind::Option(payload) => self.supports_native_felt_repr(payload),
711            SchemaTypeKind::Variant(cases) => cases.iter().all(|case| {
712                case.payload().is_none_or(|payload| self.supports_native_felt_repr(payload))
713            }),
714        }
715    }
716}
717
718/// Produces documentation for a generated WIT type.
719fn type_docs(ty: &SchemaType, fqn: &str) -> TokenStream {
720    let docs = ty
721        .docs()
722        .map(str::to_owned)
723        .unwrap_or_else(|| format!("Rust binding for WIT type `{fqn}`."));
724    quote!(#[doc = #docs])
725}
726
727/// Produces documentation for a generated record field.
728fn field_docs(field: &SchemaField) -> TokenStream {
729    let docs = field
730        .docs()
731        .map(str::to_owned)
732        .unwrap_or_else(|| format!("Value of the WIT `{}` field.", field.name()));
733    quote!(#[doc = #docs])
734}
735
736/// Produces documentation for a generated variant case.
737fn case_docs(case: &SchemaCase) -> TokenStream {
738    let docs = case
739        .docs()
740        .map(str::to_owned)
741        .unwrap_or_else(|| format!("WIT `{}` case.", case.name()));
742    quote!(#[doc = #docs])
743}
744
745/// Returns the Rust type name the generator gives one WIT type or case name.
746///
747/// A caller that maps generated types back to WIT names uses this function, so the two sides
748/// agree on the upper camel case conversion and on the escape of a Rust reserved word.
749pub fn generated_type_ident(name: &str) -> String {
750    type_ident(name).to_string()
751}
752
753/// Converts a WIT type or case name to a Rust type identifier.
754fn type_ident(name: &str) -> Ident {
755    rust_ident(&name.to_upper_camel_case())
756}
757
758/// Converts a WIT field name to a Rust value identifier.
759fn value_ident(name: &str) -> Ident {
760    rust_ident(&name.replace('-', "_").to_snake_case())
761}
762
763/// Creates an identifier and avoids Rust reserved words.
764fn rust_ident(name: &str) -> Ident {
765    const RESERVED: &[&str] = &[
766        "Self", "abstract", "as", "async", "await", "become", "box", "break", "const", "continue",
767        "crate", "do", "dyn", "else", "enum", "extern", "false", "final", "fn", "for", "gen", "if",
768        "impl", "in", "let", "loop", "macro", "match", "mod", "move", "mut", "override", "priv",
769        "pub", "ref", "return", "self", "static", "struct", "super", "trait", "true", "try",
770        "type", "typeof", "union", "unsafe", "unsized", "use", "virtual", "where", "while",
771        "yield",
772    ];
773    if RESERVED.contains(&name) {
774        format_ident!("{name}_")
775    } else {
776        Ident::new(name, Span::call_site())
777    }
778}
779
780#[cfg(test)]
781mod tests;