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#[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 let projected_struct_name =
67 syn::Ident::new(&format!("{}Projected", struct_name), struct_name.span());
68
69 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 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 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 #[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 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 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 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 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
208struct 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#[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 let (has_positional, has_named, positional_count, named_vars) = analyze_format_string(&format_value);
254
255 if !input.args.is_empty() {
257 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 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 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 && 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 let var_names = named_vars;
375
376 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 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
462fn 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 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(); break;
483 } else if next_char == ':' {
484 chars.next(); while let Some(&spec_char) = chars.peek() {
487 if spec_char == '}' {
488 chars.next(); break;
490 }
491 chars.next();
492 }
493 break;
494 } else {
495 content.push(chars.next().unwrap());
496 has_content = true;
497 }
498 }
499
500 if !has_content || content.is_empty() {
502 has_positional = true;
504 positional_count += 1;
505 } else if content.chars().all(|ch| ch.is_ascii_digit()) {
506 has_positional = true;
508 positional_count += 1;
509 } else if content.chars().next().is_some_and(|ch| ch.is_ascii_alphabetic() || ch == '_') {
510 has_named = true;
512 if !named_vars.contains(&content) {
513 named_vars.push(content);
514 }
515 } else {
516 has_positional = true;
518 positional_count += 1;
519 }
520 }
521 }
522
523 (has_positional, has_named, positional_count, named_vars)
524}
525