Crate compile_fmt

source ·
Expand description

Compile-time formatting and derived functionality (e.g., panics / assertions).

What?

This crate allows formatting values in compile time (e.g., in const fns). The formatted values are not required to be constants; e.g., arguments or local vars in const fn can be formatted.

Features:

  • Zero dependencies.
  • Unconditionally #[no_std]-compatible.
  • The formatting logic is space-efficient; i.e., it allocates the least amount of bytes that can provably to be sufficient for all possible provided inputs. As a consequence, non-constant formatted args require a format specifier.
  • Does not rely on proc macros. This makes the library more lightweight.

Why?

A guiding use case for the crate is richer dynamic compile-time panic messages. It can be used in other contexts as well (including in runtime).

Limitations

  • Only a few types from the standard library can be formatted: integers, chars and strings.
  • Formatting specifiers do not support hex encoding, debug formatting etc.
  • Padding logic assumes that any Unicode char has identical displayed width, which isn’t really true (e.g., there are chars that have zero width and instead combine with the previous char). The same assumption is made by the std padding logic.

Alternatives and similar tools

  • const_panic provides functionality covering the guiding use case (compile-time panics). It supports more types and formats at the cost of being more complex. It also uses a different approach to compute produced message sizes.
  • const_format provides general-purpose formatting of constant values. It doesn’t seem to support “dynamic” / non-constant args.

Examples

Basic usage

use compile_fmt::{compile_assert, fmt};

const THRESHOLD: usize = 42;

const fn check_value(value: usize) {
    compile_assert!(
        value <= THRESHOLD,
        "Expected ", value => fmt::<usize>(), " to not exceed ", THRESHOLD
    );
    // main logic
}

Note the formatting spec produced with fmt().

Usage with dynamic strings

use compile_fmt::{compile_assert, clip};

const fn check_str(s: &str) {
    const MAX_LEN: usize = 16;
    compile_assert!(
        s.len() <= MAX_LEN,
        "String '", s => clip(MAX_LEN, "…"), "' is too long; \
         expected no more than ", MAX_LEN, " bytes"
    );
    // main logic
}

Printing dynamically-sized messages

compile_args! allows specifying capacity of the produced message. This is particularly useful when formatting enums (e.g., to compile-format errors):

#[derive(Debug)]
enum Error {
    Number(u64),
    Tuple(usize, char),
}

type ErrorArgs = CompileArgs<55>;
// ^ 55 is the exact lower boundary on capacity. It's valid to specify
// a greater value, e.g. 64.

impl Error {
    const fn fmt(&self) -> ErrorArgs {
        match *self {
            Self::Number(number) => compile_args!(
                capacity: ErrorArgs::CAPACITY,
                "don't like number ", number => fmt::<u64>()
            ),
            Self::Tuple(pos, ch) => compile_args!(
                "don't like char '", ch => fmt::<char>(), "' at position ",
                pos => fmt::<usize>()
            ),
        }
    }
}

// `Error::fmt()` can be used as a building block for more complex messages:
let err = Error::Tuple(1_234, '?');
let message = compile_args!("Operation failed: ", &err.fmt() => fmt::<&ErrorArgs>());
assert_eq!(
    message.as_str(),
    "Operation failed: don't like char '?' at position 1234"
);

See docs for macros and format specifiers for more examples.

Macros

Structs

Traits

  • Type that can be formatted. Implemented for standard integer types, &str and char.
  • Type that has a known upper boundary for the formatted length.

Functions

  • Creates a format that will clip the value to the specified max char width (not byte width!). If clipped, the end of the string will be replaced with the specified replacer, which can be empty.
  • Same as clip(), but for Ascii strings.
  • Creates a default format for a type that has known bounded formatting width.