Skip to main content

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 = match parser.parse(input) {
66        Ok(exprs) => exprs,
67        Err(err) => return err.to_compile_error().into(),
68    };
69    let mut exprs_iter = exprs.iter();
70
71    #[cfg(not(feature = "std"))]
72    const STD: bool = false;
73    #[cfg(feature = "std")]
74    const STD: bool = true;
75
76    let msg_expr = match exprs_iter.next() {
77        Some(expr) => expr,
78        None => {
79            return syn::Error::new(
80                proc_macro2::Span::call_site(),
81                "Expected at least one expression",
82            )
83            .to_compile_error()
84            .into();
85        }
86    };
87    let (index, msg_str) = if let Expr::Lit(ExprLit {
88        lit: Lit::Str(msg), ..
89    }) = msg_expr
90    {
91        let s = msg.value();
92        let index = match intern_string(&s) {
93            Some(index) => index,
94            None => {
95                return syn::Error::new_spanned(msg_expr, "Failed to intern log string.")
96                    .to_compile_error()
97                    .into();
98            }
99        };
100        (index, s)
101    } else {
102        return syn::Error::new_spanned(
103            msg_expr,
104            "The first parameter of the argument needs to be a string literal.",
105        )
106        .to_compile_error()
107        .into();
108    };
109
110    let level_ident = match level {
111        CuLogLevel::Debug => quote! { Debug },
112        CuLogLevel::Info => quote! { Info },
113        CuLogLevel::Warning => quote! { Warning },
114        CuLogLevel::Error => quote! { Error },
115        CuLogLevel::Critical => quote! { Critical },
116    };
117
118    // Partition unnamed vs named args (a = b treated as named)
119    let mut unnamed_params = Vec::<&Expr>::new();
120    let mut named_params = Vec::<(&Expr, &Expr)>::new();
121
122    for expr in exprs_iter {
123        if let Expr::Assign(ExprAssign { left, right, .. }) = expr {
124            named_params.push((left, right));
125        } else {
126            unnamed_params.push(expr);
127        }
128    }
129
130    // Build the CuLogEntry population tokens
131    let unnamed_prints = unnamed_params.iter().map(|value| {
132        quote! {
133            let param = to_value(#value).expect("Failed to convert a parameter to a Value");
134            log_entry.add_param(ANONYMOUS, param);
135        }
136    });
137
138    let mut named_prints = Vec::with_capacity(named_params.len());
139    for (name, value) in &named_params {
140        let name_str = quote!(#name).to_string();
141        let idx = match intern_string(&name_str) {
142            Some(idx) => idx,
143            None => {
144                return syn::Error::new_spanned(name, "Failed to intern log parameter name.")
145                    .to_compile_error()
146                    .into();
147            }
148        };
149        named_prints.push(quote! {
150            let param = to_value(#value).expect("Failed to convert a parameter to a Value");
151            log_entry.add_param(#idx, param);
152        });
153    }
154
155    // ---------- For baremetal: build a defmt format literal and arg list ----------
156    // defmt line: "<msg> | a={:?}, b={:?}, arg0={:?} ..."
157    #[cfg(feature = "textlogs")]
158    let (defmt_fmt_lit, defmt_args_unnamed_ts, defmt_args_named_ts, defmt_available) = {
159        let defmt_fmt_lit = {
160            let mut s = msg_str.clone();
161            if !named_params.is_empty() {
162                s.push_str(" |");
163            }
164            for (name, _) in named_params.iter() {
165                let name_str = quote!(#name).to_string();
166                s.push(' ');
167                s.push_str(&name_str);
168                s.push_str("={:?}");
169            }
170            syn::LitStr::new(&s, msg_expr.span())
171        };
172
173        let defmt_args_unnamed_ts: Vec<TokenStream2> =
174            unnamed_params.iter().map(|e| quote! { #e }).collect();
175        let defmt_args_named_ts: Vec<TokenStream2> = named_params
176            .iter()
177            .map(|(_, rhs)| quote! { #rhs })
178            .collect();
179
180        let defmt_available = crate_name("defmt").is_ok();
181
182        (
183            defmt_fmt_lit,
184            defmt_args_unnamed_ts,
185            defmt_args_named_ts,
186            defmt_available,
187        )
188    };
189
190    #[cfg(feature = "textlogs")]
191    fn defmt_macro_path(level: CuLogLevel) -> TokenStream2 {
192        let macro_ident = match level {
193            CuLogLevel::Debug => quote! { defmt_debug },
194            CuLogLevel::Info => quote! { defmt_info },
195            CuLogLevel::Warning => quote! { defmt_warn },
196            CuLogLevel::Error => quote! { defmt_error },
197            CuLogLevel::Critical => quote! { defmt_error },
198        };
199
200        let (base, use_prelude) = match crate_name("cu29-log") {
201            Ok(FoundCrate::Name(name)) => {
202                let ident = proc_macro2::Ident::new(&name, Span::call_site());
203                (quote! { ::#ident }, false)
204            }
205            Ok(FoundCrate::Itself) => (quote! { crate }, false),
206            Err(_) => match crate_name("cu29") {
207                Ok(FoundCrate::Name(name)) => {
208                    let ident = proc_macro2::Ident::new(&name, Span::call_site());
209                    (quote! { ::#ident }, true)
210                }
211                Ok(FoundCrate::Itself) => (quote! { crate }, true),
212                Err(_) => (quote! { ::cu29_log }, false),
213            },
214        };
215
216        if use_prelude {
217            quote! { #base::prelude::#macro_ident }
218        } else {
219            quote! { #base::#macro_ident }
220        }
221    }
222
223    // Runtime logging path (unchanged)
224    #[cfg(not(debug_assertions))]
225    let log_stmt = quote! { let r = log(&mut log_entry); };
226
227    #[cfg(debug_assertions)]
228    let log_stmt: TokenStream2 = {
229        let keys: Vec<_> = named_params
230            .iter()
231            .map(|(name, _)| {
232                let name_str = quote!(#name).to_string();
233                syn::LitStr::new(&name_str, name.span())
234            })
235            .collect();
236        quote! {
237            let r = log_debug_mode(&mut log_entry, #msg_str, &[#(#keys),*]);
238        }
239    };
240
241    let error_handling: Option<TokenStream2> = Some(quote! {
242        if let Err(_e) = r {
243            let _ = &_e;
244        }
245    });
246
247    #[cfg(feature = "textlogs")]
248    let defmt_macro: TokenStream2 = defmt_macro_path(level);
249
250    #[cfg(feature = "textlogs")]
251    let maybe_inject_defmt: Option<TokenStream2> = if STD || !defmt_available {
252        None // defmt is only emitted in no-std builds.
253    } else {
254        Some(quote! {
255            #defmt_macro!(#defmt_fmt_lit, #(#defmt_args_unnamed_ts,)* #(#defmt_args_named_ts,)*);
256        })
257    };
258
259    #[cfg(not(feature = "textlogs"))]
260    let maybe_inject_defmt: Option<TokenStream2> = None; // defmt emission disabled
261
262    // Emit both: defmt (conditionally) + Copper structured logging
263    quote! {{
264        let mut log_entry = CuLogEntry::new(#index, CuLogLevel::#level_ident);
265        #(#unnamed_prints)*
266        #(#named_prints)*
267
268        #maybe_inject_defmt
269
270        #log_stmt
271        #error_handling
272    }}
273    .into()
274}
275
276/// This macro is used to log a debug message with parameters.
277/// The first parameter is a string literal that represents the message to be logged.
278/// Only `{}` is supported as a placeholder for parameters.
279/// The rest of the parameters are the values to be logged.
280/// The parameters can be named or unnamed.
281/// Named parameters are specified as `name = value`.
282/// Unnamed parameters are specified as `value`.
283/// # Example
284/// ```ignore
285/// use cu29_log_derive::debug;
286/// let a = 1;
287/// let b = 2;
288/// debug!("a = {}, b = {}", my_value = a, b); // named and unnamed parameters
289/// ```
290///
291/// You can retrieve this data using the log_reader generated with your project and giving it the
292/// unified .copper log file and the string index file generated at compile time.
293///
294/// Note: In debug mode, the log will also be printed to the console. (ie slooow).
295/// In release mode, the log will be only be written to the unified logger.
296///
297/// This macro will be compiled out if the max log level is set to a level higher than Debug.
298#[cfg(any(feature = "log-level-debug", cu29_default_log_level_debug))]
299#[proc_macro]
300pub fn debug(input: TokenStream) -> TokenStream {
301    create_log_entry(input, CuLogLevel::Debug)
302}
303
304/// This macro is used to log an info message with parameters.
305/// The first parameter is a string literal that represents the message to be logged.
306/// Only `{}` is supported as a placeholder for parameters.
307/// The rest of the parameters are the values to be logged.
308///
309/// This macro will be compiled out if the max log level is set to a level higher than Info.
310#[cfg(any(feature = "log-level-debug", feature = "log-level-info",))]
311#[proc_macro]
312pub fn info(input: TokenStream) -> TokenStream {
313    create_log_entry(input, CuLogLevel::Info)
314}
315
316/// This macro is used to log a warning 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 will be compiled out if the max log level is set to a level higher than Warning.
322#[cfg(any(
323    feature = "log-level-debug",
324    feature = "log-level-info",
325    feature = "log-level-warning",
326))]
327#[proc_macro]
328pub fn warning(input: TokenStream) -> TokenStream {
329    create_log_entry(input, CuLogLevel::Warning)
330}
331
332/// This macro is used to log an error message with parameters.
333/// The first parameter is a string literal that represents the message to be logged.
334/// Only `{}` is supported as a placeholder for parameters.
335/// The rest of the parameters are the values to be logged.
336///
337/// This macro will be compiled out if the max log level is set to a level higher than Error.
338#[cfg(any(
339    feature = "log-level-debug",
340    feature = "log-level-info",
341    feature = "log-level-warning",
342    feature = "log-level-error",
343))]
344#[proc_macro]
345pub fn error(input: TokenStream) -> TokenStream {
346    create_log_entry(input, CuLogLevel::Error)
347}
348
349/// This macro is used to log a critical message with parameters.
350/// The first parameter is a string literal that represents the message to be logged.
351/// Only `{}` is supported as a placeholder for parameters.
352/// The rest of the parameters are the values to be logged.
353///
354/// This macro is always compiled in, regardless of the max log level setting.
355#[cfg(any(
356    feature = "log-level-debug",
357    feature = "log-level-info",
358    feature = "log-level-warning",
359    feature = "log-level-error",
360    feature = "log-level-critical",
361))]
362#[proc_macro]
363pub fn critical(input: TokenStream) -> TokenStream {
364    create_log_entry(input, CuLogLevel::Critical)
365}
366
367// Provide empty implementations for macros that are compiled out
368#[cfg(not(any(feature = "log-level-debug", cu29_default_log_level_debug)))]
369#[proc_macro]
370pub fn debug(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
371    reference_unused_variables(input)
372}
373
374#[cfg(not(any(feature = "log-level-debug", feature = "log-level-info",)))]
375#[proc_macro]
376pub fn info(input: TokenStream) -> TokenStream {
377    reference_unused_variables(input)
378}
379
380#[cfg(not(any(
381    feature = "log-level-debug",
382    feature = "log-level-info",
383    feature = "log-level-warning",
384)))]
385#[proc_macro]
386pub fn warning(input: TokenStream) -> TokenStream {
387    reference_unused_variables(input)
388}
389
390#[cfg(not(any(
391    feature = "log-level-debug",
392    feature = "log-level-info",
393    feature = "log-level-warning",
394    feature = "log-level-error",
395)))]
396#[proc_macro]
397pub fn error(input: TokenStream) -> TokenStream {
398    reference_unused_variables(input)
399}
400
401#[cfg(not(any(
402    feature = "log-level-debug",
403    feature = "log-level-info",
404    feature = "log-level-warning",
405    feature = "log-level-error",
406    feature = "log-level-critical",
407)))]
408#[proc_macro]
409pub fn critical(input: TokenStream) -> TokenStream {
410    reference_unused_variables(input)
411}
412
413/// Interns a string
414/// For example:
415///
416/// let string_number: u32 = intern!("my string");
417///
418/// will store "my string" in the interned string db at compile time and return the index of the string.
419#[proc_macro]
420pub fn intern(input: TokenStream) -> TokenStream {
421    let expr = match syn::parse::<Expr>(input) {
422        Ok(expr) => expr,
423        Err(err) => return err.to_compile_error().into(),
424    };
425    let index = if let Expr::Lit(ExprLit {
426        lit: Lit::Str(msg), ..
427    }) = &expr
428    {
429        let msg = msg.value();
430        match intern_string(&msg) {
431            Some(index) => index,
432            None => {
433                return syn::Error::new_spanned(&expr, "Failed to intern log string.")
434                    .to_compile_error()
435                    .into();
436            }
437        }
438    } else {
439        return syn::Error::new_spanned(
440            &expr,
441            "The first parameter of the argument needs to be a string literal.",
442        )
443        .to_compile_error()
444        .into();
445    };
446    quote! { #index }.into()
447}