cu29_log_derive/
lib.rs

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