1use 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#[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 let projected_struct_name =
70 syn::Ident::new(&format!("{struct_name}Projected"), struct_name.span());
71
72 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 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 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 #[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 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 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 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 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
215struct 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#[proc_macro]
253#[allow(clippy::similar_names)] pub 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 let (has_positional, has_named, positional_count, named_vars) =
261 analyze_format_string(&format_value);
262
263 if !input.args.is_empty() {
265 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 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 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 && 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 let var_names = named_vars;
322
323 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 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 SignalExt::map(#arg.clone(), |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 SignalExt::map(zip(#arg1.clone(), #arg2.clone()), |(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 SignalExt::map(
377 zip(zip(#arg1.clone(), #arg2.clone()), #arg3.clone()),
378 |((arg1, arg2), arg3)| nami::__format!(#format_str, arg1, arg2, arg3)
379 )
380 }
381 })
382 .into()
383 }
384 4 => {
385 let arg1 = &args[0];
386 let arg2 = &args[1];
387 let arg3 = &args[2];
388 let arg4 = &args[3];
389 (quote! {
390 {
391 use ::nami::{SignalExt, zip::zip};
392 SignalExt::map(
393 zip(
394 zip(#arg1.clone(), #arg2.clone()),
395 zip(#arg3.clone(), #arg4.clone())
396 ),
397 |((arg1, arg2), (arg3, arg4))| nami::__format!(#format_str, arg1, arg2, arg3, arg4)
398 )
399 }
400 }).into()
401 }
402 _ => syn::Error::new_spanned(format_str, "Too many arguments, maximum 4 supported")
403 .to_compile_error()
404 .into(),
405 }
406}
407
408#[allow(clippy::similar_names)]
409fn handle_s_named_vars(format_str: &LitStr, var_idents: &[syn::Ident]) -> TokenStream {
410 match var_idents.len() {
411 1 => {
412 let var = &var_idents[0];
413 (quote! {
414 {
415 use ::nami::SignalExt;
416 SignalExt::map(#var.clone(), |#var| {
417 nami::__format!(#format_str)
418 })
419 }
420 })
421 .into()
422 }
423 2 => {
424 let var1 = &var_idents[0];
425 let var2 = &var_idents[1];
426 (quote! {
427 {
428 use ::nami::{SignalExt, zip::zip};
429 SignalExt::map(zip(#var1.clone(), #var2.clone()), |(#var1, #var2)| {
430 nami::__format!(#format_str)
431 })
432 }
433 })
434 .into()
435 }
436 3 => {
437 let var1 = &var_idents[0];
438 let var2 = &var_idents[1];
439 let var3 = &var_idents[2];
440 (quote! {
441 {
442 use ::nami::{SignalExt, zip::zip};
443 SignalExt::map(
444 zip(zip(#var1.clone(), #var2.clone()), #var3.clone()),
445 |((#var1, #var2), #var3)| {
446 ::nami::__format!(#format_str)
447 }
448 )
449 }
450 })
451 .into()
452 }
453 4 => {
454 let var1 = &var_idents[0];
455 let var2 = &var_idents[1];
456 let var3 = &var_idents[2];
457 let var4 = &var_idents[3];
458 (quote! {
459 {
460 use ::nami::{SignalExt, zip::zip};
461 SignalExt::map(
462 zip(
463 zip(#var1.clone(), #var2.clone()),
464 zip(#var3.clone(), #var4.clone())
465 ),
466 |((#var1, #var2), (#var3, #var4))| {
467 ::nami::__format!(#format_str)
468 }
469 )
470 }
471 })
472 .into()
473 }
474 _ => syn::Error::new_spanned(format_str, "Too many named variables, maximum 4 supported")
475 .to_compile_error()
476 .into(),
477 }
478}
479
480fn analyze_format_string(format_str: &str) -> (bool, bool, usize, Vec<String>) {
482 let mut has_positional = false;
483 let mut has_named = false;
484 let mut positional_count = 0;
485 let mut named_vars = Vec::new();
486 let mut chars = format_str.chars().peekable();
487
488 while let Some(c) = chars.next() {
489 if c == '{' {
490 if chars.peek() == Some(&'{') {
491 chars.next();
493 continue;
494 }
495
496 let mut content = String::new();
497 let mut has_content = false;
498
499 while let Some(&next_char) = chars.peek() {
500 if next_char == '}' {
501 chars.next(); break;
503 } else if next_char == ':' {
504 chars.next(); while let Some(&spec_char) = chars.peek() {
507 if spec_char == '}' {
508 chars.next(); break;
510 }
511 chars.next();
512 }
513 break;
514 }
515 content.push(chars.next().unwrap());
516 has_content = true;
517 }
518
519 if !has_content || content.is_empty() {
521 has_positional = true;
523 positional_count += 1;
524 } else if content.chars().all(|ch| ch.is_ascii_digit()) {
525 has_positional = true;
527 positional_count += 1;
528 } else if content
529 .chars()
530 .next()
531 .is_some_and(|ch| ch.is_ascii_alphabetic() || ch == '_')
532 {
533 has_named = true;
535 if !named_vars.contains(&content) {
536 named_vars.push(content);
537 }
538 } else {
539 has_positional = true;
541 positional_count += 1;
542 }
543 }
544 }
545
546 (has_positional, has_named, positional_count, named_vars)
547}