#![deny(rustdoc::broken_intra_doc_links, rustdoc::private_intra_doc_links)]
use proc_macro::TokenStream;
use proc_macro_error2::{abort, proc_macro_error};
use quote::quote;
mod model;
use model::{StructModel, VariantShape};
#[proc_macro_error]
#[proc_macro_derive(Ix, attributes(ix))]
pub fn derive_ix(input: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(input as syn::DeriveInput);
let model = StructModel::analyze(&input);
expand(&model).into()
}
fn expand(model: &StructModel) -> proc_macro2::TokenStream {
let ident = &model.ident;
let version = model.version;
let repr = &model.repr;
let (impl_generics, ty_generics, where_clause) = model.generics.split_for_impl();
let field_specs = model.fields.iter().map(|f| {
let name = &f.ident;
let ty = &f.ty;
let since = f.since;
quote! {
::ix_schema::FieldSpec {
name: ::core::stringify!(#name),
type_name: ::core::stringify!(#ty),
offset: ::core::mem::offset_of!(Self, #name),
size: ::core::mem::size_of::<#ty>(),
align: ::core::mem::align_of::<#ty>(),
since: #since,
}
}
});
let enum_is_fieldless = !model.variants.is_empty()
&& model
.variants
.iter()
.all(|v| matches!(v.kind, VariantShape::Unit));
let variant_specs = model.variants.iter().map(|v| {
let vname = &v.ident;
let kind = match v.kind {
VariantShape::Unit => quote!(::ix_schema::VariantKind::Unit),
VariantShape::Tuple => quote!(::ix_schema::VariantKind::Tuple),
VariantShape::Struct => quote!(::ix_schema::VariantKind::Struct),
};
let discriminant = if enum_is_fieldless {
quote!(::core::option::Option::Some(Self::#vname as i64))
} else {
quote!(::core::option::Option::None)
};
let variant_fields = v.fields.iter().map(|f| {
let ty = &f.ty;
let name = match &f.name {
Some(id) => quote!(::core::option::Option::Some(::core::stringify!(#id))),
None => quote!(::core::option::Option::None),
};
quote! {
::ix_schema::VariantFieldSpec {
name: #name,
type_name: ::core::stringify!(#ty),
size: ::core::mem::size_of::<#ty>(),
align: ::core::mem::align_of::<#ty>(),
}
}
});
quote! {
::ix_schema::VariantSpec {
name: ::core::stringify!(#vname),
discriminant: #discriminant,
kind: #kind,
fields: &[ #(#variant_fields),* ],
}
}
});
let (evolution_expr, migration_impl) = match &model.migrate_from {
None => (quote!(::ix_schema::EvolutionSpec::GENESIS), quote!()),
Some(prev_ty) => {
let changes = model.fields.iter().filter_map(field_change);
let removed = model
.removed
.iter()
.map(|name| quote!(::ix_schema::FieldChange::Removed { name: #name }));
let inits = model.fields.iter().map(field_init);
let evolution = quote! {
::ix_schema::EvolutionSpec {
migrates_from: ::core::option::Option::Some(
<#prev_ty as ::ix_schema::Ix>::MANIFEST.schema_version
),
changes: &[ #(#changes,)* #(#removed),* ],
}
};
let migration = quote! {
const _: () = ::core::assert!(
<#prev_ty as ::ix_schema::Ix>::MANIFEST.schema_version < #version,
"ix-schema: `migrate_from` target must be an older schema version",
);
impl #impl_generics ::ix_schema::MigrateFrom<#prev_ty> for #ident #ty_generics
#where_clause {
fn migrate_from(prev: #prev_ty) -> Self {
Self { #(#inits),* }
}
}
};
(evolution, migration)
}
};
let ix_impl = quote! {
impl #impl_generics ::ix_schema::Ix for #ident #ty_generics #where_clause {
const MANIFEST: ::ix_schema::Manifest<'static> = ::ix_schema::Manifest {
type_name: ::core::concat!(::core::module_path!(), "::", ::core::stringify!(#ident)),
schema_version: #version,
layout: ::ix_schema::LayoutSpec {
size: ::core::mem::size_of::<Self>(),
align: ::core::mem::align_of::<Self>(),
repr: #repr,
},
fields: &[ #(#field_specs),* ],
variants: &[ #(#variant_specs),* ],
evolution: #evolution_expr,
};
}
};
quote! {
#ix_impl
#migration_impl
}
}
fn field_init(field: &model::FieldModel) -> proc_macro2::TokenStream {
let name = &field.ident;
if let Some(default) = &field.default {
quote!(#name: #default)
} else if let Some(with) = &field.with {
quote!(#name: #with(prev.#name))
} else if let Some(old) = &field.rename_from {
let old = syn::Ident::new(old, field.span);
quote!(#name: prev.#old)
} else {
quote!(#name: prev.#name)
}
}
fn field_change(field: &model::FieldModel) -> Option<proc_macro2::TokenStream> {
let name = &field.ident;
if field.with.is_some() {
Some(quote!(::ix_schema::FieldChange::Transformed {
name: ::core::stringify!(#name)
}))
} else if let Some(old) = &field.rename_from {
Some(
quote!(::ix_schema::FieldChange::Renamed { from: #old, to: ::core::stringify!(#name) }),
)
} else if field.default.is_some() {
Some(quote!(::ix_schema::FieldChange::Added {
name: ::core::stringify!(#name)
}))
} else {
None
}
}
fn reject_duplicate(seen: bool, meta: &syn::meta::ParseNestedMeta) -> syn::Result<()> {
if seen {
return Err(meta.error("duplicate `ix` attribute key"));
}
Ok(())
}
fn skip_optional_parens(meta: &syn::meta::ParseNestedMeta) -> syn::Result<()> {
if meta.input.peek(syn::token::Paren) {
let content;
syn::parenthesized!(content in meta.input);
let _: proc_macro2::TokenStream = content.parse()?;
}
Ok(())
}
fn parse_repr(attrs: &[syn::Attribute]) -> proc_macro2::TokenStream {
use proc_macro_error2::ResultExt as _;
let mut repr = quote!(::ix_schema::Repr::Rust);
for attr in attrs {
if !attr.path().is_ident("repr") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("C") {
repr = quote!(::ix_schema::Repr::C);
} else if meta.path.is_ident("transparent") {
repr = quote!(::ix_schema::Repr::Transparent);
} else if meta.path.is_ident("packed") {
if meta.input.peek(syn::token::Paren) {
let content;
syn::parenthesized!(content in meta.input);
let n: syn::LitInt = content.parse()?;
let n: usize = n.base10_parse()?;
repr = quote!(::ix_schema::Repr::Packed(#n));
} else {
repr = quote!(::ix_schema::Repr::Packed(1));
}
} else {
skip_optional_parens(&meta)?;
}
Ok(())
})
.unwrap_or_abort();
}
repr
}
fn abort_unsupported(input: &syn::DeriveInput) -> ! {
match &input.data {
syn::Data::Union(_) => abort!(
input.ident,
"`#[derive(Ix)]` supports structs and fieldless enums, not unions";
help = "model the union as a struct with a tag field"
),
syn::Data::Struct(_) => abort!(
input.ident,
"`#[derive(Ix)]` requires a struct with named fields";
help = "tuple and unit structs have no field names to put in the manifest"
),
syn::Data::Enum(_) => abort!(input.ident, "`#[derive(Ix)]` could not model this enum"),
}
}