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;
8#[allow(unused)]
9use syn::Token;
10use syn::parse::Parser;
11use syn::spanned::Spanned;
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> = Some(quote! {
163        if let Err(_e) = r {
164            let _ = &_e;
165        }
166    });
167
168    #[cfg(debug_assertions)]
169    let defmt_macro: TokenStream2 = match level {
170        CuLogLevel::Debug => quote! { ::cu29::prelude::__cu29_defmt_debug },
171        CuLogLevel::Info => quote! { ::cu29::prelude::__cu29_defmt_info  },
172        CuLogLevel::Warning => quote! { ::cu29::prelude::__cu29_defmt_warn  },
173        CuLogLevel::Error => quote! { ::cu29::prelude::__cu29_defmt_error },
174        CuLogLevel::Critical => quote! { ::cu29::prelude::__cu29_defmt_error },
175    };
176
177    #[cfg(debug_assertions)]
178    let maybe_inject_defmt: Option<TokenStream2> = if STD {
179        None // defmt never exist in std mode ...
180    } else {
181        Some(quote! {
182             #[cfg(debug_assertions)]
183             {
184                 #defmt_macro!(#defmt_fmt_lit, #(#defmt_args_unnamed_ts,)* #(#defmt_args_named_ts,)*);
185             }
186        })
187    };
188
189    #[cfg(not(debug_assertions))]
190    let maybe_inject_defmt: Option<TokenStream2> = None; // ... neither in release mode
191
192    // Emit both: defmt (conditionally) + Copper structured logging
193    quote! {{
194        let mut log_entry = CuLogEntry::new(#index, CuLogLevel::#level_ident);
195        #(#unnamed_prints)*
196        #(#named_prints)*
197
198        #maybe_inject_defmt
199
200        #log_stmt
201        #error_handling
202    }}
203    .into()
204}
205
206/// This macro is used to log a debug message with parameters.
207/// The first parameter is a string literal that represents the message to be logged.
208/// Only `{}` is supported as a placeholder for parameters.
209/// The rest of the parameters are the values to be logged.
210/// The parameters can be named or unnamed.
211/// Named parameters are specified as `name = value`.
212/// Unnamed parameters are specified as `value`.
213/// # Example
214/// ```ignore
215/// use cu29_log_derive::debug;
216/// let a = 1;
217/// let b = 2;
218/// debug!("a = {}, b = {}", my_value = a, b); // named and unnamed parameters
219/// ```
220///
221/// You can retrieve this data using the log_reader generated with your project and giving it the
222/// unified .copper log file and the string index file generated at compile time.
223///
224/// Note: In debug mode, the log will also be printed to the console. (ie slooow).
225/// In release mode, the log will be only be written to the unified logger.
226///
227/// This macro will be compiled out if the max log level is set to a level higher than Debug.
228#[cfg(any(feature = "log-level-debug", cu29_default_log_level_debug))]
229#[proc_macro]
230pub fn debug(input: TokenStream) -> TokenStream {
231    create_log_entry(input, CuLogLevel::Debug)
232}
233
234/// This macro is used to log an info message with parameters.
235/// The first parameter is a string literal that represents the message to be logged.
236/// Only `{}` is supported as a placeholder for parameters.
237/// The rest of the parameters are the values to be logged.
238///
239/// This macro will be compiled out if the max log level is set to a level higher than Info.
240#[cfg(any(feature = "log-level-debug", feature = "log-level-info",))]
241#[proc_macro]
242pub fn info(input: TokenStream) -> TokenStream {
243    create_log_entry(input, CuLogLevel::Info)
244}
245
246/// This macro is used to log a warning message with parameters.
247/// The first parameter is a string literal that represents the message to be logged.
248/// Only `{}` is supported as a placeholder for parameters.
249/// The rest of the parameters are the values to be logged.
250///
251/// This macro will be compiled out if the max log level is set to a level higher than Warning.
252#[cfg(any(
253    feature = "log-level-debug",
254    feature = "log-level-info",
255    feature = "log-level-warning",
256))]
257#[proc_macro]
258pub fn warning(input: TokenStream) -> TokenStream {
259    create_log_entry(input, CuLogLevel::Warning)
260}
261
262/// This macro is used to log an error message with parameters.
263/// The first parameter is a string literal that represents the message to be logged.
264/// Only `{}` is supported as a placeholder for parameters.
265/// The rest of the parameters are the values to be logged.
266///
267/// This macro will be compiled out if the max log level is set to a level higher than Error.
268#[cfg(any(
269    feature = "log-level-debug",
270    feature = "log-level-info",
271    feature = "log-level-warning",
272    feature = "log-level-error",
273))]
274#[proc_macro]
275pub fn error(input: TokenStream) -> TokenStream {
276    create_log_entry(input, CuLogLevel::Error)
277}
278
279/// This macro is used to log a critical message with parameters.
280/// The first parameter is a string literal that represents the message to be logged.
281/// Only `{}` is supported as a placeholder for parameters.
282/// The rest of the parameters are the values to be logged.
283///
284/// This macro is always compiled in, regardless of the max log level setting.
285#[cfg(any(
286    feature = "log-level-debug",
287    feature = "log-level-info",
288    feature = "log-level-warning",
289    feature = "log-level-error",
290    feature = "log-level-critical",
291))]
292#[proc_macro]
293pub fn critical(input: TokenStream) -> TokenStream {
294    create_log_entry(input, CuLogLevel::Critical)
295}
296
297// Provide empty implementations for macros that are compiled out
298#[cfg(not(any(feature = "log-level-debug", cu29_default_log_level_debug)))]
299#[proc_macro]
300pub fn debug(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
301    reference_unused_variables(input)
302}
303
304#[cfg(not(any(feature = "log-level-debug", feature = "log-level-info",)))]
305#[proc_macro]
306pub fn info(input: TokenStream) -> TokenStream {
307    reference_unused_variables(input)
308}
309
310#[cfg(not(any(
311    feature = "log-level-debug",
312    feature = "log-level-info",
313    feature = "log-level-warning",
314)))]
315#[proc_macro]
316pub fn warning(input: TokenStream) -> TokenStream {
317    reference_unused_variables(input)
318}
319
320#[cfg(not(any(
321    feature = "log-level-debug",
322    feature = "log-level-info",
323    feature = "log-level-warning",
324    feature = "log-level-error",
325)))]
326#[proc_macro]
327pub fn error(input: TokenStream) -> TokenStream {
328    reference_unused_variables(input)
329}
330
331#[cfg(not(any(
332    feature = "log-level-debug",
333    feature = "log-level-info",
334    feature = "log-level-warning",
335    feature = "log-level-error",
336    feature = "log-level-critical",
337)))]
338#[proc_macro]
339pub fn critical(input: TokenStream) -> TokenStream {
340    reference_unused_variables(input)
341}
342
343/// Interns a string
344/// For example:
345///
346/// let string_number: u32 = intern!("my string");
347///
348/// will store "my string" in the interned string db at compile time and return the index of the string.
349#[proc_macro]
350pub fn intern(input: TokenStream) -> TokenStream {
351    let expr = syn::parse::<Expr>(input).expect("Failed to parse input as expression");
352    let (index, _msg) = if let Expr::Lit(ExprLit {
353        lit: Lit::Str(msg), ..
354    }) = expr
355    {
356        let msg = msg.value();
357        let index = intern_string(&msg).expect("Failed to insert log string.");
358        (index, msg)
359    } else {
360        panic!("The first parameter of the argument needs to be a string literal.");
361    };
362    quote! { #index }.into()
363}