use proc_macro::TokenStream;
use quote::quote;
use syn::{
parse::Parse, parse_macro_input, punctuated::Punctuated, Data, DeriveInput, Expr, Fields,
LitStr, Token, Type,
};
#[proc_macro_derive(Project)]
pub fn derive_project(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
match &input.data {
Data::Struct(data_struct) => match &data_struct.fields {
Fields::Named(fields_named) => derive_project_struct(&input, fields_named),
Fields::Unnamed(fields_unnamed) => derive_project_tuple_struct(&input, fields_unnamed),
Fields::Unit => derive_project_unit_struct(&input),
},
Data::Enum(_) => {
syn::Error::new_spanned(input, "Project derive macro does not support enums")
.to_compile_error()
.into()
}
Data::Union(_) => {
syn::Error::new_spanned(input, "Project derive macro does not support unions")
.to_compile_error()
.into()
}
}
}
fn derive_project_struct(input: &DeriveInput, fields: &syn::FieldsNamed) -> TokenStream {
let struct_name = &input.ident;
let (_impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let projected_struct_name =
syn::Ident::new(&format!("{struct_name}Projected"), struct_name.span());
let projected_fields = fields.named.iter().map(|field| {
let field_name = &field.ident;
let field_type = &field.ty;
quote! {
pub #field_name: ::nami::Binding<#field_type>
}
});
let field_projections = fields.named.iter().map(|field| {
let field_name = &field.ident;
quote! {
#field_name: {
let source = source.clone();
::nami::Binding::mapping(
&source,
|value| value.#field_name.clone(),
move |binding, value| {
binding.with_mut(|b| {
b.#field_name = value;
});
},
)
}
}
});
let mut generics_with_static = input.generics.clone();
for param in &mut generics_with_static.params {
if let syn::GenericParam::Type(type_param) = param {
type_param.bounds.push(syn::parse_quote!('static));
}
}
let (impl_generics_with_static, _, _) = generics_with_static.split_for_impl();
let expanded = quote! {
#[derive(Debug)]
pub struct #projected_struct_name #ty_generics #where_clause {
#(#projected_fields,)*
}
impl #impl_generics_with_static ::nami::project::Project for #struct_name #ty_generics #where_clause {
type Projected = #projected_struct_name #ty_generics;
fn project(source: &::nami::Binding<Self>) -> Self::Projected {
#projected_struct_name {
#(#field_projections,)*
}
}
}
};
TokenStream::from(expanded)
}
fn derive_project_tuple_struct(input: &DeriveInput, fields: &syn::FieldsUnnamed) -> TokenStream {
let struct_name = &input.ident;
let (_impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let field_types: Vec<&Type> = fields.unnamed.iter().map(|field| &field.ty).collect();
let projected_tuple = if field_types.len() == 1 {
quote! { (::nami::Binding<#(#field_types)*>,) }
} else {
quote! { (#(::nami::Binding<#field_types>),*) }
};
let field_projections = fields.unnamed.iter().enumerate().map(|(index, _)| {
let idx = syn::Index::from(index);
quote! {
{
let source = source.clone();
::nami::Binding::mapping(
&source,
|value| value.#idx.clone(),
move |binding, value| {
binding.with_mut(|b| {
b.#idx = value;
});
},
)
}
}
});
let mut generics_with_static = input.generics.clone();
for param in &mut generics_with_static.params {
if let syn::GenericParam::Type(type_param) = param {
type_param.bounds.push(syn::parse_quote!('static));
}
}
let (impl_generics_with_static, _, _) = generics_with_static.split_for_impl();
let projection_tuple = if field_projections.len() == 1 {
quote! { (#(#field_projections)*,) }
} else {
quote! { (#(#field_projections),*) }
};
let expanded = quote! {
impl #impl_generics_with_static ::nami::project::Project for #struct_name #ty_generics #where_clause {
type Projected = #projected_tuple;
fn project(source: &::nami::Binding<Self>) -> Self::Projected {
#projection_tuple
}
}
};
TokenStream::from(expanded)
}
fn derive_project_unit_struct(input: &DeriveInput) -> TokenStream {
let struct_name = &input.ident;
let (_impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let mut generics_with_static = input.generics.clone();
for param in &mut generics_with_static.params {
if let syn::GenericParam::Type(type_param) = param {
type_param.bounds.push(syn::parse_quote!('static));
}
}
let (impl_generics_with_static, _, _) = generics_with_static.split_for_impl();
let expanded = quote! {
impl #impl_generics_with_static ::nami::project::Project for #struct_name #ty_generics #where_clause {
type Projected = ();
fn project(_source: &::nami::Binding<Self>) -> Self::Projected {
()
}
}
};
TokenStream::from(expanded)
}
struct SInput {
format_str: LitStr,
args: Punctuated<Expr, Token![,]>,
}
impl Parse for SInput {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let format_str: LitStr = input.parse()?;
let args = if input.peek(Token![,]) {
input.parse::<Token![,]>()?;
Punctuated::parse_terminated(input)?
} else {
Punctuated::new()
};
Ok(Self { format_str, args })
}
}
#[proc_macro]
#[allow(clippy::similar_names)] pub fn s(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as SInput);
let format_str = input.format_str;
let format_value = format_str.value();
let (has_positional, has_named, positional_count, named_vars) =
analyze_format_string(&format_value);
if !input.args.is_empty() {
if has_named {
return syn::Error::new_spanned(
&format_str,
format!(
"Format string contains named arguments like {{{}}} but you provided positional arguments. \
Either use positional placeholders like {{}} or remove the explicit arguments to use automatic variable capture.",
named_vars.first().unwrap_or(&String::new())
)
)
.to_compile_error()
.into();
}
if positional_count != input.args.len() {
return syn::Error::new_spanned(
&format_str,
format!(
"Format string has {} positional placeholder(s) but {} arguments were provided",
positional_count,
input.args.len()
),
)
.to_compile_error()
.into();
}
let args: Vec<_> = input.args.iter().collect();
return handle_s_args(&format_str, &args);
}
if has_positional && has_named {
return syn::Error::new_spanned(
&format_str,
"Format string mixes positional {{}} and named {{var}} placeholders. \
Use either all positional with explicit arguments, or all named for automatic capture.",
)
.to_compile_error()
.into();
}
if has_positional && input.args.is_empty() {
return syn::Error::new_spanned(
&format_str,
format!(
"Format string has {positional_count} positional placeholder(s) {{}} but no arguments provided. \
Either provide arguments or use named placeholders like {{variable}} for automatic capture."
)
)
.to_compile_error()
.into();
}
let var_names = named_vars;
if var_names.is_empty() {
return quote! {
{
use ::nami::constant;
constant(nami::__format!(#format_str))
}
}
.into();
}
let var_idents: Vec<syn::Ident> = var_names
.iter()
.map(|name| syn::Ident::new(name, format_str.span()))
.collect();
handle_s_named_vars(&format_str, &var_idents)
}
#[allow(clippy::similar_names)]
fn handle_s_args(format_str: &LitStr, args: &[&Expr]) -> TokenStream {
match args.len() {
1 => {
let arg = &args[0];
(quote! {
{
use ::nami::SignalExt;
(#arg).map(|arg| nami::__format!(#format_str, arg))
}
})
.into()
}
2 => {
let arg1 = &args[0];
let arg2 = &args[1];
(quote! {
{
use nami::{SignalExt, zip::zip};
zip(#arg1.clone(), #arg2.clone()).map(|(arg1, arg2)| {
nami::__format!(#format_str, arg1, arg2)
})
}
})
.into()
}
3 => {
let arg1 = &args[0];
let arg2 = &args[1];
let arg3 = &args[2];
(quote! {
{
use ::nami::{SignalExt, zip::zip};
zip(zip(#arg1.clone(), #arg2.clone()), #arg3.clone()).map(
|((arg1, arg2), arg3)| nami::__format!(#format_str, arg1, arg2, arg3)
)
}
})
.into()
}
4 => {
let arg1 = &args[0];
let arg2 = &args[1];
let arg3 = &args[2];
let arg4 = &args[3];
(quote! {
{
use ::nami::{SignalExt, zip::zip};
zip(
zip(#arg1.clone(), #arg2.clone()),
zip(#arg3.clone(), #arg4.clone())
).map(
|((arg1, arg2), (arg3, arg4))| nami::__format!(#format_str, arg1, arg2, arg3, arg4)
)
}
}).into()
}
_ => syn::Error::new_spanned(format_str, "Too many arguments, maximum 4 supported")
.to_compile_error()
.into(),
}
}
#[allow(clippy::similar_names)]
fn handle_s_named_vars(format_str: &LitStr, var_idents: &[syn::Ident]) -> TokenStream {
match var_idents.len() {
1 => {
let var = &var_idents[0];
(quote! {
{
use ::nami::SignalExt;
(#var).map(|#var| {
nami::__format!(#format_str)
})
}
})
.into()
}
2 => {
let var1 = &var_idents[0];
let var2 = &var_idents[1];
(quote! {
{
use ::nami::{SignalExt, zip::zip};
zip(#var1.clone(), #var2.clone()).map(|(#var1, #var2)| {
nami::__format!(#format_str)
})
}
})
.into()
}
3 => {
let var1 = &var_idents[0];
let var2 = &var_idents[1];
let var3 = &var_idents[2];
(quote! {
{
use ::nami::{SignalExt, zip::zip};
zip(zip(#var1.clone(), #var2.clone()), #var3.clone()).map(
|((#var1, #var2), #var3)| {
::nami::__format!(#format_str)
}
)
}
})
.into()
}
4 => {
let var1 = &var_idents[0];
let var2 = &var_idents[1];
let var3 = &var_idents[2];
let var4 = &var_idents[3];
(quote! {
{
use ::nami::{SignalExt, zip::zip};
zip(
zip(#var1.clone(), #var2.clone()),
zip(#var3.clone(), #var4.clone())
).map(
|((#var1, #var2), (#var3, #var4))| {
::nami::__format!(#format_str)
}
)
}
})
.into()
}
_ => syn::Error::new_spanned(format_str, "Too many named variables, maximum 4 supported")
.to_compile_error()
.into(),
}
}
fn analyze_format_string(format_str: &str) -> (bool, bool, usize, Vec<String>) {
let mut has_positional = false;
let mut has_named = false;
let mut positional_count = 0;
let mut named_vars = Vec::new();
let mut chars = format_str.chars().peekable();
while let Some(c) = chars.next() {
if c == '{' {
if chars.peek() == Some(&'{') {
chars.next();
continue;
}
let mut content = String::new();
let mut has_content = false;
while let Some(&next_char) = chars.peek() {
if next_char == '}' {
chars.next(); break;
} else if next_char == ':' {
chars.next(); while let Some(&spec_char) = chars.peek() {
if spec_char == '}' {
chars.next(); break;
}
chars.next();
}
break;
}
content.push(chars.next().unwrap());
has_content = true;
}
if !has_content || content.is_empty() {
has_positional = true;
positional_count += 1;
} else if content.chars().all(|ch| ch.is_ascii_digit()) {
has_positional = true;
positional_count += 1;
} else if content
.chars()
.next()
.is_some_and(|ch| ch.is_ascii_alphabetic() || ch == '_')
{
has_named = true;
if !named_vars.contains(&content) {
named_vars.push(content);
}
} else {
has_positional = true;
positional_count += 1;
}
}
}
(has_positional, has_named, positional_count, named_vars)
}