compile_fmt/lib.rs
1//! Compile-time formatting and derived functionality (e.g., panics / assertions).
2//!
3//! # What?
4//!
5//! This crate allows formatting values in compile time (e.g., in `const fn`s). The formatted values
6//! are not required to be constants; e.g., arguments or local vars in `const fn` can be formatted.
7//!
8//! Features:
9//!
10//! - Zero dependencies.
11//! - Unconditionally `#[no_std]`-compatible.
12//! - The formatting logic is space-efficient; i.e., it allocates the least amount of bytes
13//! that can provably to be sufficient for all possible provided inputs. As a consequence, non-constant
14//! formatted args require a [format specifier](Fmt).
15//! - Does not rely on proc macros. This makes the library more lightweight.
16//!
17//! # Why?
18//!
19//! A guiding use case for the crate is richer dynamic compile-time panic messages. It can be used
20//! in other contexts as well (including in runtime).
21//!
22//! # Limitations
23//!
24//! - Only a few types from the standard library can be formatted: integers, `char`s and `str`ings.
25//! - Formatting specifiers do not support hex encoding, debug formatting etc.
26//! - Padding logic assumes that any Unicode char has identical displayed width, which isn't really
27//! true (e.g., there are chars that have zero width and instead combine with the previous char).
28//! The same assumption is made by the `std` padding logic.
29//!
30//! # Alternatives and similar tools
31//!
32//! - [`const_panic`] provides functionality covering the guiding use case (compile-time panics).
33//! It supports more types and formats at the cost of being more complex. It also uses a different
34//! approach to compute produced message sizes.
35//! - [`const_format`] provides general-purpose formatting of constant values. It doesn't seem to support
36//! "dynamic" / non-constant args.
37//!
38//! [`const_panic`]: https://crates.io/crates/const_panic
39//! [`const_format`]: https://crates.io/crates/const_format/
40//!
41//! # Examples
42//!
43//! ## Basic usage
44//!
45//! ```
46//! use compile_fmt::{compile_assert, fmt};
47//!
48//! const THRESHOLD: usize = 42;
49//!
50//! const fn check_value(value: usize) {
51//! compile_assert!(
52//! value <= THRESHOLD,
53//! "Expected ", value => fmt::<usize>(), " to not exceed ", THRESHOLD
54//! );
55//! // main logic
56//! }
57//! ```
58//!
59//! Note the formatting spec produced with [`fmt()`].
60//!
61//! ## Usage with dynamic strings
62//!
63//! ```
64//! use compile_fmt::{compile_assert, clip};
65//!
66//! const fn check_str(s: &str) {
67//! const MAX_LEN: usize = 16;
68//! compile_assert!(
69//! s.len() <= MAX_LEN,
70//! "String '", s => clip(MAX_LEN, "…"), "' is too long; \
71//! expected no more than ", MAX_LEN, " bytes"
72//! );
73//! // main logic
74//! }
75//!```
76//!
77//! ## Printing dynamically-sized messages
78//!
79//! `compile_args!` allows specifying capacity of the produced message. This is particularly useful
80//! when formatting enums (e.g., to compile-format errors):
81//!
82//! ```
83//! # use compile_fmt::{compile_args, fmt, CompileArgs};
84//! #[derive(Debug)]
85//! enum Error {
86//! Number(u64),
87//! Tuple(usize, char),
88//! }
89//!
90//! type ErrorArgs = CompileArgs<55>;
91//! // ^ 55 is the exact lower boundary on capacity. It's valid to specify
92//! // a greater value, e.g. 64.
93//!
94//! impl Error {
95//! const fn fmt(&self) -> ErrorArgs {
96//! match *self {
97//! Self::Number(number) => compile_args!(
98//! capacity: ErrorArgs::CAPACITY,
99//! "don't like number ", number => fmt::<u64>()
100//! ),
101//! Self::Tuple(pos, ch) => compile_args!(
102//! "don't like char '", ch => fmt::<char>(), "' at position ",
103//! pos => fmt::<usize>()
104//! ),
105//! }
106//! }
107//! }
108//!
109//! // `Error::fmt()` can be used as a building block for more complex messages:
110//! let err = Error::Tuple(1_234, '?');
111//! let message = compile_args!("Operation failed: ", &err.fmt() => fmt::<&ErrorArgs>());
112//! assert_eq!(
113//! message.as_str(),
114//! "Operation failed: don't like char '?' at position 1234"
115//! );
116//! ```
117//!
118//! See docs for macros and format specifiers for more examples.
119
120#![no_std]
121// Documentation settings.
122#![doc(html_root_url = "https://docs.rs/compile-fmt/0.1.0")]
123// Linter settings.
124#![warn(missing_debug_implementations, missing_docs, bare_trait_objects)]
125#![warn(clippy::all, clippy::pedantic)]
126#![allow(
127 clippy::missing_errors_doc,
128 clippy::must_use_candidate,
129 clippy::module_name_repetitions
130)]
131
132use core::{fmt, slice, str};
133#[cfg(test)]
134extern crate std;
135
136mod argument;
137mod format;
138mod macros;
139#[cfg(test)]
140mod tests;
141mod utils;
142
143#[doc(hidden)]
144pub use crate::argument::{Argument, ArgumentWrapper};
145pub use crate::{
146 argument::Ascii,
147 format::{clip, clip_ascii, fmt, Fmt, FormatArgument, MaxLength, StrLength},
148};
149use crate::{format::StrFormat, utils::ClippedStr};
150
151/// Formatted string returned by the [`compile_args!`] macro, similar to [`Arguments`](fmt::Arguments).
152///
153/// The type parameter specifies the compile-time upper boundary of the formatted string length in bytes.
154/// It is not necessarily equal to the actual byte length of the formatted string.
155#[derive(Debug)]
156pub struct CompileArgs<const CAP: usize> {
157 buffer: [u8; CAP],
158 len: usize,
159}
160
161impl<const CAP: usize> fmt::Display for CompileArgs<CAP> {
162 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
163 formatter.write_str(self.as_str())
164 }
165}
166
167impl<const CAP: usize> AsRef<str> for CompileArgs<CAP> {
168 fn as_ref(&self) -> &str {
169 self.as_str()
170 }
171}
172
173impl<const CAP: usize> CompileArgs<CAP> {
174 /// Capacity of these arguments in bytes.
175 pub const CAPACITY: usize = CAP;
176
177 #[doc(hidden)] // Implementation detail of the `compile_args` macro
178 #[track_caller]
179 pub const fn assert_capacity(required_capacity: usize) {
180 compile_assert!(
181 CAP >= required_capacity,
182 "Insufficient capacity (", CAP => fmt::<usize>(), " bytes) provided \
183 for `compile_args` macro; it requires at least ", required_capacity => fmt::<usize>(), " bytes"
184 );
185 }
186
187 const fn new() -> Self {
188 Self {
189 buffer: [0_u8; CAP],
190 len: 0,
191 }
192 }
193
194 const fn write_str(self, s: &str, fmt: Option<StrFormat>) -> Self {
195 match fmt {
196 Some(StrFormat { clip_at, using }) => {
197 let clipped = ClippedStr::new(s, clip_at);
198 match clipped {
199 ClippedStr::Full(bytes) => self.write_str_bytes(bytes),
200 ClippedStr::Clipped(bytes) => self
201 .write_str_bytes(bytes)
202 .write_str_bytes(using.as_bytes()),
203 }
204 }
205 _ => self.write_str_bytes(s.as_bytes()),
206 }
207 }
208
209 const fn write_str_bytes(self, s_bytes: &[u8]) -> Self {
210 let new_len = self.len + s_bytes.len();
211 let mut buffer = self.buffer;
212 let mut pos = self.len;
213
214 while pos < new_len {
215 buffer[pos] = s_bytes[pos - self.len];
216 pos += 1;
217 }
218 Self {
219 buffer,
220 len: new_len,
221 }
222 }
223
224 /// Writes a char to this string. Largely copied from the standard library with minor changes.
225 #[allow(clippy::cast_possible_truncation)] // false positive
226 const fn write_char(self, c: char) -> Self {
227 const TAG_CONT: u8 = 0b_1000_0000;
228 const TAG_TWO_BYTES: u8 = 0b_1100_0000;
229 const TAG_THREE_BYTES: u8 = 0b_1110_0000;
230 const TAG_FOUR_BYTES: u8 = 0b_1111_0000;
231
232 let new_len = self.len + c.len_utf8();
233 let mut buffer = self.buffer;
234 let pos = self.len;
235 let code = c as u32;
236 match c.len_utf8() {
237 1 => {
238 buffer[pos] = code as u8;
239 }
240 2 => {
241 buffer[pos] = (code >> 6 & 0x_1f) as u8 | TAG_TWO_BYTES;
242 buffer[pos + 1] = (code & 0x_3f) as u8 | TAG_CONT;
243 }
244 3 => {
245 buffer[pos] = (code >> 12 & 0x_0f) as u8 | TAG_THREE_BYTES;
246 buffer[pos + 1] = (code >> 6 & 0x_3f) as u8 | TAG_CONT;
247 buffer[pos + 2] = (code & 0x_3f) as u8 | TAG_CONT;
248 }
249 4 => {
250 buffer[pos] = (code >> 18 & 0x_07) as u8 | TAG_FOUR_BYTES;
251 buffer[pos + 1] = (code >> 12 & 0x_3f) as u8 | TAG_CONT;
252 buffer[pos + 2] = (code >> 6 & 0x_3f) as u8 | TAG_CONT;
253 buffer[pos + 3] = (code & 0x_3f) as u8 | TAG_CONT;
254 }
255 _ => unreachable!(),
256 }
257
258 Self {
259 buffer,
260 len: new_len,
261 }
262 }
263
264 /// Formats the provided sequence of [`Argument`]s.
265 #[doc(hidden)] // implementation detail of crate macros
266 pub const fn format(arguments: &[Argument]) -> Self {
267 let mut this = Self::new();
268 let mut arg_i = 0;
269 while arg_i < arguments.len() {
270 this = this.format_arg(arguments[arg_i]);
271 arg_i += 1;
272 }
273 this
274 }
275
276 /// Returns the `str` value of this formatter.
277 pub const fn as_str(&self) -> &str {
278 unsafe {
279 // SAFETY: This is equivalent to `&self.buffer[..self.len]`, only works in compile time.
280 let written_slice = slice::from_raw_parts(self.buffer.as_ptr(), self.len);
281 // SAFETY: Safe by construction; written bytes form a valid `str`.
282 str::from_utf8_unchecked(written_slice)
283 }
284 }
285}
286
287impl<const CAP: usize> FormatArgument for &CompileArgs<CAP> {
288 type Details = ();
289 const MAX_BYTES_PER_CHAR: usize = 4;
290}
291
292impl<const CAP: usize> MaxLength for &CompileArgs<CAP> {
293 const MAX_LENGTH: StrLength = StrLength::both(CAP);
294 // ^ Here, the byte length is exact and the char length is the pessimistic upper boundary.
295}
296
297#[cfg(doctest)]
298doc_comment::doctest!("../README.md");