solana_program_log_macro/
lib.rs1#![no_std]
2#![cfg_attr(docsrs, feature(doc_cfg))]
3#![allow(clippy::arithmetic_side_effects)]
4
5extern crate alloc;
6
7use {
8 alloc::{format, string::ToString, vec::Vec},
9 proc_macro::TokenStream,
10 quote::quote,
11 regex::Regex,
12 syn::{
13 parse::{Parse, ParseStream},
14 parse_macro_input, parse_str,
15 punctuated::Punctuated,
16 Error, Expr, ItemFn, LitInt, LitStr, Path, Token,
17 },
18};
19
20const DEFAULT_BUFFER_SIZE: &str = "200";
22
23const PROGRAM_LOG_PACKAGE_NAME: &str = "::solana_program_log";
26
27struct LogArgs {
29 crate_path: Path,
31
32 buffer_len: LitInt,
37
38 format_string: LitStr,
44
45 args: Punctuated<Expr, Token![,]>,
51}
52
53impl Parse for LogArgs {
54 fn parse(input: ParseStream) -> syn::Result<Self> {
55 let crate_path = if input.peek(LitStr) || input.peek(LitInt) {
57 parse_str::<Path>(PROGRAM_LOG_PACKAGE_NAME)?
58 } else {
59 let crate_path = input.parse::<Path>()?;
60 input.parse::<Token![,]>()?;
62 crate_path
63 };
64
65 let buffer_len = if input.peek(LitInt) {
67 let literal = input.parse()?;
68 input.parse::<Token![,]>()?;
70 literal
71 } else {
72 parse_str::<LitInt>(DEFAULT_BUFFER_SIZE)?
73 };
74
75 let format_string = input.parse()?;
76 let args = if input.is_empty() {
78 Punctuated::new()
79 } else {
80 input.parse::<Token![,]>()?;
81 Punctuated::parse_terminated(input)?
82 };
83
84 Ok(LogArgs {
85 crate_path,
86 buffer_len,
87 format_string,
88 args,
89 })
90 }
91}
92
93struct LogCuUsageArgs {
95 crate_path: Path,
97}
98
99impl Parse for LogCuUsageArgs {
100 fn parse(input: ParseStream) -> syn::Result<Self> {
101 if input.is_empty() {
102 return Ok(Self {
103 crate_path: parse_str::<Path>(PROGRAM_LOG_PACKAGE_NAME)?,
104 });
105 }
106
107 if input.peek(Token![crate]) {
109 input.parse::<Token![crate]>()?;
110 input.parse::<Token![=]>()?;
111 }
112
113 let crate_path = input.parse::<Path>()?;
115
116 if !input.is_empty() {
117 return Err(input.error("unexpected tokens after crate path"));
118 }
119
120 Ok(Self { crate_path })
121 }
122}
123
124#[proc_macro]
138pub fn log(input: TokenStream) -> TokenStream {
139 let LogArgs {
141 crate_path,
142 buffer_len,
143 format_string,
144 args,
145 } = parse_macro_input!(input as LogArgs);
146 let parsed_string = format_string.value();
147
148 let placeholder_regex = Regex::new(r"\{.*?\}").unwrap();
150
151 let placeholders: Vec<_> = placeholder_regex
152 .find_iter(&parsed_string)
153 .map(|m| m.as_str())
154 .collect();
155
156 if placeholders.len() != args.len() {
158 let arg_message = if args.is_empty() {
159 "but no arguments were given".to_string()
160 } else {
161 format!(
162 "but there is {} {}",
163 args.len(),
164 if args.len() == 1 {
165 "argument"
166 } else {
167 "arguments"
168 }
169 )
170 };
171
172 return Error::new_spanned(
173 format_string,
174 format!(
175 "{} positional arguments in format string, {}",
176 placeholders.len(),
177 arg_message
178 ),
179 )
180 .to_compile_error()
181 .into();
182 }
183
184 if !placeholders.is_empty() {
185 let mut replaced_parts = Vec::new();
187
188 let parts: Vec<&str> = placeholder_regex.split(&parsed_string).collect();
189 let part_iter = parts.iter();
190
191 let mut arg_iter = args.iter();
192 let mut ph_iter = placeholders.iter();
193
194 for part in part_iter {
196 if !part.is_empty() {
197 replaced_parts.push(quote! { logger.append(#part) });
198 }
199
200 if let Some(arg) = arg_iter.next() {
201 let placeholder = ph_iter.next().unwrap();
204
205 match *placeholder {
206 "{}" => {
207 replaced_parts.push(quote! { logger.append(#arg) });
208 }
209 value if value.starts_with("{:.") => {
210 let Ok(precision) = value[3..value.len() - 1].parse::<u8>() else {
211 return Error::new_spanned(
212 format_string,
213 format!("invalid precision format: {value}"),
214 )
215 .to_compile_error()
216 .into();
217 };
218
219 replaced_parts.push(quote! {
220 logger.append_with_args(
221 #arg,
222 &[#crate_path::logger::Argument::Precision(#precision)]
223 )
224 });
225 }
226 value if value.starts_with("{:<.") || value.starts_with("{:>.") => {
227 let Ok(size) = value[4..value.len() - 1].parse::<usize>() else {
228 return Error::new_spanned(
229 format_string,
230 format!("invalid truncate size format: {value}"),
231 )
232 .to_compile_error()
233 .into();
234 };
235
236 match value.chars().nth(2) {
237 Some('<') => {
238 replaced_parts.push(quote! {
239 logger.append_with_args(
240 #arg,
241 &[#crate_path::logger::Argument::TruncateStart(#size)]
242 )
243 });
244 }
245 Some('>') => {
246 replaced_parts.push(quote! {
247 logger.append_with_args(
248 #arg,
249 &[#crate_path::logger::Argument::TruncateEnd(#size)]
250 )
251 });
252 }
253 _ => {
254 return Error::new_spanned(
256 format_string,
257 format!("invalid truncate format: {value}"),
258 )
259 .to_compile_error()
260 .into();
261 }
262 }
263 }
264 _ => {
265 return Error::new_spanned(
266 format_string,
267 format!("invalid placeholder: {placeholder}"),
268 )
269 .to_compile_error()
270 .into();
271 }
272 }
273 }
274 }
275
276 TokenStream::from(quote! {
278 {
279 let mut logger = #crate_path::logger::Logger::<#buffer_len>::default();
280 #(#replaced_parts;)*
281 logger.log();
282 }
283 })
284 } else {
285 TokenStream::from(quote! {
286 {
287 #crate_path::logger::log_message(#format_string.as_bytes());
288 }
289 })
290 }
291}
292
293#[proc_macro_attribute]
326pub fn log_cu_usage(attr: TokenStream, item: TokenStream) -> TokenStream {
327 let crate_path = parse_macro_input!(attr as LogCuUsageArgs).crate_path;
328 let mut input = parse_macro_input!(item as ItemFn);
329 let fn_name = &input.sig.ident;
330 let block = &input.block;
331
332 input.block = syn::parse_quote!({
333 let cu_before = unsafe { #crate_path::logger::remaining_compute_units() };
334
335 let __result = (|| #block)();
336
337 let cu_after = unsafe { #crate_path::logger::remaining_compute_units() };
338 let introspection_cost = 102;
340
341 let consumed = cu_before - cu_after - introspection_cost;
342
343 #crate_path::log!("Function {} consumed {} compute units", stringify!(#fn_name), consumed);
344
345 __result
346 });
347
348 quote!(#input).into()
349}
350
351#[cfg(test)]
352mod tests {
353 use {
354 super::{LogArgs, LogCuUsageArgs},
355 syn::{parse_quote, parse_str, Path},
356 };
357
358 #[test]
359 fn log_default_crate_path() {
360 let args = parse_str::<LogArgs>("\"a simple log\"").unwrap();
361 let expected: Path = parse_quote!(::solana_program_log);
362 assert_eq!(args.crate_path, expected);
363 }
364
365 #[test]
366 fn log_default_crate_path_with_buffer_len() {
367 let args = parse_str::<LogArgs>("500, \"a simple log\"").unwrap();
368 let expected: Path = parse_quote!(::solana_program_log);
369 assert_eq!(args.crate_path, expected);
370 assert_eq!(args.buffer_len.base10_digits(), "500");
371 }
372
373 #[test]
374 fn log_with_crate_path() {
375 let args = parse_str::<LogArgs>("mylog, \"a simple log\"").unwrap();
376 let expected: Path = parse_quote!(mylog);
377 assert_eq!(args.crate_path, expected);
378 }
379
380 #[test]
381 fn log_with_crate_path_and_buffer_len() {
382 let args = parse_str::<LogArgs>("mylog, 500, \"a simple log\"").unwrap();
383 let expected: Path = parse_quote!(mylog);
384 assert_eq!(args.crate_path, expected);
385 assert_eq!(args.buffer_len.base10_digits(), "500");
386 }
387
388 #[test]
389 fn log_cu_usage() {
390 let args = parse_str::<LogCuUsageArgs>("").unwrap();
391 let expected: Path = parse_quote!(::solana_program_log);
392 assert_eq!(args.crate_path, expected);
393 }
394
395 #[test]
396 fn log_cu_usage_args_support_standalone_path() {
397 let args = parse_str::<LogCuUsageArgs>("mylog").unwrap();
398 let expected: Path = parse_quote!(mylog);
399 assert_eq!(args.crate_path, expected);
400 }
401
402 #[test]
403 fn log_cu_usage_args_support_crate_equals_path() {
404 let args = parse_str::<LogCuUsageArgs>("crate = another_log").unwrap();
405 let expected: Path = parse_quote!(another_log);
406 assert_eq!(args.crate_path, expected);
407 }
408}