extern crate proc_macro;
use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::{format_ident, quote};
use syn::{parse_macro_input, Data, DeriveInput, Expr, Fields, Lit, Meta, Type};
mod declaration;
#[proc_macro]
pub fn declaration(input: TokenStream) -> TokenStream {
declaration::expand(input.into()).into()
}
#[derive(Clone, Copy)]
enum FieldKind {
Lens,
Prism,
Traversal,
Iso,
}
struct AnnotatedField {
ident: syn::Ident,
ty: Type,
kind: FieldKind,
}
#[proc_macro_derive(Prism, attributes(oid, prism, lens, traversal, iso))]
pub fn derive_prism(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let oid_name = extract_oid_name(&input, "Prism");
let annotated = extract_annotated_fields(&input);
validate_annotations(&annotated);
let accessor_structs = generate_accessors(name, &annotated);
let optic_fields_impl =
generate_optic_fields(name, &annotated, &impl_generics, &ty_generics, where_clause);
let expanded = quote! {
impl #impl_generics prismqueer::Addressable for #name #ty_generics #where_clause {
fn oid(&self) -> prismqueer::Oid {
prismqueer::Oid::hash(#oid_name.as_bytes())
}
}
impl #impl_generics ::std::fmt::Display for #name #ty_generics #where_clause {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
write!(f, "{}", #oid_name)
}
}
#accessor_structs
#optic_fields_impl
};
TokenStream::from(expanded)
}
#[proc_macro_derive(Lambda, attributes(oid))]
pub fn derive_lambda(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let oid_name = extract_oid_name(&input, "Lambda");
let expanded = quote! {
impl #impl_generics prismqueer::Addressable for #name #ty_generics #where_clause {
fn oid(&self) -> prismqueer::Oid {
prismqueer::Oid::hash(#oid_name.as_bytes())
}
}
impl #impl_generics ::std::fmt::Display for #name #ty_generics #where_clause {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
write!(f, "{}", #oid_name)
}
}
impl<__LambdaT: Clone + PartialEq> ::std::convert::From<#name> for prismqueer::lambda::Lambda<__LambdaT> {
fn from(_: #name) -> prismqueer::lambda::Lambda<__LambdaT> {
let oid = prismqueer::Oid::hash(#oid_name.as_bytes());
prismqueer::lambda::Lambda::abs(oid.clone(), prismqueer::lambda::Lambda::bind(oid))
}
}
impl<__LambdaT: Clone + PartialEq> prismqueer::lambda::Composable<__LambdaT> for #name #ty_generics #where_clause {}
};
TokenStream::from(expanded)
}
fn extract_oid_name(input: &DeriveInput, derive_name: &str) -> String {
for attr in &input.attrs {
if attr.path().is_ident("oid") {
if let Meta::List(meta_list) = &attr.meta {
let tokens = meta_list.tokens.clone();
if let Ok(expr) = syn::parse2::<Expr>(tokens) {
if let Expr::Lit(lit) = expr {
if let Lit::Str(s) = &lit.lit {
let val = s.value();
if !val.starts_with('@') {
panic!("#[oid(\"...\")] value must start with '@', got: {:?}", val);
}
return val;
}
}
}
}
}
}
panic!(
"#[derive({})] requires #[oid(\"@name\")] attribute",
derive_name
);
}
fn extract_annotated_fields(input: &DeriveInput) -> Vec<AnnotatedField> {
let fields = match &input.data {
Data::Struct(data) => match &data.fields {
Fields::Named(named) => &named.named,
_ => return Vec::new(),
},
_ => return Vec::new(),
};
let mut annotated = Vec::new();
for field in fields {
let ident = match &field.ident {
Some(id) => id.clone(),
None => continue,
};
let mut kind = None;
for attr in &field.attrs {
if attr.path().is_ident("lens") {
kind = Some(FieldKind::Lens);
} else if attr.path().is_ident("prism") {
match &attr.meta {
Meta::Path(_) => {
kind = Some(FieldKind::Prism);
}
Meta::List(_) => {
}
_ => {}
}
} else if attr.path().is_ident("traversal") {
kind = Some(FieldKind::Traversal);
} else if attr.path().is_ident("iso") {
kind = Some(FieldKind::Iso);
}
}
if let Some(k) = kind {
annotated.push(AnnotatedField {
ident,
ty: field.ty.clone(),
kind: k,
});
}
}
annotated
}
fn validate_annotations(fields: &[AnnotatedField]) {
for field in fields {
match field.kind {
FieldKind::Lens => {
if is_option_type(&field.ty) {
panic!(
"#[lens] on field `{}`: Lens is total, but Option<T> is partial. Use #[prism] instead.",
field.ident
);
}
}
_ => {}
}
}
}
fn is_option_type(ty: &Type) -> bool {
if let Type::Path(type_path) = ty {
if let Some(segment) = type_path.path.segments.last() {
return segment.ident == "Option";
}
}
false
}
fn is_vec_type(ty: &Type) -> bool {
if let Type::Path(type_path) = ty {
if let Some(segment) = type_path.path.segments.last() {
return segment.ident == "Vec";
}
}
false
}
fn extract_inner_type(ty: &Type) -> Option<&Type> {
if let Type::Path(type_path) = ty {
if let Some(segment) = type_path.path.segments.last() {
if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
if let Some(syn::GenericArgument::Type(inner)) = args.args.first() {
return Some(inner);
}
}
}
}
None
}
fn to_pascal_case(s: &str) -> String {
s.split('_')
.filter(|part| !part.is_empty())
.map(|part| {
let mut chars = part.chars();
match chars.next() {
Some(c) => c.to_uppercase().to_string() + chars.as_str(),
None => String::new(),
}
})
.collect()
}
fn generate_accessors(
struct_name: &syn::Ident,
fields: &[AnnotatedField],
) -> proc_macro2::TokenStream {
let mut tokens = proc_macro2::TokenStream::new();
for field in fields {
let field_ident = &field.ident;
let field_name = field.ident.to_string();
let pascal = to_pascal_case(&field_name);
match field.kind {
FieldKind::Lens => {
let accessor_name = format_ident!("{}Lens", pascal, span = Span::call_site());
let field_ty = &field.ty;
tokens.extend(quote! {
pub struct #accessor_name;
impl #accessor_name {
pub fn view(source: &#struct_name) -> &#field_ty {
&source.#field_ident
}
pub fn set(source: &mut #struct_name, value: #field_ty) {
source.#field_ident = value;
}
}
impl prismqueer::Named<#accessor_name> {
pub fn lens() -> prismqueer::Named<#accessor_name> {
prismqueer::Named(#field_name, #accessor_name)
}
}
});
}
FieldKind::Prism => {
let accessor_name = format_ident!("{}Prism", pascal, span = Span::call_site());
let inner_ty = extract_inner_type(&field.ty);
if let Some(inner) = inner_ty {
if is_option_type(&field.ty) {
tokens.extend(quote! {
pub struct #accessor_name;
impl #accessor_name {
pub fn extract(source: &#struct_name) -> Option<&#inner> {
source.#field_ident.as_ref()
}
pub fn review(source: &mut #struct_name, value: #inner) {
source.#field_ident = Some(value);
}
}
impl prismqueer::Named<#accessor_name> {
pub fn prism() -> prismqueer::Named<#accessor_name> {
prismqueer::Named(#field_name, #accessor_name)
}
}
});
} else {
let field_ty = &field.ty;
tokens.extend(quote! {
pub struct #accessor_name;
impl #accessor_name {
pub fn extract(source: &#struct_name) -> Option<&#field_ty> {
Some(&source.#field_ident)
}
pub fn review(source: &mut #struct_name, value: #field_ty) {
source.#field_ident = value;
}
}
});
}
} else {
let field_ty = &field.ty;
tokens.extend(quote! {
pub struct #accessor_name;
impl #accessor_name {
pub fn extract(source: &#struct_name) -> Option<&#field_ty> {
Some(&source.#field_ident)
}
pub fn review(source: &mut #struct_name, value: #field_ty) {
source.#field_ident = value;
}
}
});
}
}
FieldKind::Traversal => {
let accessor_name = format_ident!("{}Traversal", pascal, span = Span::call_site());
let field_ty = &field.ty;
if is_vec_type(&field.ty) {
if let Some(inner) = extract_inner_type(&field.ty) {
tokens.extend(quote! {
pub struct #accessor_name;
impl #accessor_name {
pub fn traverse(source: &#struct_name) -> &[#inner] {
&source.#field_ident
}
pub fn traverse_mut(source: &mut #struct_name) -> &mut Vec<#inner> {
&mut source.#field_ident
}
}
impl prismqueer::Named<#accessor_name> {
pub fn traversal() -> prismqueer::Named<#accessor_name> {
prismqueer::Named(#field_name, #accessor_name)
}
}
});
}
} else {
tokens.extend(quote! {
pub struct #accessor_name;
impl #accessor_name {
pub fn traverse(source: &#struct_name) -> &#field_ty {
&source.#field_ident
}
pub fn traverse_mut(source: &mut #struct_name) -> &mut #field_ty {
&mut source.#field_ident
}
}
});
}
}
FieldKind::Iso => {
let accessor_name = format_ident!("{}Iso", pascal, span = Span::call_site());
let field_ty = &field.ty;
tokens.extend(quote! {
pub struct #accessor_name;
impl #accessor_name {
pub fn forward(source: &#struct_name) -> &#field_ty {
&source.#field_ident
}
pub fn backward(source: &mut #struct_name, value: #field_ty) {
source.#field_ident = value;
}
}
});
}
}
}
tokens
}
fn generate_optic_fields(
struct_name: &syn::Ident,
fields: &[AnnotatedField],
impl_generics: &syn::ImplGenerics,
ty_generics: &syn::TypeGenerics,
where_clause: Option<&syn::WhereClause>,
) -> proc_macro2::TokenStream {
if fields.is_empty() {
return quote! {};
}
let entries: Vec<proc_macro2::TokenStream> = fields
.iter()
.map(|f| {
let field_name = f.ident.to_string();
let kind = match f.kind {
FieldKind::Iso => quote! { prismqueer::OpticKind::Iso },
FieldKind::Lens => quote! { prismqueer::OpticKind::Lens },
FieldKind::Prism => quote! { prismqueer::OpticKind::Prism },
FieldKind::Traversal => quote! { prismqueer::OpticKind::Traversal },
};
quote! {
prismqueer::FieldOptic {
name: #field_name,
kind: #kind,
}
}
})
.collect();
let count = entries.len();
quote! {
impl #impl_generics #struct_name #ty_generics #where_clause {
pub fn optic_fields() -> &'static [prismqueer::FieldOptic] {
static FIELDS: [prismqueer::FieldOptic; #count] = [
#(#entries),*
];
&FIELDS
}
}
}
}