nami_derive/
lib.rs

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