#![macro_use]
#![deny(unused)]
use std::collections::{HashMap, HashSet};
use proc_macro2::{Ident, TokenStream};
use quote::{format_ident, quote};
use syn::{
parse_quote, spanned::Spanned, ConstParam, GenericParam, Generics, Item, LifetimeParam, Path,
Result, Type, TypeArray, TypeParam, TypeParen, TypePath, TypeReference, TypeSlice, TypeTuple,
WhereClause, WherePredicate,
};
use crate::{deps::Dependencies, utils::format_generics};
#[macro_use]
mod utils;
mod attr;
mod deps;
mod types;
#[derive(Default, Clone)]
struct EnumDef {
pub variant_names: Vec<String>,
pub test_str: TokenStream,
pub num_variant_classes: usize,
}
struct DerivedPY {
crate_rename: Path,
py_name: String,
docs: String,
inline: TokenStream,
inline_flattened: Option<TokenStream>,
dependencies: Dependencies,
concrete: HashMap<Ident, Type>,
bound: Option<Vec<WherePredicate>>,
enum_def: Option<EnumDef>,
export: bool,
export_to: Option<String>,
}
impl DerivedPY {
fn into_impl(mut self, rust_ty: Ident, generics: Generics) -> TokenStream {
let export = self
.export
.then(|| self.generate_export_test(&rust_ty, &generics));
let output_path_fn = {
let path = match self.export_to.as_deref() {
Some(dirname) if dirname.ends_with('/') => {
format!("{}{}.py", dirname, self.py_name)
}
Some(filename) => filename.to_owned(),
None => format!("{}.py", self.py_name),
};
quote! {
fn output_path() -> Option<&'static std::path::Path> {
Some(std::path::Path::new(#path))
}
}
};
let docs = match &*self.docs {
"" => None,
docs => Some(quote!(const DOCS: Option<&'static str> = Some(#docs);)),
};
let crate_rename = self.crate_rename.clone();
let ident = self.py_name.clone();
let impl_start = generate_impl_block_header(
&crate_rename,
&rust_ty,
&generics,
self.bound.as_deref(),
&self.dependencies,
);
let assoc_type = generate_assoc_type(&rust_ty, &crate_rename, &generics, &self.concrete);
let name = self.generate_name_fn(&generics);
let inline = self.generate_inline_fn();
let decl = self.generate_decl_fn(&rust_ty, &generics);
let dependencies = &self.dependencies;
let generics_fn = self.generate_generics_fn(&generics);
let enum_decl = self.generate_variant_classes_decl();
quote! {
#impl_start {
#assoc_type
fn ident() -> String {
#ident.to_owned()
}
#enum_decl
#docs
#name
#decl
#inline
#generics_fn
#output_path_fn
fn visit_dependencies(v: &mut impl #crate_rename::TypeVisitor)
where
Self: 'static,
{
#dependencies
}
}
#export
}
}
fn name_with_generics(&self, generics: &Generics) -> TokenStream {
let name = &self.py_name;
let crate_rename = &self.crate_rename;
let mut generics_py_names = generics
.type_params()
.filter(|ty| !self.concrete.contains_key(&ty.ident))
.map(|ty| &ty.ident)
.map(|generic| quote!(<#generic as #crate_rename::PY>::name()))
.peekable();
if generics_py_names.peek().is_some() {
quote! {
format!("{}<{}>", #name, vec![#(#generics_py_names),*].join(", "))
}
} else {
quote!(#name.to_owned())
}
}
fn generate_generic_types(&self, generics: &Generics) -> TokenStream {
let crate_rename = &self.crate_rename;
let generics = generics
.type_params()
.filter(|ty| !self.concrete.contains_key(&ty.ident))
.map(|ty| ty.ident.clone());
let name = quote![<Self as #crate_rename::PY>::name()];
quote! {
#(
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
struct #generics;
impl std::fmt::Display for #generics {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
impl #crate_rename::PY for #generics {
type WithoutGenerics = #generics;
fn name() -> String { stringify!(#generics).to_owned() }
fn inline() -> String { panic!("{} cannot be inlined", #name) }
fn inline_flattened() -> String { stringify!(#generics).to_owned() }
fn decl() -> String { panic!("{} cannot be declared", #name) }
fn decl_concrete() -> String { panic!("{} cannot be declared", #name) }
fn variant_classes_decl() -> String {
panic!("{} cannot be declared", #name)
}
}
)*
}
}
fn generate_variant_classes_decl(&self) -> TokenStream {
if let Some(enum_def) = self.enum_def.clone() {
let name = &self.py_name;
let variant_text = enum_def
.variant_names
.iter()
.map(|i| format!("{} = \"{}\"", i, i))
.collect::<Vec<String>>()
.join("\n\t");
if enum_def.num_variant_classes > 0 {
let variant_classes = enum_def.test_str;
return quote! {
fn variant_classes_decl() -> String {
let variant_classes = #variant_classes;
let variants = format!("{}", #variant_text); let enum_str = format!("class {}Identifier(StrEnum):\n\t{variants}\n\n{variant_classes}", #name);
enum_str
}
};
}
quote! {
fn variant_classes_decl() -> String {
let variants = format!("{}", #variant_text); let enum_str = format!("class {}Identifier(StrEnum):\n\t{variants}\n", #name);
enum_str
}
}
} else {
return quote! {
fn variant_classes_decl() -> String {
String::new()
}
};
}
}
fn generate_export_test(&self, rust_ty: &Ident, generics: &Generics) -> TokenStream {
let test_fn = format_ident!(
"export_bindings_{}_py",
rust_ty.to_string().to_lowercase().replace("r#", "")
);
let crate_rename = &self.crate_rename;
let generic_params = generics
.type_params()
.map(|ty| match self.concrete.get(&ty.ident) {
None => quote! { #crate_rename::Dummy },
Some(ty) => quote! { #ty },
});
let ty = quote!(<#rust_ty<#(#generic_params),*> as #crate_rename::PY>);
quote! {
#[cfg(test)]
#[test]
fn #test_fn() {
#ty::export_all().expect("could not export type");
}
}
}
fn generate_generics_fn(&self, generics: &Generics) -> TokenStream {
let crate_rename = &self.crate_rename;
let generics = generics
.type_params()
.filter(|ty| !self.concrete.contains_key(&ty.ident))
.map(|TypeParam { ident, .. }| {
quote![
v.visit::<#ident>();
<#ident as #crate_rename::PY>::visit_generics(v);
]
});
quote! {
fn visit_generics(v: &mut impl #crate_rename::TypeVisitor)
where
Self: 'static,
{
#(#generics)*
}
}
}
fn generate_name_fn(&self, generics: &Generics) -> TokenStream {
let name = self.name_with_generics(generics);
quote! {
fn name() -> String {
#name
}
}
}
fn generate_inline_fn(&self) -> TokenStream {
let inline = &self.inline;
let crate_rename = &self.crate_rename;
let inline_flattened = self.inline_flattened.as_ref().map_or_else(
|| {
quote! {
fn inline_flattened() -> String {
panic!("{} cannot be flattened", <Self as #crate_rename::PY>::name())
}
}
},
|inline_flattened| {
quote! {
fn inline_flattened() -> String {
#inline_flattened
}
}
},
);
let inline = quote! {
fn inline() -> String {
#inline
}
};
quote! {
#inline
#inline_flattened
}
}
fn generate_decl_fn(&mut self, rust_ty: &Ident, generics: &Generics) -> TokenStream {
let name = &self.py_name;
let crate_rename = &self.crate_rename;
let generic_types = self.generate_generic_types(generics);
let py_generics = format_generics(
&mut self.dependencies,
crate_rename,
generics,
&self.concrete,
);
use GenericParam as G;
let generic_idents = generics.params.iter().filter_map(|p| match p {
G::Lifetime(_) => None,
G::Type(TypeParam { ident, .. }) => match self.concrete.get(ident) {
None => Some(quote!(#ident)),
Some(concrete) => Some(quote!(#concrete)),
},
G::Const(ConstParam { ident, .. }) => Some(quote!(#ident)),
});
if let Some(_) = self.enum_def.clone() {
quote! {
fn decl_concrete() -> String {
format!("{}\n\n{} = {}", <Self as #crate_rename::PY>::variant_classes_decl(), #name, <Self as #crate_rename::PY>::inline())
}
fn decl() -> String { #generic_types
let inline = <#rust_ty<#(#generic_idents,)*> as #crate_rename::PY>::inline();
let generics = #py_generics;
format!("{}\n\n{}{generics} = {inline}", <Self as #crate_rename::PY>::variant_classes_decl(), #name)
}
}
} else {
let docs = self.docs.clone();
quote! {
fn decl_concrete() -> String {
format!("\nclass {}(BaseModel):\n\t{}\n\t{}", #name, #docs, <Self as #crate_rename::PY>::inline())
}
fn decl() -> String { #generic_types
let inline = <#rust_ty<#(#generic_idents,)*> as #crate_rename::PY>::inline();
let generics = #py_generics;
format!("\nclass {}{generics}(BaseModel):\n{}\n\t{inline}", #name, #docs)
}
}
}
}
}
fn generate_assoc_type(
rust_ty: &Ident,
crate_rename: &Path,
generics: &Generics,
concrete: &HashMap<Ident, Type>,
) -> TokenStream {
use GenericParam as G;
let generics_params = generics.params.iter().map(|x| match x {
G::Type(ty) => match concrete.get(&ty.ident) {
None => quote! { #crate_rename::Dummy },
Some(ty) => quote! { #ty },
},
G::Const(ConstParam { ident, .. }) => quote! { #ident },
G::Lifetime(LifetimeParam { lifetime, .. }) => quote! { #lifetime },
});
quote! { type WithoutGenerics = #rust_ty<#(#generics_params),*>; }
}
fn generate_impl_block_header(
crate_rename: &Path,
ty: &Ident,
generics: &Generics,
bounds: Option<&[WherePredicate]>,
dependencies: &Dependencies,
) -> TokenStream {
use GenericParam as G;
let params = generics.params.iter().map(|param| match param {
G::Type(TypeParam {
ident,
colon_token,
bounds,
..
}) => quote!(#ident #colon_token #bounds),
G::Lifetime(LifetimeParam {
lifetime,
colon_token,
bounds,
..
}) => quote!(#lifetime #colon_token #bounds),
G::Const(ConstParam {
const_token,
ident,
colon_token,
ty,
..
}) => quote!(#const_token #ident #colon_token #ty),
});
let type_args = generics.params.iter().map(|param| match param {
G::Type(TypeParam { ident, .. }) | G::Const(ConstParam { ident, .. }) => quote!(#ident),
G::Lifetime(LifetimeParam { lifetime, .. }) => quote!(#lifetime),
});
let where_bound = match bounds {
Some(bounds) => quote! { where #(#bounds),* },
None => {
let bounds = generate_where_clause(crate_rename, generics, dependencies);
quote! { #bounds }
}
};
quote!(impl <#(#params),*> #crate_rename::PY for #ty <#(#type_args),*> #where_bound)
}
fn generate_where_clause(
crate_rename: &Path,
generics: &Generics,
dependencies: &Dependencies,
) -> WhereClause {
let used_types = {
let is_type_param = |id: &Ident| generics.type_params().any(|p| &p.ident == id);
let mut used_types = HashSet::new();
for ty in dependencies.used_types() {
used_type_params(&mut used_types, ty, is_type_param);
}
used_types.into_iter()
};
let existing = generics.where_clause.iter().flat_map(|w| &w.predicates);
parse_quote! {
where #(#existing,)* #(#used_types: #crate_rename::PY),*
}
}
fn used_type_params<'ty, 'out>(
out: &'out mut HashSet<&'ty Type>,
ty: &'ty Type,
is_type_param: impl Fn(&'ty Ident) -> bool + Copy + 'out,
) {
use syn::{
AngleBracketedGenericArguments as GenericArgs, GenericArgument as G, PathArguments as P,
};
match ty {
Type::Array(TypeArray { elem, .. })
| Type::Paren(TypeParen { elem, .. })
| Type::Reference(TypeReference { elem, .. })
| Type::Slice(TypeSlice { elem, .. }) => used_type_params(out, elem, is_type_param),
Type::Tuple(TypeTuple { elems, .. }) => elems
.iter()
.for_each(|elem| used_type_params(out, elem, is_type_param)),
Type::Path(TypePath { qself: None, path }) => {
let first = path.segments.first().unwrap();
if is_type_param(&first.ident) {
out.insert(ty);
return;
}
let last = path.segments.last().unwrap();
if let P::AngleBracketed(GenericArgs { ref args, .. }) = last.arguments {
for generic in args {
if let G::Type(ty) = generic {
used_type_params(out, ty, is_type_param);
}
}
}
}
_ => (),
}
}
#[proc_macro_derive(PY, attributes(py))]
pub fn python(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
match entry(input) {
Err(err) => err.to_compile_error(),
Ok(result) => result,
}
.into()
}
fn entry(input: proc_macro::TokenStream) -> Result<TokenStream> {
let input = syn::parse::<Item>(input)?;
let (py, ident, generics) = match input {
Item::Struct(s) => (types::struct_def(&s)?, s.ident, s.generics),
Item::Enum(e) => (types::enum_def(&e)?, e.ident, e.generics),
_ => syn_err!(input.span(); "unsupported item"),
};
Ok(py.into_impl(ident, generics))
}