nami_derive/
lib.rs

1//! This crate provides the derive macro for the `nami` crate.
2//! It includes the `Project` derive macro and the `s!` procedural macro.
3
4use proc_macro::TokenStream;
5use quote::quote;
6use syn::{
7    parse::Parse, parse_macro_input, punctuated::Punctuated, Data, DeriveInput, Expr, Fields,
8    LitStr, Token, Type,
9};
10
11/// Derive macro for implementing the `Project` trait on structs.
12///
13/// This macro automatically generates a `Project` implementation that allows
14/// decomposing a struct binding into separate bindings for each field.
15///
16/// # Examples
17///
18/// ```rust,ignore
19/// use nami::{Binding, binding};
20/// use nami_derive::Project;
21///
22/// #[derive(Project,Clone,Debug)]
23/// struct Person {
24///     name: String,
25///     age: u32,
26/// }
27///
28/// let person_binding: Binding<Person> = binding(Person {
29///     name: "Alice".to_string(),
30///     age: 30,
31/// });
32///
33/// let mut projected = person_binding.project();
34/// projected.name.set_from("Bob");
35/// projected.age.set(25);
36///
37/// let person = person_binding.get();
38/// assert_eq!(person.name, "Bob");
39/// assert_eq!(person.age, 25);
40/// ```
41#[proc_macro_derive(Project)]
42pub fn derive_project(input: TokenStream) -> TokenStream {
43    let input = parse_macro_input!(input as DeriveInput);
44
45    match &input.data {
46        Data::Struct(data_struct) => match &data_struct.fields {
47            Fields::Named(fields_named) => derive_project_struct(&input, fields_named),
48            Fields::Unnamed(fields_unnamed) => derive_project_tuple_struct(&input, fields_unnamed),
49            Fields::Unit => derive_project_unit_struct(&input),
50        },
51        Data::Enum(_) => {
52            syn::Error::new_spanned(input, "Project derive macro does not support enums")
53                .to_compile_error()
54                .into()
55        }
56        Data::Union(_) => {
57            syn::Error::new_spanned(input, "Project derive macro does not support unions")
58                .to_compile_error()
59                .into()
60        }
61    }
62}
63
64fn derive_project_struct(input: &DeriveInput, fields: &syn::FieldsNamed) -> TokenStream {
65    let struct_name = &input.ident;
66    let (_impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
67
68    // Create the projected struct type
69    let projected_struct_name =
70        syn::Ident::new(&format!("{struct_name}Projected"), struct_name.span());
71
72    // Generate fields for the projected struct
73    let projected_fields = fields.named.iter().map(|field| {
74        let field_name = &field.ident;
75        let field_type = &field.ty;
76        quote! {
77            pub #field_name: ::nami::Binding<#field_type>
78        }
79    });
80
81    // Generate the projection logic
82    let field_projections = fields.named.iter().map(|field| {
83        let field_name = &field.ident;
84        quote! {
85            #field_name: {
86                let source = source.clone();
87                ::nami::Binding::mapping(
88                    &source,
89                    |value| value.#field_name.clone(),
90                    move |binding, value| {
91                        binding.with_mut(|b| {
92                            b.#field_name = value;
93                        });
94                    },
95                )
96            }
97        }
98    });
99
100    // Add lifetime bounds to generic parameters
101    let mut generics_with_static = input.generics.clone();
102    for param in &mut generics_with_static.params {
103        if let syn::GenericParam::Type(type_param) = param {
104            type_param.bounds.push(syn::parse_quote!('static));
105        }
106    }
107    let (impl_generics_with_static, _, _) = generics_with_static.split_for_impl();
108
109    let expanded = quote! {
110        /// Projected version of #struct_name with each field wrapped in a Binding.
111        #[derive(Debug)]
112        pub struct #projected_struct_name #ty_generics #where_clause {
113            #(#projected_fields,)*
114        }
115
116        impl #impl_generics_with_static ::nami::project::Project for #struct_name #ty_generics #where_clause {
117            type Projected = #projected_struct_name #ty_generics;
118
119            fn project(source: &::nami::Binding<Self>) -> Self::Projected {
120                #projected_struct_name {
121                    #(#field_projections,)*
122                }
123            }
124        }
125    };
126
127    TokenStream::from(expanded)
128}
129
130fn derive_project_tuple_struct(input: &DeriveInput, fields: &syn::FieldsUnnamed) -> TokenStream {
131    let struct_name = &input.ident;
132    let (_impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
133
134    // Generate tuple type for projection
135    let field_types: Vec<&Type> = fields.unnamed.iter().map(|field| &field.ty).collect();
136    let projected_tuple = if field_types.len() == 1 {
137        quote! { (::nami::Binding<#(#field_types)*>,) }
138    } else {
139        quote! { (#(::nami::Binding<#field_types>),*) }
140    };
141
142    // Generate field projections using index access
143    let field_projections = fields.unnamed.iter().enumerate().map(|(index, _)| {
144        let idx = syn::Index::from(index);
145        quote! {
146            {
147                let source = source.clone();
148                ::nami::Binding::mapping(
149                    &source,
150                    |value| value.#idx.clone(),
151                    move |binding, value| {
152                        binding.with_mut(|b| {
153                            b.#idx = value;
154                        });
155                    },
156                )
157            }
158        }
159    });
160
161    // Add lifetime bounds to generic parameters
162    let mut generics_with_static = input.generics.clone();
163    for param in &mut generics_with_static.params {
164        if let syn::GenericParam::Type(type_param) = param {
165            type_param.bounds.push(syn::parse_quote!('static));
166        }
167    }
168    let (impl_generics_with_static, _, _) = generics_with_static.split_for_impl();
169
170    let projection_tuple = if field_projections.len() == 1 {
171        quote! { (#(#field_projections)*,) }
172    } else {
173        quote! { (#(#field_projections),*) }
174    };
175
176    let expanded = quote! {
177        impl #impl_generics_with_static ::nami::project::Project for #struct_name #ty_generics #where_clause {
178            type Projected = #projected_tuple;
179
180            fn project(source: &::nami::Binding<Self>) -> Self::Projected {
181                #projection_tuple
182            }
183        }
184    };
185
186    TokenStream::from(expanded)
187}
188
189fn derive_project_unit_struct(input: &DeriveInput) -> TokenStream {
190    let struct_name = &input.ident;
191    let (_impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
192
193    // Add lifetime bounds to generic parameters
194    let mut generics_with_static = input.generics.clone();
195    for param in &mut generics_with_static.params {
196        if let syn::GenericParam::Type(type_param) = param {
197            type_param.bounds.push(syn::parse_quote!('static));
198        }
199    }
200    let (impl_generics_with_static, _, _) = generics_with_static.split_for_impl();
201
202    let expanded = quote! {
203        impl #impl_generics_with_static ::nami::project::Project for #struct_name #ty_generics #where_clause {
204            type Projected = ();
205
206            fn project(_source: &::nami::Binding<Self>) -> Self::Projected {
207                ()
208            }
209        }
210    };
211
212    TokenStream::from(expanded)
213}
214
215/// Input structure for the `s!` macro
216struct SInput {
217    format_str: LitStr,
218    args: Punctuated<Expr, Token![,]>,
219}
220
221impl Parse for SInput {
222    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
223        let format_str: LitStr = input.parse()?;
224        let args = if input.peek(Token![,]) {
225            input.parse::<Token![,]>()?;
226            Punctuated::parse_terminated(input)?
227        } else {
228            Punctuated::new()
229        };
230        Ok(Self { format_str, args })
231    }
232}
233
234/// Function-like procedural macro for creating formatted string signals with automatic variable capture.
235///
236/// This macro automatically detects named variables in format strings and captures them from scope.
237///
238/// # Examples
239///
240/// ```rust
241/// use nami::*;
242///
243/// let name = constant("Alice");
244/// let age = constant(25);
245///
246/// // Automatic variable capture from format string
247/// let msg = s!("Hello {name}, you are {age} years old");
248///
249/// // Positional arguments still work
250/// let msg2 = s!("Hello {}, you are {}", name, age);
251/// ```
252#[proc_macro]
253#[allow(clippy::similar_names)] // Allow arg1, arg2, etc.
254pub fn s(input: TokenStream) -> TokenStream {
255    let input = parse_macro_input!(input as SInput);
256    let format_str = input.format_str;
257    let format_value = format_str.value();
258
259    // Check for format string issues
260    let (has_positional, has_named, positional_count, named_vars) =
261        analyze_format_string(&format_value);
262
263    // If there are explicit arguments, validate and use positional approach
264    if !input.args.is_empty() {
265        // Check for mixed usage errors
266        if has_named {
267            return syn::Error::new_spanned(
268                &format_str,
269                format!(
270                    "Format string contains named arguments like {{{}}} but you provided positional arguments. \
271                    Either use positional placeholders like {{}} or remove the explicit arguments to use automatic variable capture.",
272                    named_vars.first().unwrap_or(&String::new())
273                )
274            )
275            .to_compile_error()
276            .into();
277        }
278
279        // Check argument count matches placeholders
280        if positional_count != input.args.len() {
281            return syn::Error::new_spanned(
282                &format_str,
283                format!(
284                    "Format string has {} positional placeholder(s) but {} arguments were provided",
285                    positional_count,
286                    input.args.len()
287                ),
288            )
289            .to_compile_error()
290            .into();
291        }
292        let args: Vec<_> = input.args.iter().collect();
293        return handle_s_args(&format_str, &args);
294    }
295
296    // Check for mixed placeholders when no explicit arguments
297    if has_positional && has_named {
298        return syn::Error::new_spanned(
299            &format_str,
300            "Format string mixes positional {{}} and named {{var}} placeholders. \
301            Use either all positional with explicit arguments, or all named for automatic capture.",
302        )
303        .to_compile_error()
304        .into();
305    }
306
307    // If has positional placeholders but no arguments provided
308    if has_positional && input.args.is_empty() {
309        return syn::Error::new_spanned(
310            &format_str,
311            format!(
312                "Format string has {positional_count} positional placeholder(s) {{}} but no arguments provided. \
313                Either provide arguments or use named placeholders like {{variable}} for automatic capture."
314            )
315        )
316        .to_compile_error()
317        .into();
318    }
319
320    // Parse format string to extract variable names for automatic capture
321    let var_names = named_vars;
322
323    // If no variables found, return constant
324    if var_names.is_empty() {
325        return quote! {
326            {
327                use ::nami::constant;
328                constant(nami::__format!(#format_str))
329            }
330        }
331        .into();
332    }
333
334    // Generate code for named variable capture
335    let var_idents: Vec<syn::Ident> = var_names
336        .iter()
337        .map(|name| syn::Ident::new(name, format_str.span()))
338        .collect();
339
340    handle_s_named_vars(&format_str, &var_idents)
341}
342
343#[allow(clippy::similar_names)]
344fn handle_s_args(format_str: &LitStr, args: &[&Expr]) -> TokenStream {
345    match args.len() {
346        1 => {
347            let arg = &args[0];
348            (quote! {
349                {
350                    use ::nami::SignalExt;
351                    (#arg).map(|arg| nami::__format!(#format_str, arg))
352                }
353            })
354            .into()
355        }
356        2 => {
357            let arg1 = &args[0];
358            let arg2 = &args[1];
359            (quote! {
360                {
361                    use nami::{SignalExt, zip::zip};
362                    zip(#arg1.clone(), #arg2.clone()).map(|(arg1, arg2)| {
363                        nami::__format!(#format_str, arg1, arg2)
364                    })
365                }
366            })
367            .into()
368        }
369        3 => {
370            let arg1 = &args[0];
371            let arg2 = &args[1];
372            let arg3 = &args[2];
373            (quote! {
374                {
375                    use ::nami::{SignalExt, zip::zip};
376                    zip(zip(#arg1.clone(), #arg2.clone()), #arg3.clone()).map(
377                        |((arg1, arg2), arg3)| nami::__format!(#format_str, arg1, arg2, arg3)
378                    )
379                }
380            })
381            .into()
382        }
383        4 => {
384            let arg1 = &args[0];
385            let arg2 = &args[1];
386            let arg3 = &args[2];
387            let arg4 = &args[3];
388            (quote! {
389                {
390                    use ::nami::{SignalExt, zip::zip};
391                    zip(
392                        zip(#arg1.clone(), #arg2.clone()),
393                        zip(#arg3.clone(), #arg4.clone())
394                    ).map(
395                        |((arg1, arg2), (arg3, arg4))| nami::__format!(#format_str, arg1, arg2, arg3, arg4)
396                    )
397                }
398            }).into()
399        }
400        _ => syn::Error::new_spanned(format_str, "Too many arguments, maximum 4 supported")
401            .to_compile_error()
402            .into(),
403    }
404}
405
406#[allow(clippy::similar_names)]
407fn handle_s_named_vars(format_str: &LitStr, var_idents: &[syn::Ident]) -> TokenStream {
408    match var_idents.len() {
409        1 => {
410            let var = &var_idents[0];
411            (quote! {
412                {
413                    use ::nami::SignalExt;
414                    (#var).map(|#var| {
415                        nami::__format!(#format_str)
416                    })
417                }
418            })
419            .into()
420        }
421        2 => {
422            let var1 = &var_idents[0];
423            let var2 = &var_idents[1];
424            (quote! {
425                {
426                    use ::nami::{SignalExt, zip::zip};
427                    zip(#var1.clone(), #var2.clone()).map(|(#var1, #var2)| {
428                        nami::__format!(#format_str)
429                    })
430                }
431            })
432            .into()
433        }
434        3 => {
435            let var1 = &var_idents[0];
436            let var2 = &var_idents[1];
437            let var3 = &var_idents[2];
438            (quote! {
439                {
440                    use ::nami::{SignalExt, zip::zip};
441                    zip(zip(#var1.clone(), #var2.clone()), #var3.clone()).map(
442                        |((#var1, #var2), #var3)| {
443                            ::nami::__format!(#format_str)
444                        }
445                    )
446                }
447            })
448            .into()
449        }
450        4 => {
451            let var1 = &var_idents[0];
452            let var2 = &var_idents[1];
453            let var3 = &var_idents[2];
454            let var4 = &var_idents[3];
455            (quote! {
456                {
457                    use ::nami::{SignalExt, zip::zip};
458                    zip(
459                        zip(#var1.clone(), #var2.clone()),
460                        zip(#var3.clone(), #var4.clone())
461                    ).map(
462                        |((#var1, #var2), (#var3, #var4))| {
463                            ::nami::__format!(#format_str)
464                        }
465                    )
466                }
467            })
468            .into()
469        }
470        _ => syn::Error::new_spanned(format_str, "Too many named variables, maximum 4 supported")
471            .to_compile_error()
472            .into(),
473    }
474}
475
476/// Analyze a format string to detect placeholder types and extract variable names
477fn analyze_format_string(format_str: &str) -> (bool, bool, usize, Vec<String>) {
478    let mut has_positional = false;
479    let mut has_named = false;
480    let mut positional_count = 0;
481    let mut named_vars = Vec::new();
482    let mut chars = format_str.chars().peekable();
483
484    while let Some(c) = chars.next() {
485        if c == '{' {
486            if chars.peek() == Some(&'{') {
487                // Skip escaped braces
488                chars.next();
489                continue;
490            }
491
492            let mut content = String::new();
493            let mut has_content = false;
494
495            while let Some(&next_char) = chars.peek() {
496                if next_char == '}' {
497                    chars.next(); // consume }
498                    break;
499                } else if next_char == ':' {
500                    // Format specifier found, we've captured the name/position part
501                    chars.next(); // consume :
502                    while let Some(&spec_char) = chars.peek() {
503                        if spec_char == '}' {
504                            chars.next(); // consume }
505                            break;
506                        }
507                        chars.next();
508                    }
509                    break;
510                }
511                content.push(chars.next().unwrap());
512                has_content = true;
513            }
514
515            // Analyze the content
516            if !has_content || content.is_empty() {
517                // Empty {} is positional
518                has_positional = true;
519                positional_count += 1;
520            } else if content.chars().all(|ch| ch.is_ascii_digit()) {
521                // Numeric like {0} or {1} is positional
522                has_positional = true;
523                positional_count += 1;
524            } else if content
525                .chars()
526                .next()
527                .is_some_and(|ch| ch.is_ascii_alphabetic() || ch == '_')
528            {
529                // Starts with letter or underscore, likely a variable name
530                has_named = true;
531                if !named_vars.contains(&content) {
532                    named_vars.push(content);
533                }
534            } else {
535                // Other cases treat as positional
536                has_positional = true;
537                positional_count += 1;
538            }
539        }
540    }
541
542    (has_positional, has_named, positional_count, named_vars)
543}