Skip to main content

ic_wire_derive/
lib.rs

1//! Explicit-layout derives for IC Wire contracts.
2use proc_macro::TokenStream;
3use quote::{ToTokens, format_ident, quote};
4use std::collections::BTreeSet;
5use syn::{
6    Data, DeriveInput, Fields, GenericParam, Lifetime, LifetimeParam, parse_macro_input,
7    parse_quote,
8};
9
10mod enum_expansion;
11
12/// Derive a bounded record or tagged enum. Fields/variants require `#[wire(id = N)]`.
13#[proc_macro_derive(Wire, attributes(wire))]
14pub fn derive_wire(input: TokenStream) -> TokenStream {
15    expand(parse_macro_input!(input as DeriveInput))
16        .unwrap_or_else(syn::Error::into_compile_error)
17        .into()
18}
19
20fn identity(attrs: &[syn::Attribute]) -> syn::Result<u32> {
21    let mut id = None;
22    for attr in attrs.iter().filter(|a| a.path().is_ident("wire")) {
23        attr.parse_nested_meta(|meta| {
24            if !meta.path.is_ident("id") || id.is_some() {
25                return Err(meta.error("expected one wire id"));
26            }
27            id = Some(meta.value()?.parse::<syn::LitInt>()?.base10_parse()?);
28            Ok(())
29        })?;
30    }
31    id.ok_or_else(|| {
32        syn::Error::new(
33            proc_macro2::Span::call_site(),
34            "explicit #[wire(id = N)] required",
35        )
36    })
37}
38
39// Generated local types and generics must not shadow names in the contract.
40fn fresh_ident(source: &str, base: &str) -> syn::Ident {
41    let mut name = base.to_owned();
42    while source.contains(&name) {
43        name.push('_');
44    }
45    format_ident!("{name}")
46}
47
48fn expand(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
49    if input
50        .generics
51        .params
52        .iter()
53        .any(|p| !matches!(p, GenericParam::Lifetime(_)))
54        || input.generics.where_clause.is_some()
55    {
56        return Err(syn::Error::new_spanned(
57            input.generics,
58            "Wire supports lifetime parameters only; type/const parameters and where clauses are unsupported",
59        ));
60    }
61    if input.attrs.iter().any(|a| a.path().is_ident("wire")) {
62        return Err(syn::Error::new_spanned(
63            &input.ident,
64            "wire ids belong on fields or variants",
65        ));
66    }
67    let source = input.to_token_stream().to_string();
68    let serializer = fresh_ident(&source, "S");
69    let deserializer = fresh_ident(&source, "D");
70    let serializer_binding = fresh_ident(&source, "__ic_wire_serializer");
71    let deserializer_binding = fresh_ident(&source, "__ic_wire_deserializer");
72    let fields_binding = fresh_ident(&source, "__ic_wire_fields");
73    let generics = input.generics;
74    let mut decode_name = "__ic_wire_de".to_owned();
75    while generics
76        .lifetimes()
77        .any(|p| p.lifetime.ident == decode_name)
78    {
79        decode_name.push('_');
80    }
81    let decode_lifetime = Lifetime::new(&format!("'{decode_name}"), proc_macro2::Span::call_site());
82    let mut decode_generics = generics.clone();
83    decode_generics.params.insert(
84        0,
85        GenericParam::Lifetime(LifetimeParam::new(decode_lifetime.clone())),
86    );
87    for lifetime in generics.lifetimes().map(|p| &p.lifetime) {
88        decode_generics
89            .make_where_clause()
90            .predicates
91            .push(parse_quote!(#decode_lifetime: #lifetime));
92    }
93    let (impl_generics, type_generics, where_clause) = generics.split_for_impl();
94    let (decode_impl_generics, _, decode_where_clause) = decode_generics.split_for_impl();
95    let name = input.ident;
96    let mut ids = BTreeSet::new();
97    let (serialize, deserialize, shape, schema) = match input.data {
98        Data::Struct(data) => {
99            let Fields::Named(fields) = data.fields else {
100                return Err(syn::Error::new_spanned(
101                    name,
102                    "use a named-field record (empty records are allowed)",
103                ));
104            };
105            if fields.named.len() > 16 {
106                return Err(syn::Error::new_spanned(
107                    name,
108                    "at most 16 fields; group related fields in a nested record",
109                ));
110            }
111            let mut fields = fields
112                .named
113                .into_iter()
114                .map(|field| {
115                    let id = identity(&field.attrs)?;
116                    if !ids.insert(id) {
117                        return Err(syn::Error::new_spanned(&field, "duplicate wire id"));
118                    }
119                    Ok((id, field.ident.unwrap(), field.ty))
120                })
121                .collect::<syn::Result<Vec<_>>>()?;
122            fields.sort_by_key(|f| f.0);
123            let names: Vec<_> = fields.iter().map(|f| &f.1).collect();
124            let types: Vec<_> = fields.iter().map(|f| &f.2).collect();
125            let labels: Vec<_> = names
126                .iter()
127                .map(|name| name.to_string().trim_start_matches("r#").to_owned())
128                .collect();
129            let indices: Vec<_> = (0..fields.len()).map(syn::Index::from).collect();
130            let field_ids: Vec<_> = fields.iter().map(|f| f.0).collect();
131            (
132                quote! { ::ic_wire::__private::serde::Serialize::serialize(&(#(&self.#names,)*), #serializer_binding) },
133                quote! {
134                    let #fields_binding: (#(#types,)*) = ::ic_wire::__private::serde::Deserialize::deserialize(#deserializer_binding)?;
135                    ::core::result::Result::Ok(Self { #(#names: #fields_binding.#indices,)* })
136                },
137                quote! { ::ic_wire::Shape::record(&[#(<#types as ::ic_wire::Wire>::SHAPE,)*]) },
138                quote! { ::ic_wire::schema::Schema::Record(&[#(::ic_wire::schema::Field { name: #labels, id: #field_ids, value: &<#types as ::ic_wire::Wire>::SCHEMA },)*]) },
139            )
140        }
141        Data::Enum(data) => enum_expansion::expand(
142            &name,
143            &generics,
144            &decode_generics,
145            &decode_lifetime,
146            data,
147            &source,
148            (&serializer_binding, &deserializer_binding),
149        )?,
150        Data::Union(_) => return Err(syn::Error::new_spanned(name, "unions are unsupported")),
151    };
152    Ok(quote! {
153        impl #impl_generics ::ic_wire::__private::serde::Serialize for #name #type_generics #where_clause {
154            fn serialize<#serializer: ::ic_wire::__private::serde::Serializer>(&self, #serializer_binding: #serializer) -> ::core::result::Result<<#serializer as ::ic_wire::__private::serde::Serializer>::Ok, <#serializer as ::ic_wire::__private::serde::Serializer>::Error> { #serialize }
155        }
156        impl #decode_impl_generics ::ic_wire::__private::serde::Deserialize<#decode_lifetime> for #name #type_generics #decode_where_clause {
157            fn deserialize<#deserializer: ::ic_wire::__private::serde::Deserializer<#decode_lifetime>>(#deserializer_binding: #deserializer) -> ::core::result::Result<Self, <#deserializer as ::ic_wire::__private::serde::Deserializer<#decode_lifetime>>::Error> { #deserialize }
158        }
159        impl #impl_generics ::ic_wire::Wire for #name #type_generics #where_clause {
160            const SHAPE: ::ic_wire::Shape = #shape;
161            const SCHEMA: ::ic_wire::schema::Schema = #schema;
162        }
163    })
164}