1use std::{
5 collections::HashMap,
6 sync::{Mutex, OnceLock},
7};
8
9use proc_macro::TokenStream;
10use proc_macro2::TokenStream as TokenStream2;
11use quote::quote;
12use syn::{
13 Data, DeriveInput, Expr, Fields, GenericArgument, Lit, PathArguments, Type, parse_macro_input,
14};
15
16const DEFAULT_BCS_SCHEMA_FILE: &str = "bcs-schema.abnf";
17
18#[cfg(feature = "move-shape")]
19mod move_shape;
20
21fn defined_names() -> &'static Mutex<HashMap<String, String>> {
22 static NAMES: OnceLock<Mutex<HashMap<String, String>>> = OnceLock::new();
23 NAMES.get_or_init(|| Mutex::new(HashMap::new()))
24}
25
26#[proc_macro_derive(BcsSchema, attributes(bcs_schema))]
27pub fn derive_bcs_schema(input: TokenStream) -> TokenStream {
28 let input = parse_macro_input!(input as DeriveInput);
29 match expand(&input) {
30 Ok(ts) => ts.into(),
31 Err(e) => e.to_compile_error().into(),
32 }
33}
34
35#[cfg(feature = "move-shape")]
36#[proc_macro_derive(MoveShape)]
37pub fn derive_move_shape(input: TokenStream) -> TokenStream {
38 let input = parse_macro_input!(input as DeriveInput);
39 match move_shape::expand(&input) {
40 Ok(ts) => ts.into(),
41 Err(e) => e.to_compile_error().into(),
42 }
43}
44
45struct TypeAttrs {
50 name: Option<String>,
51 definition: Option<String>,
52}
53
54struct FieldAttrs {
55 skip: bool,
56 as_type: Option<String>,
57}
58
59struct VariantAttrs {
60 skip: bool,
61 as_type: Option<String>,
62}
63
64fn parse_type_attrs(input: &DeriveInput) -> syn::Result<TypeAttrs> {
65 let mut attrs = TypeAttrs {
66 name: None,
67 definition: None,
68 };
69 for attr in &input.attrs {
70 if !attr.path().is_ident("bcs_schema") {
71 continue;
72 }
73 attr.parse_nested_meta(|meta| {
74 if meta.path.is_ident("name") {
75 let value = meta.value()?;
76 let s: syn::LitStr = value.parse()?;
77 attrs.name = Some(s.value());
78 Ok(())
79 } else if meta.path.is_ident("definition") {
80 let value = meta.value()?;
81 let s: syn::LitStr = value.parse()?;
82 attrs.definition = Some(s.value());
83 Ok(())
84 } else {
85 Err(meta.error("expected `name` or `definition`"))
86 }
87 })?;
88 }
89 Ok(attrs)
90}
91
92fn parse_variant_attrs(variant: &syn::Variant) -> syn::Result<VariantAttrs> {
93 let mut attrs = VariantAttrs {
94 skip: false,
95 as_type: None,
96 };
97 for attr in &variant.attrs {
98 if !attr.path().is_ident("bcs_schema") {
99 continue;
100 }
101 attr.parse_nested_meta(|meta| {
102 if meta.path.is_ident("skip") {
103 attrs.skip = true;
104 Ok(())
105 } else if meta.path.is_ident("as_type") {
106 let value = meta.value()?;
107 let s: syn::LitStr = value.parse()?;
108 attrs.as_type = Some(s.value());
109 Ok(())
110 } else {
111 Err(meta.error("expected `skip` or `as_type`"))
112 }
113 })?;
114 }
115 Ok(attrs)
116}
117
118fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
119 let mut attrs = FieldAttrs {
120 skip: false,
121 as_type: None,
122 };
123 for attr in &field.attrs {
124 if !attr.path().is_ident("bcs_schema") {
125 continue;
126 }
127 attr.parse_nested_meta(|meta| {
128 if meta.path.is_ident("skip") {
129 attrs.skip = true;
130 Ok(())
131 } else if meta.path.is_ident("as_type") {
132 let value = meta.value()?;
133 let s: syn::LitStr = value.parse()?;
134 attrs.as_type = Some(s.value());
135 Ok(())
136 } else {
137 Err(meta.error("expected `skip` or `as_type`"))
138 }
139 })?;
140 }
141 Ok(attrs)
142}
143
144fn type_to_schema(ty: &Type) -> String {
149 match ty {
150 Type::Path(type_path) => {
151 let seg = match type_path.path.segments.last() {
152 Some(s) => s,
153 None => return "unknown".into(),
154 };
155 let name = seg.ident.to_string();
156 match name.as_str() {
157 "u8" | "u16" | "u32" | "u64" | "u128" | "i8" | "i16" | "i32" | "i64" | "i128"
158 | "bool" => name,
159 "str" | "String" => "string".into(),
160 "Vec" => match extract_single_generic(seg) {
161 Some(inner) if matches_type_name(&inner, "u8") => "bytes".into(),
162 Some(inner) => {
164 format!("(size {})", wrap_for_repetition(&type_to_schema(&inner)))
165 }
166 None => "(size *unknown)".into(),
167 },
168 "Option" => match extract_single_generic(seg) {
169 Some(inner) => {
171 let inner_str = type_to_schema(&inner);
172 let rhs = if (inner_str.contains(' ') && !inner_str.starts_with('('))
174 || inner_str.starts_with('*')
175 || inner_str.starts_with('[')
176 {
177 format!("({inner_str})")
178 } else {
179 inner_str
180 };
181 format!("(%d00 / %d01 {rhs})")
182 }
183 None => "(%d00 / %d01 unknown)".into(),
184 },
185 "Box" => match extract_single_generic(seg) {
186 Some(inner) => type_to_schema(&inner),
187 None => "unknown".into(),
188 },
189 "BTreeMap" | "HashMap" => match extract_two_generics(seg) {
190 Some((k, v)) => {
192 format!("(size *({} {}))", type_to_schema(&k), type_to_schema(&v))
193 }
194 None => "(size *(unknown unknown))".into(),
195 },
196 "BTreeSet" => match extract_single_generic(seg) {
200 Some(inner) => {
201 format!("(size {})", wrap_for_repetition(&type_to_schema(&inner)))
202 }
203 None => "(size *unknown)".into(),
204 },
205 other => to_kebab_case(other),
206 }
207 }
208 Type::Array(arr) => {
209 let elem = type_to_schema(&arr.elem);
210 if elem == "u8" {
211 if let Expr::Lit(expr_lit) = &arr.len
212 && let Lit::Int(lit_int) = &expr_lit.lit
213 {
214 return format!("{}OCTET", lit_int.base10_digits());
215 }
216 "*OCTET".into()
218 } else if let Expr::Lit(expr_lit) = &arr.len
219 && let Lit::Int(lit_int) = &expr_lit.lit
220 {
221 let n = lit_int.base10_digits();
223 if elem.starts_with('(') || (!elem.contains(' ') && !elem.starts_with('*')) {
224 format!("{n}{elem}")
225 } else {
226 format!("{n}({elem})")
227 }
228 } else {
229 wrap_for_repetition(&elem)
230 }
231 }
232 Type::Tuple(tuple) if tuple.elems.is_empty() => "unit".into(),
233 Type::Tuple(tuple) => {
234 let elems: Vec<String> = tuple.elems.iter().map(type_to_schema).collect();
235 format!("({})", elems.join(" "))
236 }
237 _ => "unknown".into(),
238 }
239}
240
241fn extract_single_generic(seg: &syn::PathSegment) -> Option<Type> {
242 if let PathArguments::AngleBracketed(args) = &seg.arguments
243 && let Some(GenericArgument::Type(ty)) = args.args.first()
244 {
245 return Some(ty.clone());
246 }
247 None
248}
249
250fn extract_two_generics(seg: &syn::PathSegment) -> Option<(Type, Type)> {
251 if let PathArguments::AngleBracketed(args) = &seg.arguments {
252 let mut iter = args.args.iter();
253 if let (Some(GenericArgument::Type(k)), Some(GenericArgument::Type(v))) =
254 (iter.next(), iter.next())
255 {
256 return Some((k.clone(), v.clone()));
257 }
258 }
259 None
260}
261
262fn matches_type_name(ty: &Type, name: &str) -> bool {
263 if let Type::Path(p) = ty
264 && let Some(seg) = p.path.segments.last()
265 {
266 return seg.ident == name;
267 }
268 false
269}
270
271fn wrap_for_repetition(s: &str) -> String {
282 if s.starts_with('(') || s.starts_with('[') || (!s.contains(' ') && !s.starts_with('*')) {
283 format!("*{s}")
284 } else {
285 format!("*({s})")
286 }
287}
288
289fn to_kebab_case(s: &str) -> String {
294 let mut result = String::with_capacity(s.len() + 4);
295 let chars: Vec<char> = s.chars().collect();
296 for (i, &ch) in chars.iter().enumerate() {
297 if ch == '_' {
298 result.push('-');
302 continue;
303 }
304 if ch.is_uppercase() {
305 if i > 0 {
306 let prev = chars[i - 1];
307 let prev_upper = prev.is_uppercase();
308 let next_lower = i + 1 < chars.len() && chars[i + 1].is_lowercase();
309 if prev != '_' && (!prev_upper || next_lower) {
312 result.push('-');
313 }
314 }
315 for lower in ch.to_lowercase() {
316 result.push(lower);
317 }
318 } else {
319 result.push(ch);
320 }
321 }
322 result
323}
324
325fn gen_struct(schema_name: &str, data: &syn::DataStruct) -> syn::Result<String> {
330 match &data.fields {
331 Fields::Named(fields) => {
332 let mut parts: Vec<(String, String)> = Vec::new(); for field in &fields.named {
334 let fa = parse_field_attrs(field)?;
335 if fa.skip {
336 continue;
337 }
338 let type_str = fa.as_type.unwrap_or_else(|| type_to_schema(&field.ty));
339 let name = field.ident.as_ref().unwrap().to_string().replace('_', "-");
340 parts.push((type_str, name));
341 }
342
343 if parts.is_empty() {
344 return Ok(format!("{schema_name} = unit"));
345 }
346 if parts.len() == 1 {
347 let (ty, nm) = &parts[0];
348 return Ok(format!("{schema_name} = {ty} ; {nm}"));
349 }
350
351 let max_type_len = parts.iter().map(|(t, _)| t.len()).max().unwrap_or(0);
352 let indent = " ".repeat(schema_name.len() + 3); let lines: Vec<String> = parts
354 .iter()
355 .enumerate()
356 .map(|(i, (ty, name))| {
357 let pad = " ".repeat(max_type_len - ty.len());
358 if i == 0 {
359 format!("{schema_name} = {ty}{pad} ; {name}")
360 } else {
361 format!("{indent}{ty}{pad} ; {name}")
362 }
363 })
364 .collect();
365 Ok(lines.join("\n"))
366 }
367 Fields::Unnamed(fields) => {
368 if fields.unnamed.len() == 1 {
369 let field = &fields.unnamed[0];
370 let fa = parse_field_attrs(field)?;
371 let type_str = fa.as_type.unwrap_or_else(|| type_to_schema(&field.ty));
372 Ok(format!("{schema_name} = {type_str}"))
373 } else {
374 let mut types = Vec::new();
375 for field in &fields.unnamed {
376 let fa = parse_field_attrs(field)?;
377 types.push(fa.as_type.unwrap_or_else(|| type_to_schema(&field.ty)));
378 }
379 Ok(format!("{schema_name} = {}", types.join(" ")))
380 }
381 }
382 Fields::Unit => Ok(format!("{schema_name} = unit")),
383 }
384}
385
386fn gen_enum(schema_name: &str, data: &syn::DataEnum) -> syn::Result<String> {
391 let indent = " ".repeat(schema_name.len() + 1);
392 let mut rows: Vec<(String, String, String)> = Vec::new(); for (idx, variant) in data.variants.iter().enumerate() {
395 let va = parse_variant_attrs(variant)?;
396 if va.skip {
401 continue;
402 }
403 let variant_name = variant.ident.to_string();
404 let prefix = format!("%d{idx:02}");
405
406 let fields_str = match &variant.fields {
407 Fields::Unit => {
408 match &va.as_type {
412 Some(t) => format!(" {t}"),
413 None => String::new(),
414 }
415 }
416 Fields::Unnamed(fields) => {
417 let mut types = Vec::new();
418 for f in &fields.unnamed {
419 let fa = parse_field_attrs(f)?;
420 types.push(fa.as_type.unwrap_or_else(|| type_to_schema(&f.ty)));
421 }
422 format!(" {}", types.join(" "))
423 }
424 Fields::Named(fields) => {
425 let mut types = Vec::new();
426 for f in &fields.named {
427 let fa = parse_field_attrs(f)?;
428 if fa.skip {
429 continue;
430 }
431 types.push(fa.as_type.unwrap_or_else(|| type_to_schema(&f.ty)));
432 }
433 if types.is_empty() {
434 String::new()
435 } else {
436 format!(" {}", types.join(" "))
437 }
438 }
439 };
440
441 rows.push((prefix, fields_str, variant_name));
442 }
443
444 let max_body_len = rows
445 .iter()
446 .map(|(p, f, _)| p.len() + f.len())
447 .max()
448 .unwrap_or(0);
449
450 let lines: Vec<String> = rows
451 .iter()
452 .enumerate()
453 .map(|(idx, (prefix, fields_str, variant_name))| {
454 let pad = " ".repeat(max_body_len - prefix.len() - fields_str.len());
455 if idx == 0 {
456 format!("{schema_name} = {prefix}{fields_str}{pad} ; {variant_name}")
457 } else {
458 format!("{indent}/ {prefix}{fields_str}{pad} ; {variant_name}")
459 }
460 })
461 .collect();
462
463 Ok(lines.join("\n"))
464}
465
466fn schema_file_path() -> std::path::PathBuf {
471 if let Ok(p) = std::env::var("BCS_SCHEMA_FILE") {
472 return std::path::PathBuf::from(p);
473 }
474 let manifest = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".into());
475 std::path::PathBuf::from(manifest).join(DEFAULT_BCS_SCHEMA_FILE)
476}
477
478fn primitive_entries() -> &'static [(&'static str, &'static str)] {
484 &[
485 ("bool", "bool = %d00 ; false\n / %d01 ; true"),
486 ("bytes", "bytes = size *OCTET"),
487 ("i64", "i64 = 8OCTET"),
488 (
489 "size",
490 "size = uleb128 ; BCS sequence/string length (ULEB128-encoded)",
491 ),
492 ("string", "string = size *OCTET ; UTF-8 encoded"),
493 ("u8", "u8 = 1OCTET"),
494 ("u16", "u16 = 2OCTET"),
495 ("u32", "u32 = 4OCTET"),
496 ("u64", "u64 = 8OCTET"),
497 ("u128", "u128 = 16OCTET"),
498 (
499 "uleb128",
500 "uleb128 = *(%x80-FF) %x00-7F ; variable-length unsigned integer",
501 ),
502 ]
503}
504
505fn write_schema_entry(schema_name: &str, definition: &str) {
506 let path = schema_file_path();
507 let content = std::fs::read_to_string(&path).unwrap_or_default();
508
509 let mut entries: Vec<(String, String)> = Vec::new();
511 for block in content.split("\n\n") {
512 let trimmed = block.trim();
513 if trimmed.is_empty() {
514 continue;
515 }
516 if !trimmed.contains('=') {
518 continue;
519 }
520 let rule_name = if let Some(idx) = trimmed.find(" =") {
522 trimmed[..idx].trim().to_string()
523 } else if let Some(idx) = trimmed.find('=') {
524 trimmed[..idx].trim().to_string()
525 } else {
526 continue;
527 };
528 entries.push((rule_name, trimmed.to_string()));
529 }
530
531 let mut found = false;
533 for entry in &mut entries {
534 if entry.0 == schema_name {
535 entry.1 = definition.to_string();
536 found = true;
537 break;
538 }
539 }
540 if !found {
541 entries.push((schema_name.to_string(), definition.to_string()));
542 }
543
544 for (name, def) in primitive_entries() {
546 if !entries.iter().any(|(n, _)| n == name) {
547 entries.push((name.to_string(), def.to_string()));
548 }
549 }
550
551 entries.sort_by(|(a, _), (b, _)| a.cmp(b));
553
554 let mut output =
556 String::from("; Auto-generated BCS schema definitions\n; Do not edit manually\n");
557 for (_, def) in &entries {
558 output.push('\n');
559 output.push_str(def);
560 output.push('\n');
561 }
562
563 let _ = std::fs::write(&path, output);
565}
566
567fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
572 let type_attrs = parse_type_attrs(input)?;
573 let ident = &input.ident;
574 let schema_name = type_attrs
575 .name
576 .unwrap_or_else(|| to_kebab_case(&ident.to_string()));
577
578 {
579 let mut names = defined_names().lock().unwrap();
580 let type_name = ident.to_string();
581 if let Some(existing) = names.get(&schema_name) {
582 if existing != &type_name {
583 return Err(syn::Error::new_spanned(
584 ident,
585 format!(
586 "BcsSchema: duplicate schema name `{schema_name}` (already used by `{existing}`)"
587 ),
588 ));
589 }
590 } else {
591 names.insert(schema_name.clone(), type_name);
592 }
593 }
594
595 let definition = match type_attrs.definition {
596 Some(def) => format!("{schema_name} = {def}"),
597 None => match &input.data {
598 Data::Struct(data) => gen_struct(&schema_name, data)?,
599 Data::Enum(data) => gen_enum(&schema_name, data)?,
600 Data::Union(_) => {
601 return Err(syn::Error::new_spanned(
602 ident,
603 "BcsSchema cannot be derived for unions",
604 ));
605 }
606 },
607 };
608
609 let bcs_schema_enabled = std::env::var("BCS_SCHEMA").is_ok_and(|v| !v.is_empty() && v != "0");
613 if bcs_schema_enabled {
614 write_schema_entry(&schema_name, &definition);
615 }
616
617 Ok(quote! {})
618}
619
620#[cfg(test)]
625mod tests {
626 use super::*;
627
628 #[test]
629 fn kebab_case() {
630 assert_eq!(to_kebab_case("Address"), "address");
631 assert_eq!(to_kebab_case("ObjectId"), "object-id");
632 assert_eq!(to_kebab_case("GasCostSummary"), "gas-cost-summary");
633 assert_eq!(to_kebab_case("TransactionV1"), "transaction-v1");
634 assert_eq!(to_kebab_case("ObjectID"), "object-id");
635 assert_eq!(to_kebab_case("BTreeMap"), "b-tree-map");
636 assert_eq!(
639 to_kebab_case("STARDUST_UPGRADE_LABEL"),
640 "stardust-upgrade-label"
641 );
642 assert_eq!(to_kebab_case("UQ32_32"), "uq32-32");
643 assert_eq!(to_kebab_case("UQ64_64"), "uq64-64");
644 }
645
646 fn enum_schema(source: &str) -> String {
647 let input: DeriveInput = syn::parse_str(source).unwrap();
648 let name = to_kebab_case(&input.ident.to_string());
649 let Data::Enum(data) = &input.data else {
650 panic!("expected an enum");
651 };
652 gen_enum(&name, data).unwrap()
653 }
654
655 #[test]
656 fn skip_variant_holds_its_discriminant() {
657 let schema = enum_schema(
660 r#"
661 enum Scheme {
662 Ed25519(Ed25519Signature),
663 #[bcs_schema(skip)]
664 Bls12381Reserved,
665 Passkey(PasskeyAuthenticator),
666 }
667 "#,
668 );
669 assert_eq!(
672 schema,
673 "scheme = %d00 ed25519-signature ; Ed25519\n\
674 \x20 / %d02 passkey-authenticator ; Passkey"
675 );
676 }
677}