Skip to main content

ic_wire_derive/
lib.rs

1//! Explicit-layout derives for IC Wire contracts.
2use proc_macro::TokenStream;
3use quote::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
39fn expand(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
40    if input
41        .generics
42        .params
43        .iter()
44        .any(|p| !matches!(p, GenericParam::Lifetime(_)))
45        || input.generics.where_clause.is_some()
46    {
47        return Err(syn::Error::new_spanned(
48            input.generics,
49            "Wire supports lifetime parameters only; type/const parameters and where clauses are unsupported",
50        ));
51    }
52    if input.attrs.iter().any(|a| a.path().is_ident("wire")) {
53        return Err(syn::Error::new_spanned(
54            &input.ident,
55            "wire ids belong on fields or variants",
56        ));
57    }
58    let generics = input.generics;
59    let mut decode_name = "__ic_wire_de".to_owned();
60    while generics
61        .lifetimes()
62        .any(|p| p.lifetime.ident == decode_name)
63    {
64        decode_name.push('_');
65    }
66    let decode_lifetime = Lifetime::new(&format!("'{decode_name}"), proc_macro2::Span::call_site());
67    let mut decode_generics = generics.clone();
68    decode_generics.params.insert(
69        0,
70        GenericParam::Lifetime(LifetimeParam::new(decode_lifetime.clone())),
71    );
72    for lifetime in generics.lifetimes().map(|p| &p.lifetime) {
73        decode_generics
74            .make_where_clause()
75            .predicates
76            .push(parse_quote!(#decode_lifetime: #lifetime));
77    }
78    let (impl_generics, type_generics, where_clause) = generics.split_for_impl();
79    let (decode_impl_generics, _, decode_where_clause) = decode_generics.split_for_impl();
80    let name = input.ident;
81    let mut ids = BTreeSet::new();
82    let (serialize, deserialize, shape, schema) = match input.data {
83        Data::Struct(data) => {
84            let Fields::Named(fields) = data.fields else {
85                return Err(syn::Error::new_spanned(
86                    name,
87                    "use a named-field record (empty records are allowed)",
88                ));
89            };
90            if fields.named.len() > 16 {
91                return Err(syn::Error::new_spanned(
92                    name,
93                    "at most 16 fields; group related fields in a nested record",
94                ));
95            }
96            let mut fields = fields
97                .named
98                .into_iter()
99                .map(|field| {
100                    let id = identity(&field.attrs)?;
101                    if !ids.insert(id) {
102                        return Err(syn::Error::new_spanned(&field, "duplicate wire id"));
103                    }
104                    Ok((id, field.ident.unwrap(), field.ty))
105                })
106                .collect::<syn::Result<Vec<_>>>()?;
107            fields.sort_by_key(|f| f.0);
108            let names: Vec<_> = fields.iter().map(|f| &f.1).collect();
109            let types: Vec<_> = fields.iter().map(|f| &f.2).collect();
110            let labels: Vec<_> = names
111                .iter()
112                .map(|name| name.to_string().trim_start_matches("r#").to_owned())
113                .collect();
114            let field_ids: Vec<_> = fields.iter().map(|f| f.0).collect();
115            (
116                quote! { ::ic_wire::__private::serde::Serialize::serialize(&(#(&self.#names,)*), serializer) },
117                quote! {
118                    let (#(#names,)*): (#(#types,)*) = ::ic_wire::__private::serde::Deserialize::deserialize(deserializer)?;
119                    Ok(Self { #(#names,)* })
120                },
121                quote! { ::ic_wire::Shape::record(&[#(<#types as ::ic_wire::Wire>::SHAPE,)*]) },
122                quote! { ::ic_wire::schema::Schema::Record(&[#(::ic_wire::schema::Field { name: #labels, id: #field_ids, value: &<#types as ::ic_wire::Wire>::SCHEMA },)*]) },
123            )
124        }
125        Data::Enum(data) => {
126            enum_expansion::expand(&name, &generics, &decode_generics, &decode_lifetime, data)?
127        }
128        Data::Union(_) => return Err(syn::Error::new_spanned(name, "unions are unsupported")),
129    };
130    Ok(quote! {
131        impl #impl_generics ::ic_wire::__private::serde::Serialize for #name #type_generics #where_clause {
132            fn serialize<S: ::ic_wire::__private::serde::Serializer>(&self, serializer: S) -> ::core::result::Result<S::Ok, S::Error> { #serialize }
133        }
134        impl #decode_impl_generics ::ic_wire::__private::serde::Deserialize<#decode_lifetime> for #name #type_generics #decode_where_clause {
135            fn deserialize<D: ::ic_wire::__private::serde::Deserializer<#decode_lifetime>>(deserializer: D) -> ::core::result::Result<Self, D::Error> { #deserialize }
136        }
137        impl #impl_generics ::ic_wire::Wire for #name #type_generics #where_clause {
138            const SHAPE: ::ic_wire::Shape = #shape;
139            const SCHEMA: ::ic_wire::schema::Schema = #schema;
140        }
141    })
142}