1#![deny(rustdoc::broken_intra_doc_links, rustdoc::private_intra_doc_links)]
14
15use proc_macro::TokenStream;
16use proc_macro_error2::{abort, proc_macro_error};
17use quote::quote;
18
19mod model;
20
21use model::{StructModel, VariantShape};
22
23#[proc_macro_error]
43#[proc_macro_derive(Ix, attributes(ix))]
44pub fn derive_ix(input: TokenStream) -> TokenStream {
45 let input = syn::parse_macro_input!(input as syn::DeriveInput);
46 let model = StructModel::analyze(&input);
47 expand(&model).into()
48}
49
50fn expand(model: &StructModel) -> proc_macro2::TokenStream {
52 let ident = &model.ident;
53 let version = model.version;
54 let repr = &model.repr;
55 let type_doc = &model.doc;
56 let (impl_generics, ty_generics, where_clause) = model.generics.split_for_impl();
57
58 let field_specs = model.fields.iter().map(|f| {
59 let name = &f.member;
60 let ty = &f.ty;
61 let since = f.since;
62 let doc = &f.doc;
63 quote! {
64 ::ix_schema::FieldSpec {
65 name: ::core::stringify!(#name),
66 doc: #doc,
67 type_name: ::core::stringify!(#ty),
68 offset: ::core::mem::offset_of!(Self, #name),
69 size: ::core::mem::size_of::<#ty>(),
70 align: ::core::mem::align_of::<#ty>(),
71 since: #since,
72 }
73 }
74 });
75
76 let enum_is_fieldless = !model.variants.is_empty()
79 && model
80 .variants
81 .iter()
82 .all(|v| matches!(v.kind, VariantShape::Unit));
83
84 let variant_specs = model.variants.iter().map(|v| {
85 let vname = &v.ident;
86 let vdoc = &v.doc;
87 let kind = match v.kind {
88 VariantShape::Unit => quote!(::ix_schema::VariantKind::Unit),
89 VariantShape::Tuple => quote!(::ix_schema::VariantKind::Tuple),
90 VariantShape::Struct => quote!(::ix_schema::VariantKind::Struct),
91 };
92 let discriminant = if enum_is_fieldless {
95 quote!(::core::option::Option::Some(Self::#vname as i64))
96 } else {
97 quote!(::core::option::Option::None)
98 };
99 let variant_fields = v.fields.iter().map(|f| {
102 let ty = &f.ty;
103 let name = match &f.name {
104 Some(id) => quote!(::core::option::Option::Some(::core::stringify!(#id))),
105 None => quote!(::core::option::Option::None),
106 };
107 quote! {
108 ::ix_schema::VariantFieldSpec {
109 name: #name,
110 type_name: ::core::stringify!(#ty),
111 size: ::core::mem::size_of::<#ty>(),
112 align: ::core::mem::align_of::<#ty>(),
113 }
114 }
115 });
116 quote! {
117 ::ix_schema::VariantSpec {
118 name: ::core::stringify!(#vname),
119 doc: #vdoc,
120 discriminant: #discriminant,
121 kind: #kind,
122 fields: &[ #(#variant_fields),* ],
123 }
124 }
125 });
126
127 let (evolution_expr, migration_impl) = match &model.migrate_from {
128 None => (quote!(::ix_schema::EvolutionSpec::GENESIS), quote!()),
129 Some(prev_ty) => {
130 let changes = model.fields.iter().filter_map(field_change);
131 let removed = model
132 .removed
133 .iter()
134 .map(|name| quote!(::ix_schema::FieldChange::Removed { name: #name }));
135 let inits = model.fields.iter().map(field_init);
136 let evolution = quote! {
137 ::ix_schema::EvolutionSpec {
138 migrates_from: ::core::option::Option::Some(
139 <#prev_ty as ::ix_schema::Ix>::MANIFEST.schema_version
140 ),
141 changes: &[ #(#changes,)* #(#removed),* ],
142 }
143 };
144 let migration = quote! {
145 const _: () = ::core::assert!(
147 <#prev_ty as ::ix_schema::Ix>::MANIFEST.schema_version < #version,
148 "ix-schema: `migrate_from` target must be an older schema version",
149 );
150
151 impl #impl_generics ::ix_schema::MigrateFrom<#prev_ty> for #ident #ty_generics
152 #where_clause {
153 fn migrate_from(prev: #prev_ty) -> Self {
154 Self { #(#inits),* }
155 }
156 }
157 };
158 (evolution, migration)
159 }
160 };
161
162 let ix_impl = quote! {
163 impl #impl_generics ::ix_schema::Ix for #ident #ty_generics #where_clause {
164 const MANIFEST: ::ix_schema::Manifest<'static> = ::ix_schema::Manifest {
165 type_name: ::core::concat!(::core::module_path!(), "::", ::core::stringify!(#ident)),
166 doc: #type_doc,
167 schema_version: #version,
168 layout: ::ix_schema::LayoutSpec {
169 size: ::core::mem::size_of::<Self>(),
170 align: ::core::mem::align_of::<Self>(),
171 repr: #repr,
172 },
173 fields: &[ #(#field_specs),* ],
174 variants: &[ #(#variant_specs),* ],
175 evolution: #evolution_expr,
176 };
177 }
178 };
179
180 quote! {
181 #ix_impl
182 #migration_impl
183 }
184}
185
186fn field_init(field: &model::FieldModel) -> proc_macro2::TokenStream {
192 let name = &field.member;
193 if let Some(default) = &field.default {
194 quote!(#name: #default)
195 } else if let Some(with) = &field.with {
196 quote!(#name: #with(prev.#name))
197 } else if let Some(old) = &field.rename_from {
198 let old = syn::Ident::new(old, field.span);
199 quote!(#name: prev.#old)
200 } else {
201 quote!(#name: prev.#name)
202 }
203}
204
205fn field_change(field: &model::FieldModel) -> Option<proc_macro2::TokenStream> {
207 let name = &field.member;
208 if field.with.is_some() {
209 Some(quote!(::ix_schema::FieldChange::Transformed {
210 name: ::core::stringify!(#name)
211 }))
212 } else if let Some(old) = &field.rename_from {
213 Some(
214 quote!(::ix_schema::FieldChange::Renamed { from: #old, to: ::core::stringify!(#name) }),
215 )
216 } else if field.default.is_some() {
217 Some(quote!(::ix_schema::FieldChange::Added {
218 name: ::core::stringify!(#name)
219 }))
220 } else {
221 None
222 }
223}
224
225fn reject_duplicate(seen: bool, meta: &syn::meta::ParseNestedMeta) -> syn::Result<()> {
227 if seen {
228 return Err(meta.error("duplicate `ix` attribute key"));
229 }
230 Ok(())
231}
232
233fn skip_optional_parens(meta: &syn::meta::ParseNestedMeta) -> syn::Result<()> {
235 if meta.input.peek(syn::token::Paren) {
236 let content;
237 syn::parenthesized!(content in meta.input);
238 let _: proc_macro2::TokenStream = content.parse()?;
239 }
240 Ok(())
241}
242
243fn parse_repr(attrs: &[syn::Attribute]) -> proc_macro2::TokenStream {
245 use proc_macro_error2::ResultExt as _;
246
247 let mut repr = quote!(::ix_schema::Repr::Rust);
248 for attr in attrs {
249 if !attr.path().is_ident("repr") {
250 continue;
251 }
252 attr.parse_nested_meta(|meta| {
253 if meta.path.is_ident("C") {
254 repr = quote!(::ix_schema::Repr::C);
255 } else if meta.path.is_ident("transparent") {
256 repr = quote!(::ix_schema::Repr::Transparent);
257 } else if meta.path.is_ident("packed") {
258 if meta.input.peek(syn::token::Paren) {
259 let content;
260 syn::parenthesized!(content in meta.input);
261 let n: syn::LitInt = content.parse()?;
262 let n: usize = n.base10_parse()?;
263 repr = quote!(::ix_schema::Repr::Packed(#n));
264 } else {
265 repr = quote!(::ix_schema::Repr::Packed(1));
266 }
267 } else {
268 skip_optional_parens(&meta)?;
271 }
272 Ok(())
273 })
274 .unwrap_or_abort();
275 }
276 repr
277}
278
279fn abort_unsupported(input: &syn::DeriveInput) -> ! {
282 match &input.data {
283 syn::Data::Union(_) => abort!(
284 input.ident,
285 "`#[derive(Ix)]` supports structs and enums, not unions";
286 help = "a union's fields overlap in memory; model it as a struct with a tag field"
287 ),
288 _ => abort!(input.ident, "`#[derive(Ix)]` could not model this type"),
291 }
292}