1#![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#[derive(Clone, Debug, Eq, PartialEq)]
20pub struct CodegenError {
21 message: String,
22}
23
24impl CodegenError {
25 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
41pub 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 pub const fn tokens(&self) -> &TokenStream {
52 &self.tokens
53 }
54
55 pub const fn root_ident(&self) -> &Ident {
57 &self.root_ident
58 }
59
60 pub fn type_idents(&self) -> &[Ident] {
62 &self.type_idents
63 }
64
65 pub const fn has_nested_named_types(&self) -> bool {
67 self.has_nested_named_types
68 }
69}
70
71#[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 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 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
118pub 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
185fn 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
226fn 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 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 #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
395fn 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 let derives = if felt_repr_support.supports_native_felt_repr(definition) {
408 let miden_field_repr = &runtime.miden_field_repr;
409 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 pub const WIT_FQN: &'static str = #fqn;
454 }
455
456 #encode_impl
457 #decode_impl
458 })
459}
460
461fn 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
520fn 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
627fn 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
662fn 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#[derive(Default)]
679struct FeltReprSupport {
680 answers: HashMap<*const SchemaType, bool>,
681}
682
683impl FeltReprSupport {
684 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 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
718fn 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
727fn 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
736fn 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
745pub fn generated_type_ident(name: &str) -> String {
750 type_ident(name).to_string()
751}
752
753fn type_ident(name: &str) -> Ident {
755 rust_ident(&name.to_upper_camel_case())
756}
757
758fn value_ident(name: &str) -> Ident {
760 rust_ident(&name.replace('-', "_").to_snake_case())
761}
762
763fn 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;