diff --git a/Cargo.lock b/Cargo.lock
index 70bd1de..6b3cd06 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -21,6 +21,21 @@ checksum = "9b2a72055cd9cffc40c9f75f1e5810c80559e158796cf2202292ce4745889588"
name = "pinocchio"
version = "0.6.0"
+[[package]]
+name = "pinocchio-log"
+version = "0.1.0"
+dependencies = [
+ "pinocchio-log-macro",
+]
+
+[[package]]
+name = "pinocchio-log-macro"
+version = "0.1.0"
+dependencies = [
+ "quote",
+ "syn",
+]
+
[[package]]
name = "pinocchio-pubkey"
version = "0.2.1"
@@ -37,6 +52,41 @@ dependencies = [
"pinocchio-pubkey",
]
+[[package]]
+name = "proc-macro2"
+version = "1.0.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
+
[[package]]
name = "pinocchio-token"
version = "0.2.0"
diff --git a/Cargo.toml b/Cargo.toml
index e7c7548..9709685 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,13 @@
[workspace]
resolver = "2"
-members = ["programs/system", "programs/token", "sdk/pinocchio", "sdk/pubkey"]
+members = [
+ "programs/system",
+ "programs/token",
+ "sdk/log/crate",
+ "sdk/log/macro",
+ "sdk/pinocchio",
+ "sdk/pubkey",
+]
[workspace.package]
edition = "2021"
diff --git a/sdk/log/crate/Cargo.toml b/sdk/log/crate/Cargo.toml
new file mode 100644
index 0000000..5a3dc27
--- /dev/null
+++ b/sdk/log/crate/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "pinocchio-log"
+description = "Lightweight log utility for Solana programs"
+version = "0.1.0"
+edition = { workspace = true }
+license = { workspace = true }
+readme = "./README.md"
+repository = { workspace = true }
+
+[dependencies]
+pinocchio-log-macro = { version = "^0", path = "../macro", optional = true }
+
+[lints.rust]
+unexpected_cfgs = {level = "warn", check-cfg = ['cfg(target_os, values("solana"))'] }
+
+[features]
+default = ["macro"]
+macro = ["dep:pinocchio-log-macro"]
diff --git a/sdk/log/crate/README.md b/sdk/log/crate/README.md
new file mode 100644
index 0000000..9428a36
--- /dev/null
+++ b/sdk/log/crate/README.md
@@ -0,0 +1,79 @@
+#
+
+Lightweight log utility for Solana programs.
+
+## Overview
+
+Currently, logging messages that require formatting are a bit heavy on the CU consumption. There are two aspects when comes to determining the cost of a log message:
+
+1. `base cost`: this is the cost of the log syscall. It will either be the [`syscall_base_cost`](https://github.com/anza-xyz/agave/blob/master/compute-budget/src/compute_budget.rs#L167) (currently `100` CU) or a number of CUs equal to the length of the message, whichever value is higher.
+
+2. `formatting cost`: the compute units required to format the message. This is variable and depends on the number and type of the arguments. Formatting is performed using Rust built-in `format!` routines, which in turn use `format_args!`.
+
+It is known that Rust formatting routines are CPU-intensive for constrained environments. This has been noted on both the `solana-program` [`msg!`](https://docs.rs/solana-program/latest/solana_program/macro.msg.html) documentation and more generally on [rust development](https://github.com/rust-lang/rust/issues/99012).
+
+While the cost related to (1) is *fixed*, in the sense that it does not change with the addition of formatting, it is possible to improve the overall cost of logging a formatted message using a lightweight formatting routine — this is what this crate does.
+
+This crate defines a lightweight `Logger` type to format log messages and a companion `log!` macro. The logger is a fixed size buffer that can be used to format log messages before sending them to the log output. Any type that implements the `Log` trait can be appended to the logger.
+
+Below is a sample of the improvements observed when formatting log messages, measured in terms of compute units (CU):
+| Ouput message | `log!` | `msg!` | Improvement (%) |
+|------------------------------------|--------|--------------|-----------------|
+| `"Hello world!"` | 103 | 103 | - |
+| `"lamports={}"` + `u64` | 374 | 627 (+253) | 40% |
+| `"{}"` + `[&str; 2]` | 384 | 1648 (+1264) | 76% |
+| `"{}"` + `[u64; 2]` | 601 | 1060 (+459) | 44% |
+| `"lamports={}"` + `i64` | 389 | 660 (+271) | 41% |
+| `"{}"` + `[u8; 32]` (pubkey bytes) | 3147 | 8401 (+5254) | 62% |
+
+> Note: The improvement in CU is accumulative, meaning that if you are logging multiple `u64` values, there will be a 40% improvement per formatted `u64` value.
+
+## Features
+
+* Zero dependencies and `no_std` crate
+* Independent of SDK (i.e., works with `pinocchio`, `solana-program` or `anchor`)
+* Support for `&str`, unsigned and signed integer types
+* `log!` macro to facilitate log message formatting
+
+## Getting Started
+
+From your project folder:
+```bash
+cargo add pinocchio-log
+```
+
+## Usage
+
+The `Logger` can be used directly:
+```rust
+use pinocchio_log::logger::Logger;
+
+let mut logger = Logger::<100>::default();
+logger.append("Hello ");
+logger.append("world!");
+logger.log();
+```
+
+ or via the `log!` macro:
+ ```rust
+use pinocchio_log::log
+
+let amount = 1_000_000_000;
+log!("transfer amount: {}", amount);
+```
+
+Since the formatting routine does not perform additional allocations, the `Logger` type has a fixed size specified on its creation. When using the `log!` macro, it is also possible to specify the size of the logger buffer:
+
+```rust
+use pinocchio_log::log
+
+let amount = 1_000_000_000;
+log!(100, "transfer amount: {}", amount);
+```
+## Limitations
+
+Currently the `log!` macro does not offer extra formatting options apart from the placeholder "`{}`" for argument values.
+
+## License
+
+The code is licensed under the [Apache License Version 2.0](LICENSE)
diff --git a/sdk/log/crate/src/lib.rs b/sdk/log/crate/src/lib.rs
new file mode 100644
index 0000000..ad2e6fe
--- /dev/null
+++ b/sdk/log/crate/src/lib.rs
@@ -0,0 +1,113 @@
+//! Lightweight log utility for Solana programs.
+//!
+//! This crate provides a `Logger` struct that can be used to efficiently log messages
+//! in a Solana program. The `Logger` struct is a wrapper around a fixed-size buffer,
+//! where types that implement the `Log` trait can be appended to the buffer.
+//!
+//! The `Logger` struct is generic over the size of the buffer, and the buffer size
+//! should be chosen based on the expected size of the log messages. When the buffer is
+//! full, the log message will be truncated. This is represented by the `@` character
+//! at the end of the log message.
+//!
+//! # Example
+//!
+//! Creating a `Logger` with a buffer size of 100 bytes, and appending a string and an
+//! `u64` value:
+//!
+//! ```
+//! use pinocchio_log::logger::Logger;
+//!
+//! let mut logger = Logger::<100>::default();
+//! logger.append("balance=");
+//! logger.append(1_000_000_000);
+//! logger.log();
+//!
+//! // Clear the logger buffer.
+//! logger.clear();
+//!
+//! logger.append(&["Hello ", "world!"]);
+//! logger.log();
+//! ```
+
+#![no_std]
+
+pub mod logger;
+
+#[cfg(feature = "macro")]
+pub use pinocchio_log_macro::*;
+
+#[cfg(test)]
+mod tests {
+ use crate::logger::Logger;
+
+ #[test]
+ fn test_logger() {
+ let mut logger = Logger::<100>::default();
+ logger.append("Hello ");
+ logger.append("world!");
+
+ assert!(&*logger == "Hello world!".as_bytes());
+
+ logger.clear();
+
+ logger.append("balance=");
+ logger.append(1_000_000_000);
+
+ assert!(&*logger == "balance=1000000000".as_bytes());
+ }
+
+ #[test]
+ fn test_logger_trucated() {
+ let mut logger = Logger::<8>::default();
+ logger.append("Hello ");
+ logger.append("world!");
+
+ assert!(&*logger == "Hello w@".as_bytes());
+
+ let mut logger = Logger::<12>::default();
+
+ logger.append("balance=");
+ logger.append(1_000_000_000);
+
+ assert!(&*logger == "balance=100@".as_bytes());
+ }
+
+ #[test]
+ fn test_logger_slice() {
+ let mut logger = Logger::<20>::default();
+ logger.append(&["Hello ", "world!"]);
+
+ assert!(&*logger == "[\"Hello \", \"world!\"]".as_bytes());
+
+ let mut logger = Logger::<20>::default();
+ logger.append(&[123, 456]);
+
+ assert!(&*logger == "[123, 456]".as_bytes());
+ }
+
+ #[test]
+ fn test_logger_truncated_slice() {
+ let mut logger = Logger::<5>::default();
+ logger.append(&["Hello ", "world!"]);
+
+ assert!(&*logger == "[\"He@".as_bytes());
+
+ let mut logger = Logger::<4>::default();
+ logger.append(&[123, 456]);
+
+ assert!(&*logger == "[12@".as_bytes());
+ }
+
+ #[test]
+ fn test_logger_signed() {
+ let mut logger = Logger::<2>::default();
+ logger.append(-2);
+
+ assert!(&*logger == "-2".as_bytes());
+
+ let mut logger = Logger::<5>::default();
+ logger.append(-200_000_000);
+
+ assert!(&*logger == "-200@".as_bytes());
+ }
+}
diff --git a/sdk/log/crate/src/logger.rs b/sdk/log/crate/src/logger.rs
new file mode 100644
index 0000000..58e1a05
--- /dev/null
+++ b/sdk/log/crate/src/logger.rs
@@ -0,0 +1,424 @@
+use core::{mem::MaybeUninit, ops::Deref, slice::from_raw_parts};
+
+#[cfg(target_os = "solana")]
+extern "C" {
+ pub fn sol_log_(message: *const u8, len: u64);
+
+ pub fn sol_memcpy_(dst: *mut u8, src: *const u8, n: u64);
+}
+
+#[cfg(not(target_os = "solana"))]
+extern crate std;
+
+/// Byte representation of the digits [0, 9].
+const DIGITS: [u8; 10] = [b'0', b'1', b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9'];
+
+/// Byte represeting a truncated log.
+const TRUCATED: u8 = b'@';
+
+/// An uninitialized byte.
+const UNINIT_BYTE: MaybeUninit = MaybeUninit::uninit();
+
+/// Logger to efficiently format log messages.
+///
+/// The logger is a fixed size buffer that can be used to format log messages
+/// before sending them to the log output. Any type that implements the `Log`
+/// trait can be appended to the logger.
+pub struct Logger {
+ // Byte buffer to store the log message.
+ buffer: [MaybeUninit; BUFFER],
+
+ // Remaining space in the buffer.
+ offset: usize,
+}
+
+impl Default for Logger {
+ fn default() -> Self {
+ Self {
+ buffer: [UNINIT_BYTE; BUFFER],
+ offset: 0,
+ }
+ }
+}
+
+impl Deref for Logger {
+ type Target = [u8];
+
+ fn deref(&self) -> &Self::Target {
+ unsafe { from_raw_parts(self.buffer.as_ptr() as *const _, self.offset) }
+ }
+}
+
+impl Logger {
+ /// Append a value to the logger.
+ #[inline(always)]
+ pub fn append(&mut self, value: T) {
+ if self.is_full() {
+ if BUFFER > 0 {
+ unsafe {
+ let last = self.buffer.get_unchecked_mut(BUFFER - 1);
+ last.write(TRUCATED);
+ }
+ }
+ } else {
+ self.offset += value.write(&mut self.buffer[self.offset..]);
+ }
+ }
+
+ /// Log the message in the buffer.
+ #[inline(always)]
+ pub fn log(&self) {
+ log_message(self);
+ }
+
+ /// Clear the buffer.
+ #[inline(always)]
+ pub fn clear(&mut self) {
+ self.offset = 0;
+ }
+
+ /// Check if the buffer is empty.
+ #[inline(always)]
+ pub fn is_empty(&self) -> bool {
+ self.offset == 0
+ }
+
+ /// Check if the buffer is full.
+ #[inline(always)]
+ pub fn is_full(&self) -> bool {
+ self.offset == BUFFER
+ }
+
+ /// Get the length of the buffer.
+ #[inline(always)]
+ pub fn len(&self) -> usize {
+ self.offset
+ }
+
+ /// Get the remaining space in the buffer.
+ #[inline(always)]
+ pub fn remaining(&self) -> usize {
+ BUFFER - self.offset
+ }
+}
+
+/// Log a message.
+#[inline(always)]
+pub fn log_message(message: &[u8]) {
+ #[cfg(target_os = "solana")]
+ unsafe {
+ sol_log_(message.as_ptr(), message.len() as u64);
+ }
+ #[cfg(not(target_os = "solana"))]
+ {
+ let message = core::str::from_utf8(message).unwrap();
+ std::println!("{}", message);
+ }
+}
+
+/// Trait to specify the log behavior for a type.
+pub trait Log {
+ fn debug(&self, buffer: &mut [MaybeUninit]) -> usize {
+ self.write(buffer)
+ }
+
+ fn write(&self, buffer: &mut [MaybeUninit]) -> usize;
+}
+
+/// Implement the log trait for unsigned integer types.
+macro_rules! impl_log_for_unsigned_integer {
+ ( $type:tt, $max_digits:literal ) => {
+ impl Log for $type {
+ fn write(&self, buffer: &mut [MaybeUninit]) -> usize {
+ if buffer.is_empty() {
+ return 0;
+ }
+
+ match *self {
+ // Handle zero as a special case.
+ 0 => {
+ unsafe {
+ buffer.get_unchecked_mut(0).write(*DIGITS.get_unchecked(0));
+ }
+ 1
+ }
+ mut value => {
+ let mut digits = [UNINIT_BYTE; $max_digits];
+ let mut offset = $max_digits;
+
+ while value > 0 {
+ let remainder = value % 10;
+ value /= 10;
+ offset -= 1;
+
+ unsafe {
+ digits
+ .get_unchecked_mut(offset)
+ .write(*DIGITS.get_unchecked(remainder as usize));
+ }
+ }
+
+ // Number of available digits to write.
+ let available = $max_digits - offset;
+ // Size of the buffer.
+ let length = buffer.len();
+
+ // Determines if the value was truncated or not by calculating the
+ // number of digits that can be written.
+ let (overflow, written) = if available <= length {
+ (false, available)
+ } else {
+ (true, length)
+ };
+
+ unsafe {
+ let ptr = buffer.as_mut_ptr();
+ #[cfg(target_os = "solana")]
+ sol_memcpy_(
+ ptr as *mut _,
+ digits[offset..].as_ptr() as *const _,
+ length as u64,
+ );
+ #[cfg(not(target_os = "solana"))]
+ core::ptr::copy_nonoverlapping(digits[offset..].as_ptr(), ptr, written);
+ }
+
+ // There might not have been space for all the value.
+ if overflow {
+ unsafe {
+ let last = buffer.get_unchecked_mut(written - 1);
+ last.write(TRUCATED);
+ }
+ }
+
+ written
+ }
+ }
+ }
+ }
+ };
+}
+
+// Supported unsigned integer types.
+impl_log_for_unsigned_integer!(u8, 3);
+impl_log_for_unsigned_integer!(u16, 5);
+impl_log_for_unsigned_integer!(u32, 10);
+impl_log_for_unsigned_integer!(u64, 20);
+impl_log_for_unsigned_integer!(u128, 39);
+
+/// Implement the log trait for the signed integer types.
+macro_rules! impl_log_for_signed {
+ ( $type:tt, $max_digits:literal ) => {
+ impl Log for $type {
+ fn write(&self, buffer: &mut [MaybeUninit]) -> usize {
+ if buffer.is_empty() {
+ return 0;
+ }
+
+ match *self {
+ // Handle zero as a special case.
+ 0 => {
+ unsafe {
+ buffer.get_unchecked_mut(0).write(*DIGITS.get_unchecked(0));
+ }
+ 1
+ }
+ mut value => {
+ let mut delta = 0;
+
+ if *self < 0 {
+ unsafe {
+ buffer.get_unchecked_mut(0).write(b'-');
+ }
+ delta += 1;
+ value = -value
+ };
+
+ let mut digits = [UNINIT_BYTE; $max_digits];
+ let mut offset = $max_digits;
+
+ while value > 0 {
+ let remainder = value % 10;
+ value /= 10;
+ offset -= 1;
+
+ unsafe {
+ digits
+ .get_unchecked_mut(offset)
+ .write(*DIGITS.get_unchecked(remainder as usize));
+ }
+ }
+
+ // Number of available digits to write.
+ let available = $max_digits - offset;
+ // Size of the buffer.
+ let length = buffer.len() - delta;
+
+ // Determines if the value was truncated or not by calculating the
+ // number of digits that can be written.
+ let (overflow, written) = if available <= length {
+ (false, available)
+ } else {
+ (true, length)
+ };
+
+ unsafe {
+ let ptr = buffer[delta..].as_mut_ptr();
+ #[cfg(target_os = "solana")]
+ sol_memcpy_(
+ ptr as *mut _,
+ digits[offset..].as_ptr() as *const _,
+ length as u64,
+ );
+ #[cfg(not(target_os = "solana"))]
+ core::ptr::copy_nonoverlapping(digits[offset..].as_ptr(), ptr, written);
+ }
+
+ // There might not have been space for all the value.
+ if overflow {
+ unsafe {
+ let last = buffer.get_unchecked_mut(written + delta - 1);
+ last.write(TRUCATED);
+ }
+ }
+
+ written + delta
+ }
+ }
+ }
+ }
+ };
+}
+
+// Supported signed integer types.
+impl_log_for_signed!(i8, 3);
+impl_log_for_signed!(i16, 5);
+impl_log_for_signed!(i32, 10);
+impl_log_for_signed!(i64, 19);
+impl_log_for_signed!(i128, 39);
+
+/// Implement the log trait for the &str type.
+impl Log for &str {
+ fn debug(&self, buffer: &mut [MaybeUninit]) -> usize {
+ if buffer.is_empty() {
+ return 0;
+ }
+
+ unsafe {
+ buffer.get_unchecked_mut(0).write(b'"');
+ }
+
+ let mut offset = 1;
+ offset += self.write(&mut buffer[offset..]);
+
+ match buffer.len() - offset {
+ 0 => unsafe {
+ buffer.get_unchecked_mut(offset - 1).write(TRUCATED);
+ },
+ _ => {
+ unsafe {
+ buffer.get_unchecked_mut(offset).write(b'"');
+ }
+ offset += 1;
+ }
+ }
+
+ offset
+ }
+
+ fn write(&self, buffer: &mut [MaybeUninit]) -> usize {
+ let length = core::cmp::min(buffer.len(), self.len());
+ let offset = &mut buffer[..length];
+
+ for (d, s) in offset.iter_mut().zip(self.bytes()) {
+ d.write(s);
+ }
+
+ // There might not have been space for all the value.
+ if length != self.len() {
+ unsafe {
+ let last = buffer.get_unchecked_mut(length - 1);
+ last.write(TRUCATED);
+ }
+ }
+
+ length
+ }
+}
+
+/// Implement the log trait for the slice type.
+macro_rules! impl_log_for_slice {
+ ( [$type:ident] ) => {
+ impl<$type> Log for &[$type]
+ where
+ $type: Log
+ {
+ impl_log_for_slice!(@generate_write);
+ }
+ };
+ ( [$type:ident; $size:ident] ) => {
+ impl<$type, const $size: usize> Log for &[$type; $size]
+ where
+ $type: Log
+ {
+ impl_log_for_slice!(@generate_write);
+ }
+ };
+ ( @generate_write ) => {
+ fn write(&self, buffer: &mut [MaybeUninit]) -> usize {
+ if buffer.is_empty() {
+ return 0;
+ }
+
+ // Size of the buffer.
+ let length = buffer.len();
+
+ unsafe {
+ buffer.get_unchecked_mut(0).write(b'[');
+ }
+
+ let mut offset = 1;
+
+ for value in self.iter() {
+ if offset >= length {
+ unsafe {
+ buffer.get_unchecked_mut(offset - 1).write(TRUCATED);
+ }
+ offset = length;
+ break;
+ }
+
+ if offset > 1 {
+ if offset + 2 >= length {
+ unsafe {
+ buffer.get_unchecked_mut(length - 1).write(TRUCATED);
+ }
+ offset = length;
+ break;
+ } else {
+ unsafe {
+ buffer.get_unchecked_mut(offset).write(b',');
+ buffer.get_unchecked_mut(offset + 1).write(b' ');
+ }
+ offset += 2;
+ }
+ }
+
+ offset += value.debug(&mut buffer[offset..]);
+ }
+
+ if offset < length {
+ unsafe {
+ buffer.get_unchecked_mut(offset).write(b']');
+ }
+ offset += 1;
+ }
+
+ offset
+ }
+ };
+}
+
+// Supported slice types.
+impl_log_for_slice!([T]);
+impl_log_for_slice!([T; N]);
diff --git a/sdk/log/macro/Cargo.toml b/sdk/log/macro/Cargo.toml
new file mode 100644
index 0000000..51879ca
--- /dev/null
+++ b/sdk/log/macro/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "pinocchio-log-macro"
+description = "Macro for pinocchio log utility"
+version = "0.1.0"
+edition = { workspace = true }
+license = { workspace = true }
+readme = "./README.md"
+repository = { workspace = true }
+
+[lib]
+proc-macro = true
+
+[dependencies]
+quote = "^1.0"
+syn = { version = "^1.0", features = ["extra-traits", "full"] }
diff --git a/sdk/log/macro/README.md b/sdk/log/macro/README.md
new file mode 100644
index 0000000..5da26a0
--- /dev/null
+++ b/sdk/log/macro/README.md
@@ -0,0 +1,34 @@
+#
+
+Companion `log!` macro for [`pinocchio-log`](https://crates.io/crates/pinocchio-log). It automates the creation of a `Logger` object to log a message. It support a limited subset of the [`format!`](https://doc.rust-lang.org/std/fmt/) syntax. The macro parses the format string at compile time and generates the calls to a `Logger` object to generate the corresponding formatted message.
+
+## Usage
+
+The macro works very similar to `solana-program` [`msg!`](https://docs.rs/solana-program/latest/solana_program/macro.msg.html) macro.
+
+To output a simple message (static `&str`):
+```rust
+use pinocchio_log::log
+
+log!("a simple log");
+```
+
+To ouput a formatted message:
+```rust
+use pinocchio_log::log
+
+let amount = 1_000_000_000;
+log!("transfer amount: {}", amount);
+```
+
+Since a `Logger` size is statically determined, messages are limited to `200` length by default. When logging larger messages, it is possible to increase the logger buffer size:
+```rust
+use pinocchio_log::log
+
+let very_long_message = "...";
+log!(500, "message: {}", very_long_message);
+```
+
+## License
+
+The code is licensed under the [Apache License Version 2.0](LICENSE)
diff --git a/sdk/log/macro/src/lib.rs b/sdk/log/macro/src/lib.rs
new file mode 100644
index 0000000..a543c15
--- /dev/null
+++ b/sdk/log/macro/src/lib.rs
@@ -0,0 +1,164 @@
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::{
+ parse::{Parse, ParseStream},
+ parse_macro_input, parse_str,
+ punctuated::Punctuated,
+ Error, Expr, LitInt, LitStr, Token,
+};
+
+/// The default buffer size for the logger.
+const DEFAULT_BUFFER_SIZE: &str = "200";
+
+/// Represents the input arguments to the `log!` macro.
+struct LogArgs {
+ /// The length of the buffer to use for the logger.
+ ///
+ /// This does not have effect when the literal `str` does
+ /// not have value placeholders.
+ buffer_len: LitInt,
+
+ /// The literal formatting string passed to the macro.
+ ///
+ /// The `str` might have value placeholders. While this is
+ /// not a requirement, the number of placeholders must
+ /// match the number of args.
+ format_string: LitStr,
+
+ /// The arguments passed to the macro.
+ ///
+ /// The arguments represent the values to replace the
+ /// placeholders on the format `str`. Valid values must implement
+ /// the [`Log`] trait.
+ args: Punctuated,
+}
+
+impl Parse for LogArgs {
+ fn parse(input: ParseStream) -> syn::Result {
+ // Optional buffer length.
+ let buffer_len = if input.peek(LitInt) {
+ let literal = input.parse()?;
+ // Parse the comma after the buffer length.
+ input.parse::()?;
+ literal
+ } else {
+ parse_str::(DEFAULT_BUFFER_SIZE)?
+ };
+
+ let format_string = input.parse()?;
+ // Check if there are any arguments passed to the macro.
+ let args = if input.is_empty() {
+ Punctuated::new()
+ } else {
+ input.parse::()?;
+ Punctuated::parse_terminated(input)?
+ };
+
+ Ok(LogArgs {
+ buffer_len,
+ format_string,
+ args,
+ })
+ }
+}
+
+/// Companion `log!` macro for `pinocchio-log`.
+///
+/// The macro automates the creation of a `Logger` object to log a message.
+/// It support a limited subset of the [`format!`](https://doc.rust-lang.org/std/fmt/) syntax.
+/// The macro parses the format string at compile time and generates the calls to a `Logger`
+/// object to generate the corresponding formatted message.
+///
+/// # Arguments
+///
+/// - `buffer_len`: The length of the buffer to use for the logger (default to `200`). This is an optional argument.
+/// - `format_string`: The literal string to log. This string can contain placeholders `{}` to be replaced by the arguments.
+/// - `args`: The arguments to replace the placeholders in the format string. The arguments must implement the `Log` trait.
+#[proc_macro]
+pub fn log(input: TokenStream) -> TokenStream {
+ // Parse the input into a `LogArgs`.
+ let LogArgs {
+ buffer_len,
+ format_string,
+ args,
+ } = parse_macro_input!(input as LogArgs);
+ let parsed_string = format_string.value();
+
+ // Check if there are any `{}` placeholders in the format string.
+ //
+ // When the format string has placeholders, the list of arguments must
+ // not be empty. The number of placehilders will be validated later.
+ let needs_formatting = parsed_string.contains("{}");
+
+ if !(needs_formatting || args.is_empty()) {
+ return Error::new_spanned(
+ format_string,
+ "the format string must contain a `{}` placeholder for each value.",
+ )
+ .to_compile_error()
+ .into();
+ }
+
+ if needs_formatting {
+ // The parts of the format string with the placeholders replaced by arguments.
+ let mut replaced_parts = Vec::new();
+ // The number of placeholders in the format string.
+ let mut part_count = 0;
+ // The number of arguments passed to the macro.
+ let mut arg_count = 0;
+
+ let part_iter = parsed_string.split("{}").peekable();
+ let mut arg_iter = args.iter();
+
+ // Replace each occurrence of `{}` with their corresponding argument value.
+ for part in part_iter {
+ if !part.is_empty() {
+ replaced_parts.push(quote! { logger.append(#part) });
+ }
+ part_count += 1;
+
+ if let Some(arg) = arg_iter.next() {
+ replaced_parts.push(quote! { logger.append(#arg) });
+ arg_count += 1;
+ }
+ }
+
+ if (part_count - 1) != arg_count {
+ let arg_message = if arg_count == 0 {
+ "but no arguments were given".to_string()
+ } else {
+ format!(
+ "but there is {} {}",
+ arg_count,
+ if arg_count == 1 {
+ "argument"
+ } else {
+ "arguments"
+ }
+ )
+ };
+
+ return Error::new_spanned(
+ format_string,
+ format!(
+ "{} positional arguments in format string, {}",
+ part_count - 1,
+ arg_message
+ ),
+ )
+ .to_compile_error()
+ .into();
+ }
+
+ // Generate the output string as a compile-time constant
+ TokenStream::from(quote! {
+ {
+ let mut logger = pinocchio_log::logger::Logger::<#buffer_len>::default();
+ #(#replaced_parts;)*
+ logger.log();
+ }
+ })
+ } else {
+ TokenStream::from(quote! {pinocchio_log::logger::log_message(#format_string.as_bytes());})
+ }
+}