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 (#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
476fn 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 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(); break;
499 } else if next_char == ':' {
500 chars.next(); while let Some(&spec_char) = chars.peek() {
503 if spec_char == '}' {
504 chars.next(); break;
506 }
507 chars.next();
508 }
509 break;
510 }
511 content.push(chars.next().unwrap());
512 has_content = true;
513 }
514
515 if !has_content || content.is_empty() {
517 has_positional = true;
519 positional_count += 1;
520 } else if content.chars().all(|ch| ch.is_ascii_digit()) {
521 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 has_named = true;
531 if !named_vars.contains(&content) {
532 named_vars.push(content);
533 }
534 } else {
535 has_positional = true;
537 positional_count += 1;
538 }
539 }
540 }
541
542 (has_positional, has_named, positional_count, named_vars)
543}