1extern crate proc_macro;
2
3use cu29_intern_strs::intern_string;
4use cu29_log::CuLogLevel;
5use proc_macro::TokenStream;
6#[cfg(feature = "textlogs")]
7use proc_macro_crate::{FoundCrate, crate_name};
8#[cfg(feature = "textlogs")]
9use proc_macro2::Span;
10use proc_macro2::TokenStream as TokenStream2;
11use quote::quote;
12#[allow(unused)]
13use syn::Token;
14use syn::parse::Parser;
15#[cfg(any(feature = "textlogs", debug_assertions))]
16use syn::spanned::Spanned;
17use syn::{Expr, ExprLit, Lit};
18
19struct ParsedLogArgs<'a> {
20 context_expr: Option<&'a Expr>,
21 msg_expr: &'a Expr,
22 param_exprs: Vec<&'a Expr>,
23}
24
25fn parse_log_exprs(
26 input: TokenStream,
27) -> syn::Result<syn::punctuated::Punctuated<Expr, syn::Token![,]>> {
28 let parser = syn::punctuated::Punctuated::<Expr, syn::Token![,]>::parse_terminated;
29 parser.parse(input)
30}
31
32fn parse_log_args<'a>(
33 exprs: &'a syn::punctuated::Punctuated<Expr, syn::Token![,]>,
34) -> syn::Result<ParsedLogArgs<'a>> {
35 let Some(first_expr) = exprs.first() else {
36 return Err(syn::Error::new(
37 proc_macro2::Span::call_site(),
38 "Expected at least one expression",
39 ));
40 };
41
42 let (context_expr, msg_expr, msg_index) = if matches!(
43 first_expr,
44 Expr::Lit(ExprLit {
45 lit: Lit::Str(_),
46 ..
47 })
48 ) {
49 (None, first_expr, 0)
50 } else {
51 let Some(second_expr) = exprs.iter().nth(1) else {
52 return Err(syn::Error::new_spanned(
53 first_expr,
54 "Expected a string literal as the first argument, or as the second argument after a CuContext expression.",
55 ));
56 };
57 if matches!(
58 second_expr,
59 Expr::Lit(ExprLit {
60 lit: Lit::Str(_),
61 ..
62 })
63 ) {
64 (Some(first_expr), second_expr, 1)
65 } else {
66 return Err(syn::Error::new_spanned(
67 second_expr,
68 "Expected a string literal as the first argument, or as the second argument after a CuContext expression.",
69 ));
70 }
71 };
72
73 Ok(ParsedLogArgs {
74 context_expr,
75 msg_expr,
76 param_exprs: exprs.iter().skip(msg_index + 1).collect(),
77 })
78}
79
80#[allow(unused)]
83fn reference_unused_variables(input: TokenStream) -> TokenStream {
84 if let Ok(exprs) = parse_log_exprs(input.clone())
87 && let Ok(parsed) = parse_log_args(&exprs)
88 {
89 let mut var_usages = Vec::new();
90 if let Some(context_expr) = parsed.context_expr {
91 var_usages.push(quote::quote! { let _ = &#context_expr; });
92 }
93 for expr in parsed.param_exprs {
94 match expr {
95 syn::Expr::Assign(assign_expr) => {
96 let value_expr = &assign_expr.right;
97 var_usages.push(quote::quote! { let _ = &#value_expr; });
98 }
99 _ => {
100 var_usages.push(quote::quote! { let _ = &#expr; });
101 }
102 }
103 }
104 return quote::quote! { { #(#var_usages;)* } }.into();
105 }
106
107 if let Ok(exprs) = parse_log_exprs(input.clone()) {
108 let mut var_usages = Vec::new();
109 for expr in exprs.iter() {
110 match expr {
111 syn::Expr::Assign(assign_expr) => {
114 let value_expr = &assign_expr.right;
115 var_usages.push(quote::quote! { let _ = &#value_expr; });
116 }
117 _ => {
119 var_usages.push(quote::quote! { let _ = &#expr; });
120 }
121 }
122 }
123 return quote::quote! { { #(#var_usages;)* } }.into();
127 }
128
129 TokenStream::new()
132}
133
134fn is_ident(s: &str) -> bool {
137 let mut chars = s.chars();
138 match chars.next() {
139 Some(c) if c == '_' || c.is_alphabetic() => {}
140 _ => return false,
141 }
142 chars.all(|c| c == '_' || c.is_alphanumeric())
143}
144
145fn extract_captured_idents(fmt: &str) -> Vec<String> {
151 let mut names = Vec::new();
152 let mut rest = fmt;
153 while let Some(open) = rest.find('{') {
154 if rest[open + 1..].starts_with('{') {
155 rest = &rest[open + 2..]; continue;
157 }
158 let after = &rest[open + 1..];
159 let Some(close) = after.find('}') else { break };
160 let name = after[..close].split(':').next().unwrap_or("").trim();
162 if is_ident(name) && !names.iter().any(|n| n == name) {
163 names.push(name.to_string());
164 }
165 rest = &after[close + 1..];
166 }
167 names
168}
169
170#[allow(unused)]
175fn create_log_entry(input: TokenStream, level: CuLogLevel) -> TokenStream {
176 use quote::quote;
177 use syn::{Expr, ExprAssign, ExprLit, Lit, Token};
178
179 let exprs = match parse_log_exprs(input) {
180 Ok(exprs) => exprs,
181 Err(err) => return err.to_compile_error().into(),
182 };
183 let parsed = match parse_log_args(&exprs) {
184 Ok(parsed) => parsed,
185 Err(err) => return err.to_compile_error().into(),
186 };
187
188 #[cfg(not(feature = "std"))]
189 const STD: bool = false;
190 #[cfg(feature = "std")]
191 const STD: bool = true;
192
193 let msg_expr = parsed.msg_expr;
194 let (index, msg_str) = if let Expr::Lit(ExprLit {
195 lit: Lit::Str(msg), ..
196 }) = msg_expr
197 {
198 let s = msg.value();
199 let index = match intern_string(&s) {
200 Some(index) => index,
201 None => {
202 return syn::Error::new_spanned(msg_expr, "Failed to intern log string.")
203 .to_compile_error()
204 .into();
205 }
206 };
207 (index, s)
208 } else {
209 return syn::Error::new_spanned(
210 msg_expr,
211 "The first parameter of the argument needs to be a string literal.",
212 )
213 .to_compile_error()
214 .into();
215 };
216
217 let level_ident = match level {
218 CuLogLevel::Debug => quote! { Debug },
219 CuLogLevel::Info => quote! { Info },
220 CuLogLevel::Warning => quote! { Warning },
221 CuLogLevel::Error => quote! { Error },
222 CuLogLevel::Critical => quote! { Critical },
223 };
224
225 let captured_exprs: Vec<Expr> = extract_captured_idents(&msg_str)
229 .iter()
230 .filter_map(|name| syn::parse_str::<Expr>(name).ok())
231 .collect();
232
233 let mut unnamed_params = Vec::<&Expr>::new();
235 let mut named_params = Vec::<(&Expr, &Expr)>::new();
236
237 for expr in parsed.param_exprs {
238 if let Expr::Assign(ExprAssign { left, right, .. }) = expr {
239 named_params.push((left, right));
240 } else {
241 unnamed_params.push(expr);
242 }
243 }
244
245 for expr in &captured_exprs {
248 let name = quote!(#expr).to_string();
249 let already_named = named_params
250 .iter()
251 .any(|(n, _)| quote!(#n).to_string() == name);
252 if !already_named {
253 named_params.push((expr, expr));
254 }
255 }
256
257 let unnamed_prints = unnamed_params.iter().map(|value| {
259 quote! {
260 let param = to_value(#value).expect("Failed to convert a parameter to a Value");
261 log_entry.add_param(ANONYMOUS, param);
262 }
263 });
264
265 let mut named_prints = Vec::with_capacity(named_params.len());
266 for (name, value) in &named_params {
267 let name_str = quote!(#name).to_string();
268 let idx = match intern_string(&name_str) {
269 Some(idx) => idx,
270 None => {
271 return syn::Error::new_spanned(name, "Failed to intern log parameter name.")
272 .to_compile_error()
273 .into();
274 }
275 };
276 named_prints.push(quote! {
277 let param = to_value(#value).expect("Failed to convert a parameter to a Value");
278 log_entry.add_param(#idx, param);
279 });
280 }
281
282 #[cfg(feature = "textlogs")]
285 let (defmt_fmt_lit, defmt_args_unnamed_ts, defmt_args_named_ts, defmt_available) = {
286 let defmt_fmt_lit = {
287 let mut s = msg_str.clone();
288 if !named_params.is_empty() {
289 s.push_str(" |");
290 }
291 for (name, _) in named_params.iter() {
292 let name_str = quote!(#name).to_string();
293 s.push(' ');
294 s.push_str(&name_str);
295 s.push_str("={:?}");
296 }
297 syn::LitStr::new(&s, msg_expr.span())
298 };
299
300 let defmt_args_unnamed_ts: Vec<TokenStream2> =
301 unnamed_params.iter().map(|e| quote! { #e }).collect();
302 let defmt_args_named_ts: Vec<TokenStream2> = named_params
303 .iter()
304 .map(|(_, rhs)| quote! { #rhs })
305 .collect();
306
307 let defmt_available = crate_name("defmt").is_ok();
308
309 (
310 defmt_fmt_lit,
311 defmt_args_unnamed_ts,
312 defmt_args_named_ts,
313 defmt_available,
314 )
315 };
316
317 #[cfg(feature = "textlogs")]
318 fn defmt_macro_path(level: CuLogLevel) -> TokenStream2 {
319 let macro_ident = match level {
320 CuLogLevel::Debug => quote! { defmt_debug },
321 CuLogLevel::Info => quote! { defmt_info },
322 CuLogLevel::Warning => quote! { defmt_warn },
323 CuLogLevel::Error => quote! { defmt_error },
324 CuLogLevel::Critical => quote! { defmt_error },
325 };
326
327 let (base, use_prelude) = match crate_name("cu29-log") {
328 Ok(FoundCrate::Name(name)) => {
329 let ident = proc_macro2::Ident::new(&name, Span::call_site());
330 (quote! { ::#ident }, false)
331 }
332 Ok(FoundCrate::Itself) => (quote! { crate }, false),
333 Err(_) => match crate_name("cu29") {
334 Ok(FoundCrate::Name(name)) => {
335 let ident = proc_macro2::Ident::new(&name, Span::call_site());
336 (quote! { ::#ident }, true)
337 }
338 Ok(FoundCrate::Itself) => (quote! { crate }, true),
339 Err(_) => (quote! { ::cu29_log }, false),
340 },
341 };
342
343 if use_prelude {
344 quote! { #base::prelude::#macro_ident }
345 } else {
346 quote! { #base::#macro_ident }
347 }
348 }
349
350 #[cfg(not(debug_assertions))]
352 let log_stmt = quote! { let r = log(&mut log_entry); };
353
354 #[cfg(debug_assertions)]
355 let log_stmt: TokenStream2 = {
356 let keys: Vec<_> = named_params
357 .iter()
358 .map(|(name, _)| {
359 let name_str = quote!(#name).to_string();
360 syn::LitStr::new(&name_str, name.span())
361 })
362 .collect();
363 quote! {
364 let r = log_debug_mode(&mut log_entry, #msg_str, &[#(#keys),*]);
365 }
366 };
367
368 let error_handling: Option<TokenStream2> = Some(quote! {
369 if let Err(_e) = r {
370 let _ = &_e;
371 }
372 });
373
374 #[cfg(feature = "textlogs")]
375 let defmt_macro: TokenStream2 = defmt_macro_path(level);
376
377 #[cfg(feature = "textlogs")]
378 let maybe_inject_defmt: Option<TokenStream2> = if STD || !defmt_available {
379 None } else {
381 Some(quote! {
382 #defmt_macro!(#defmt_fmt_lit, #(#defmt_args_unnamed_ts,)* #(#defmt_args_named_ts,)*);
383 })
384 };
385
386 #[cfg(not(feature = "textlogs"))]
387 let maybe_inject_defmt: Option<TokenStream2> = None; let maybe_inject_origin: Option<TokenStream2> = parsed.context_expr.map(|ctx_expr| {
390 quote! {
391 let __cu29_log_ctx = &#ctx_expr;
392 log_entry.set_origin(
393 Some(__cu29_log_ctx.cl_id()),
394 __cu29_log_ctx
395 .current_component_id()
396 .map(|component_id| component_id as u32),
397 __cu29_log_ctx.task_index().map(|task_index| task_index as u32),
398 );
399 }
400 });
401
402 quote! {{
404 let mut log_entry = CuLogEntry::new(#index, CuLogLevel::#level_ident);
405 #maybe_inject_origin
406 #(#unnamed_prints)*
407 #(#named_prints)*
408
409 #maybe_inject_defmt
410
411 #log_stmt
412 #error_handling
413 }}
414 .into()
415}
416
417#[cfg(any(feature = "log-level-debug", cu29_default_log_level_debug))]
452#[proc_macro]
453pub fn debug(input: TokenStream) -> TokenStream {
454 create_log_entry(input, CuLogLevel::Debug)
455}
456
457#[cfg(any(
470 feature = "log-level-debug",
471 feature = "log-level-info",
472 cu29_default_log_level_debug,
473 cu29_default_log_level_info,
474))]
475#[proc_macro]
476pub fn info(input: TokenStream) -> TokenStream {
477 create_log_entry(input, CuLogLevel::Info)
478}
479
480#[cfg(any(
493 feature = "log-level-debug",
494 feature = "log-level-info",
495 feature = "log-level-warning",
496 cu29_default_log_level_debug,
497 cu29_default_log_level_info,
498))]
499#[proc_macro]
500pub fn warning(input: TokenStream) -> TokenStream {
501 create_log_entry(input, CuLogLevel::Warning)
502}
503
504#[cfg(any(
517 feature = "log-level-debug",
518 feature = "log-level-info",
519 feature = "log-level-warning",
520 feature = "log-level-error",
521 cu29_default_log_level_debug,
522 cu29_default_log_level_info,
523))]
524#[proc_macro]
525pub fn error(input: TokenStream) -> TokenStream {
526 create_log_entry(input, CuLogLevel::Error)
527}
528
529#[cfg(any(
542 feature = "log-level-debug",
543 feature = "log-level-info",
544 feature = "log-level-warning",
545 feature = "log-level-error",
546 feature = "log-level-critical",
547 cu29_default_log_level_debug,
548 cu29_default_log_level_info,
549))]
550#[proc_macro]
551pub fn critical(input: TokenStream) -> TokenStream {
552 create_log_entry(input, CuLogLevel::Critical)
553}
554
555#[cfg(not(any(feature = "log-level-debug", cu29_default_log_level_debug)))]
557#[proc_macro]
558pub fn debug(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
559 reference_unused_variables(input)
560}
561
562#[cfg(not(any(
563 feature = "log-level-debug",
564 feature = "log-level-info",
565 cu29_default_log_level_debug,
566 cu29_default_log_level_info,
567)))]
568#[proc_macro]
569pub fn info(input: TokenStream) -> TokenStream {
570 reference_unused_variables(input)
571}
572
573#[cfg(not(any(
574 feature = "log-level-debug",
575 feature = "log-level-info",
576 feature = "log-level-warning",
577 cu29_default_log_level_debug,
578 cu29_default_log_level_info,
579)))]
580#[proc_macro]
581pub fn warning(input: TokenStream) -> TokenStream {
582 reference_unused_variables(input)
583}
584
585#[cfg(not(any(
586 feature = "log-level-debug",
587 feature = "log-level-info",
588 feature = "log-level-warning",
589 feature = "log-level-error",
590 cu29_default_log_level_debug,
591 cu29_default_log_level_info,
592)))]
593#[proc_macro]
594pub fn error(input: TokenStream) -> TokenStream {
595 reference_unused_variables(input)
596}
597
598#[cfg(not(any(
599 feature = "log-level-debug",
600 feature = "log-level-info",
601 feature = "log-level-warning",
602 feature = "log-level-error",
603 feature = "log-level-critical",
604 cu29_default_log_level_debug,
605 cu29_default_log_level_info,
606)))]
607#[proc_macro]
608pub fn critical(input: TokenStream) -> TokenStream {
609 reference_unused_variables(input)
610}
611
612#[proc_macro]
619pub fn intern(input: TokenStream) -> TokenStream {
620 let expr = match syn::parse::<Expr>(input) {
621 Ok(expr) => expr,
622 Err(err) => return err.to_compile_error().into(),
623 };
624 let index = if let Expr::Lit(ExprLit {
625 lit: Lit::Str(msg), ..
626 }) = &expr
627 {
628 let msg = msg.value();
629 match intern_string(&msg) {
630 Some(index) => index,
631 None => {
632 return syn::Error::new_spanned(&expr, "Failed to intern log string.")
633 .to_compile_error()
634 .into();
635 }
636 }
637 } else {
638 return syn::Error::new_spanned(
639 &expr,
640 "The first parameter of the argument needs to be a string literal.",
641 )
642 .to_compile_error()
643 .into();
644 };
645 quote! { #index }.into()
646}
647
648#[cfg(test)]
649mod tests {
650 use super::{extract_captured_idents, is_ident};
651
652 #[test]
653 fn captures_simple_named_placeholder() {
654 assert_eq!(extract_captured_idents("Hash: {hash}"), vec!["hash"]);
655 }
656
657 #[test]
658 fn captures_identifier_before_format_spec() {
659 assert_eq!(
660 extract_captured_idents("{hash:?} {size:>5}"),
661 vec!["hash", "size"]
662 );
663 }
664
665 #[test]
666 fn ignores_positional_and_escaped_braces() {
667 assert!(extract_captured_idents("{} {0} {{not_a_capture}}").is_empty());
668 }
669
670 #[test]
671 fn mixes_positional_and_named_and_dedupes() {
672 assert_eq!(
673 extract_captured_idents("{} {hash} and again {hash}"),
674 vec!["hash"]
675 );
676 }
677
678 #[test]
679 fn is_ident_rejects_non_identifiers() {
680 assert!(is_ident("hash"));
681 assert!(is_ident("_x1"));
682 assert!(!is_ident("0"));
683 assert!(!is_ident(""));
684 assert!(!is_ident(":?"));
685 }
686}