cu29_log_derive/
lib.rs

1extern crate proc_macro;
2
3use cu29_intern_strs::intern_string;
4use cu29_log::CuLogLevel;
5use proc_macro::TokenStream;
6use proc_macro2::TokenStream as TokenStream2;
7use quote::quote;
8use syn::parse::Parser;
9use syn::spanned::Spanned;
10#[allow(unused)]
11use syn::Token;
12use syn::{Expr, ExprLit, Lit};
13
14/// Create reference of unused_variables to avoid warnings
15/// ex: let _ = &tmp;
16#[allow(unused)]
17fn reference_unused_variables(input: TokenStream) -> TokenStream {
18    // Attempt to parse the expressions to "use" them.
19    // This ensures variables passed to the macro are considered used by the compiler.
20    let parser = syn::punctuated::Punctuated::<syn::Expr, syn::Token![,]>::parse_terminated;
21    if let Ok(exprs) = parser.parse(input.clone()) {
22        let mut var_usages = Vec::new();
23        // Skip the first expression, which is assumed to be the format string literal.
24        // We only care about "using" the subsequent variable arguments.
25        for expr in exprs.iter().skip(1) {
26            match expr {
27                // If the argument is an assignment (e.g., `foo = bar`),
28                // we need to ensure `bar` (the right-hand side) is "used".
29                syn::Expr::Assign(assign_expr) => {
30                    let value_expr = &assign_expr.right;
31                    var_usages.push(quote::quote! { let _ = &#value_expr; });
32                }
33                // Otherwise, for any other expression, ensure it's "used".
34                _ => {
35                    var_usages.push(quote::quote! { let _ = &#expr; });
36                }
37            }
38        }
39        // Return a block that contains these dummy "usages".
40        // If only a format string was passed, var_usages will be empty,
41        // resulting in an empty block `{}`, which is fine.
42        return quote::quote! { { #(#var_usages;)* } }.into();
43    }
44
45    // Fallback: if parsing fails for some reason, return an empty TokenStream.
46    // This might still lead to warnings if parsing failed but is better than panicking.
47    TokenStream::new()
48}
49
50/// Create a log entry at the specified log level.
51///
52/// This is the internal macro implementation used by all the logging macros.
53/// Users should use the public-facing macros: `debug!`, `info!`, `warning!`, `error!`, or `critical!`.
54#[allow(unused)]
55fn create_log_entry(input: TokenStream, level: CuLogLevel) -> TokenStream {
56    use quote::quote;
57    use syn::{Expr, ExprAssign, ExprLit, Lit, Token};
58
59    let parser = syn::punctuated::Punctuated::<Expr, Token![,]>::parse_terminated;
60    let exprs = parser.parse(input).expect("Failed to parse input");
61    let mut exprs_iter = exprs.iter();
62
63    #[cfg(not(feature = "std"))]
64    const STD: bool = false;
65    #[cfg(feature = "std")]
66    const STD: bool = true;
67
68    let msg_expr = exprs_iter.next().expect("Expected at least one expression");
69    let (index, msg_str) = if let Expr::Lit(ExprLit {
70        lit: Lit::Str(msg), ..
71    }) = msg_expr
72    {
73        let s = msg.value();
74        let index = intern_string(&s).expect("Failed to insert log string.");
75        (index, s)
76    } else {
77        panic!("The first parameter of the argument needs to be a string literal.");
78    };
79
80    let level_ident = match level {
81        CuLogLevel::Debug => quote! { Debug },
82        CuLogLevel::Info => quote! { Info },
83        CuLogLevel::Warning => quote! { Warning },
84        CuLogLevel::Error => quote! { Error },
85        CuLogLevel::Critical => quote! { Critical },
86    };
87
88    // Partition unnamed vs named args (a = b treated as named)
89    let mut unnamed_params = Vec::<&Expr>::new();
90    let mut named_params = Vec::<(&Expr, &Expr)>::new();
91
92    for expr in exprs_iter {
93        if let Expr::Assign(ExprAssign { left, right, .. }) = expr {
94            named_params.push((left, right));
95        } else {
96            unnamed_params.push(expr);
97        }
98    }
99
100    // Build the CuLogEntry population tokens
101    let unnamed_prints = unnamed_params.iter().map(|value| {
102        quote! {
103            let param = to_value(#value).expect("Failed to convert a parameter to a Value");
104            log_entry.add_param(ANONYMOUS, param);
105        }
106    });
107
108    let named_prints = named_params.iter().map(|(name, value)| {
109        let idx = intern_string(quote!(#name).to_string().as_str())
110            .expect("Failed to insert log string.");
111        quote! {
112            let param = to_value(#value).expect("Failed to convert a parameter to a Value");
113            log_entry.add_param(#idx, param);
114        }
115    });
116
117    // ---------- For baremetal: build a defmt format literal and arg list ----------
118    // defmt line: "<msg> | a={:?}, b={:?}, arg0={:?} ..."
119    let defmt_fmt_lit = {
120        let mut s = msg_str.clone();
121        if !unnamed_params.is_empty() || !named_params.is_empty() {
122            s.push_str(" |");
123        }
124        for (i, _) in unnamed_params.iter().enumerate() {
125            use std::fmt::Write as _;
126            let _ = write!(&mut s, " arg{}={:?}", i, ());
127        }
128        for (name, _) in named_params.iter() {
129            let name_str = quote!(#name).to_string();
130            s.push(' ');
131            s.push_str(&name_str);
132            s.push_str("={:?}");
133        }
134        syn::LitStr::new(&s, msg_expr.span())
135    };
136
137    let defmt_args_unnamed_ts: Vec<TokenStream2> =
138        unnamed_params.iter().map(|e| quote! { #e }).collect();
139    let defmt_args_named_ts: Vec<TokenStream2> = named_params
140        .iter()
141        .map(|(_, rhs)| quote! { #rhs })
142        .collect();
143
144    // Runtime logging path (unchanged)
145    #[cfg(not(debug_assertions))]
146    let log_stmt = quote! { let r = log(&mut log_entry); };
147
148    #[cfg(debug_assertions)]
149    let log_stmt: TokenStream2 = {
150        let keys: Vec<_> = named_params
151            .iter()
152            .map(|(name, _)| {
153                let name_str = quote!(#name).to_string();
154                syn::LitStr::new(&name_str, name.span())
155            })
156            .collect();
157        quote! {
158            let r = log_debug_mode(&mut log_entry, #msg_str, &[#(#keys),*]);
159        }
160    };
161
162    let error_handling: Option<TokenStream2> = if STD {
163        Some(quote! {
164            if let Err(e) = r {
165                eprintln!("Warning: Failed to log: {}", e);
166                let backtrace = std::backtrace::Backtrace::capture();
167                eprintln!("{:?}", backtrace);
168            }
169        })
170    } else {
171        None
172    };
173
174    #[cfg(debug_assertions)]
175    let defmt_macro: TokenStream2 = match level {
176        CuLogLevel::Debug => quote! { ::cu29::prelude::__cu29_defmt_debug },
177        CuLogLevel::Info => quote! { ::cu29::prelude::__cu29_defmt_info  },
178        CuLogLevel::Warning => quote! { ::cu29::prelude::__cu29_defmt_warn  },
179        CuLogLevel::Error => quote! { ::cu29::prelude::__cu29_defmt_error },
180        CuLogLevel::Critical => quote! { ::cu29::prelude::__cu29_defmt_error },
181    };
182
183    #[cfg(debug_assertions)]
184    let maybe_inject_defmt: Option<TokenStream2> = if STD {
185        None // defmt never exist in std mode ...
186    } else {
187        Some(quote! {
188             #[cfg(debug_assertions)]
189             {
190                 #defmt_macro!(#defmt_fmt_lit, #(#defmt_args_unnamed_ts,)* #(#defmt_args_named_ts,)*);
191             }
192        })
193    };
194
195    #[cfg(not(debug_assertions))]
196    let maybe_inject_defmt: Option<TokenStream2> = None; // ... neither in release mode
197
198    // Emit both: defmt (conditionally) + Copper structured logging
199    quote! {{
200        let mut log_entry = CuLogEntry::new(#index, CuLogLevel::#level_ident);
201        #(#unnamed_prints)*
202        #(#named_prints)*
203
204        #maybe_inject_defmt
205
206        #log_stmt
207        #error_handling
208    }}
209    .into()
210}
211
212/// This macro is used to log a debug message with parameters.
213/// The first parameter is a string literal that represents the message to be logged.
214/// Only `{}` is supported as a placeholder for parameters.
215/// The rest of the parameters are the values to be logged.
216/// The parameters can be named or unnamed.
217/// Named parameters are specified as `name = value`.
218/// Unnamed parameters are specified as `value`.
219/// # Example
220/// ```ignore
221/// use cu29_log_derive::debug;
222/// let a = 1;
223/// let b = 2;
224/// debug!("a = {}, b = {}", my_value = a, b); // named and unnamed parameters
225/// ```
226///
227/// You can retrieve this data using the log_reader generated with your project and giving it the
228/// unified .copper log file and the string index file generated at compile time.
229///
230/// Note: In debug mode, the log will also be printed to the console. (ie slooow).
231/// In release mode, the log will be only be written to the unified logger.
232///
233/// This macro will be compiled out if the max log level is set to a level higher than Debug.
234#[cfg(any(feature = "log-level-debug", cu29_default_log_level_debug))]
235#[proc_macro]
236pub fn debug(input: TokenStream) -> TokenStream {
237    create_log_entry(input, CuLogLevel::Debug)
238}
239
240/// This macro is used to log an info message with parameters.
241/// The first parameter is a string literal that represents the message to be logged.
242/// Only `{}` is supported as a placeholder for parameters.
243/// The rest of the parameters are the values to be logged.
244///
245/// This macro will be compiled out if the max log level is set to a level higher than Info.
246#[cfg(any(feature = "log-level-debug", feature = "log-level-info",))]
247#[proc_macro]
248pub fn info(input: TokenStream) -> TokenStream {
249    create_log_entry(input, CuLogLevel::Info)
250}
251
252/// This macro is used to log a warning message with parameters.
253/// The first parameter is a string literal that represents the message to be logged.
254/// Only `{}` is supported as a placeholder for parameters.
255/// The rest of the parameters are the values to be logged.
256///
257/// This macro will be compiled out if the max log level is set to a level higher than Warning.
258#[cfg(any(
259    feature = "log-level-debug",
260    feature = "log-level-info",
261    feature = "log-level-warning",
262))]
263#[proc_macro]
264pub fn warning(input: TokenStream) -> TokenStream {
265    create_log_entry(input, CuLogLevel::Warning)
266}
267
268/// This macro is used to log an error message with parameters.
269/// The first parameter is a string literal that represents the message to be logged.
270/// Only `{}` is supported as a placeholder for parameters.
271/// The rest of the parameters are the values to be logged.
272///
273/// This macro will be compiled out if the max log level is set to a level higher than Error.
274#[cfg(any(
275    feature = "log-level-debug",
276    feature = "log-level-info",
277    feature = "log-level-warning",
278    feature = "log-level-error",
279))]
280#[proc_macro]
281pub fn error(input: TokenStream) -> TokenStream {
282    create_log_entry(input, CuLogLevel::Error)
283}
284
285/// This macro is used to log a critical message with parameters.
286/// The first parameter is a string literal that represents the message to be logged.
287/// Only `{}` is supported as a placeholder for parameters.
288/// The rest of the parameters are the values to be logged.
289///
290/// This macro is always compiled in, regardless of the max log level setting.
291#[cfg(any(
292    feature = "log-level-debug",
293    feature = "log-level-info",
294    feature = "log-level-warning",
295    feature = "log-level-error",
296    feature = "log-level-critical",
297))]
298#[proc_macro]
299pub fn critical(input: TokenStream) -> TokenStream {
300    create_log_entry(input, CuLogLevel::Critical)
301}
302
303// Provide empty implementations for macros that are compiled out
304#[cfg(not(any(feature = "log-level-debug", cu29_default_log_level_debug)))]
305#[proc_macro]
306pub fn debug(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
307    reference_unused_variables(input)
308}
309
310#[cfg(not(any(feature = "log-level-debug", feature = "log-level-info",)))]
311#[proc_macro]
312pub fn info(input: TokenStream) -> TokenStream {
313    reference_unused_variables(input)
314}
315
316#[cfg(not(any(
317    feature = "log-level-debug",
318    feature = "log-level-info",
319    feature = "log-level-warning",
320)))]
321#[proc_macro]
322pub fn warning(input: TokenStream) -> TokenStream {
323    reference_unused_variables(input)
324}
325
326#[cfg(not(any(
327    feature = "log-level-debug",
328    feature = "log-level-info",
329    feature = "log-level-warning",
330    feature = "log-level-error",
331)))]
332#[proc_macro]
333pub fn error(input: TokenStream) -> TokenStream {
334    reference_unused_variables(input)
335}
336
337#[cfg(not(any(
338    feature = "log-level-debug",
339    feature = "log-level-info",
340    feature = "log-level-warning",
341    feature = "log-level-error",
342    feature = "log-level-critical",
343)))]
344#[proc_macro]
345pub fn critical(input: TokenStream) -> TokenStream {
346    reference_unused_variables(input)
347}
348
349/// Interns a string
350/// For example:
351///
352/// let string_number: u32 = intern!("my string");
353///
354/// will store "my string" in the interned string db at compile time and return the index of the string.
355#[proc_macro]
356pub fn intern(input: TokenStream) -> TokenStream {
357    let expr = syn::parse::<Expr>(input).expect("Failed to parse input as expression");
358    let (index, _msg) = if let Expr::Lit(ExprLit {
359        lit: Lit::Str(msg), ..
360    }) = expr
361    {
362        let msg = msg.value();
363        let index = intern_string(&msg).expect("Failed to insert log string.");
364        (index, msg)
365    } else {
366        panic!("The first parameter of the argument needs to be a string literal.");
367    };
368    quote! { #index }.into()
369}