Skip to content

Commit

Permalink
Allow configuring compile_args! capacity (#3)
Browse files Browse the repository at this point in the history
## What?

Allows configuring `compile_args!` capacity. The capacity is checked
against the minimum required capacity in compile time.

## Why?

This is useful for more complex compile-time formatting, e.g. formatting
`enum`s.
  • Loading branch information
slowli committed Oct 11, 2023
1 parent 925174a commit c6a41c8
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 59 deletions.
40 changes: 25 additions & 15 deletions src/argument.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
use core::fmt;

use crate::{
format::{Fmt, FormatArgument, FormattedLen, Pad, StrFormat},
format::{Fmt, FormatArgument, Pad, StrFormat, StrLength},
utils::{assert_is_ascii, count_chars, ClippedStr},
CompileArgs,
};
Expand All @@ -17,22 +17,22 @@ enum ArgumentInner<'a> {
}

impl ArgumentInner<'_> {
const fn formatted_len(&self) -> FormattedLen {
const fn formatted_len(&self) -> StrLength {
match self {
Self::Str(s, None) => FormattedLen::for_str(s),
Self::Str(s, None) => StrLength::for_str(s),
Self::Str(s, Some(fmt)) => match ClippedStr::new(s, fmt.clip_at) {
ClippedStr::Full(_) => FormattedLen::for_str(s),
ClippedStr::Clipped(bytes) => FormattedLen {
ClippedStr::Full(_) => StrLength::for_str(s),
ClippedStr::Clipped(bytes) => StrLength {
bytes: bytes.len() + fmt.using.len(),
chars: fmt.clip_at + count_chars(fmt.using),
},
},
Self::Char(c) => FormattedLen::for_char(*c),
Self::Char(c) => StrLength::for_char(*c),
Self::Int(value) => {
let bytes = (*value < 0) as usize + log_10_ceil(value.unsigned_abs());
FormattedLen::ascii(bytes)
StrLength::both(bytes)
}
Self::UnsignedInt(value) => FormattedLen::ascii(log_10_ceil(*value)),
Self::UnsignedInt(value) => StrLength::both(log_10_ceil(*value)),
}
}
}
Expand Down Expand Up @@ -242,6 +242,16 @@ impl<'a> ArgumentWrapper<Ascii<'a>> {
}
}

impl<'a, const CAP: usize> ArgumentWrapper<&'a CompileArgs<CAP>> {
/// Performs the conversion.
pub const fn into_argument(self) -> Argument<'a> {
Argument {
inner: ArgumentInner::Str(self.value.as_str(), None),
pad: None,
}
}
}

impl ArgumentWrapper<i128> {
/// Performs the conversion.
pub const fn into_argument(self) -> Argument<'static> {
Expand Down Expand Up @@ -455,7 +465,7 @@ mod tests {
using: "",
}),
);
assert_eq!(arg.formatted_len(), FormattedLen::for_str("te"));
assert_eq!(arg.formatted_len(), StrLength::for_str("te"));

let arg = ArgumentInner::Str(
"teßt",
Expand All @@ -464,7 +474,7 @@ mod tests {
using: "...",
}),
);
assert_eq!(arg.formatted_len(), FormattedLen::for_str("te..."));
assert_eq!(arg.formatted_len(), StrLength::for_str("te..."));

let arg = ArgumentInner::Str(
"teßt",
Expand All @@ -473,7 +483,7 @@ mod tests {
using: "…",
}),
);
assert_eq!(arg.formatted_len(), FormattedLen::for_str("te…"));
assert_eq!(arg.formatted_len(), StrLength::for_str("te…"));

let arg = ArgumentInner::Str(
"teßt",
Expand All @@ -482,7 +492,7 @@ mod tests {
using: "",
}),
);
assert_eq!(arg.formatted_len(), FormattedLen::for_str("teß"));
assert_eq!(arg.formatted_len(), StrLength::for_str("teß"));

let arg = ArgumentInner::Str(
"teßt",
Expand All @@ -491,7 +501,7 @@ mod tests {
using: "…",
}),
);
assert_eq!(arg.formatted_len(), FormattedLen::for_str("teß…"));
assert_eq!(arg.formatted_len(), StrLength::for_str("teß…"));

let arg = ArgumentInner::Str(
"teßt",
Expand All @@ -500,12 +510,12 @@ mod tests {
using: "-",
}),
);
assert_eq!(arg.formatted_len(), FormattedLen::for_str("teß-"));
assert_eq!(arg.formatted_len(), StrLength::for_str("teß-"));

for clip_at in [4, 5, 16] {
for using in ["", "...", "…"] {
let arg = ArgumentInner::Str("teßt", Some(StrFormat { clip_at, using }));
assert_eq!(arg.formatted_len(), FormattedLen::for_str("teßt"));
assert_eq!(arg.formatted_len(), StrLength::for_str("teßt"));
}
}
}
Expand Down
83 changes: 43 additions & 40 deletions src/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,35 @@ use core::fmt::Alignment;

use crate::utils::{assert_is_ascii, count_chars};

/// Length of a string measured in bytes and chars.
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct FormattedLen {
pub struct StrLength {
/// Number of bytes the string occupies.
pub bytes: usize,
/// Number of chars in the string.
pub chars: usize,
}

impl FormattedLen {
pub const fn for_str(s: &str) -> Self {
impl StrLength {
pub(crate) const fn for_str(s: &str) -> Self {
Self {
bytes: s.len(),
chars: count_chars(s),
}
}

pub const fn for_char(c: char) -> Self {
pub(crate) const fn for_char(c: char) -> Self {
Self {
bytes: c.len_utf8(),
chars: 1,
}
}

pub const fn ascii(bytes: usize) -> Self {
/// Creates a length in which both `bytes` and `chars` fields are set to the specified `value`.
pub const fn both(value: usize) -> Self {
Self {
bytes,
chars: bytes,
bytes: value,
chars: value,
}
}
}
Expand Down Expand Up @@ -129,21 +133,18 @@ pub struct Fmt<T: FormatArgument> {
/// Byte capacity of the format without taking padding into account. This is a field
/// rather than a method in `FormatArgument` because we wouldn't be able to call this method
/// in `const fn`s.
capacity: FormattedLen,
capacity: StrLength,
pub(crate) details: T::Details,
pub(crate) pad: Option<Pad>,
}

/// Creates a default format for a type that has known bounded formatting width.
pub const fn fmt<T>() -> Fmt<T>
where
T: FormatArgument<Details = ()> + MaxWidth,
T: FormatArgument<Details = ()> + MaxLength,
{
Fmt {
capacity: FormattedLen {
bytes: T::MAX_WIDTH * T::MAX_BYTES_PER_CHAR,
chars: T::MAX_WIDTH,
},
capacity: T::MAX_LENGTH,
details: (),
pad: None,
}
Expand All @@ -158,8 +159,8 @@ where
pub const fn clip<'a>(clip_at: usize, using: &'static str) -> Fmt<&'a str> {
assert!(clip_at > 0, "Clip width must be positive");
Fmt {
capacity: FormattedLen {
bytes: clip_at * char::MAX_WIDTH + using.len(),
capacity: StrLength {
bytes: clip_at * char::MAX_LENGTH.bytes + using.len(),
chars: clip_at + count_chars(using),
},
details: StrFormat { clip_at, using },
Expand All @@ -176,7 +177,7 @@ pub const fn clip_ascii<'a>(clip_at: usize, using: &'static str) -> Fmt<Ascii<'a
assert!(clip_at > 0, "Clip width must be positive");
assert_is_ascii(using);
Fmt {
capacity: FormattedLen::ascii(clip_at + using.len()),
capacity: StrLength::both(clip_at + using.len()),
details: StrFormat { clip_at, using },
pad: None,
}
Expand Down Expand Up @@ -269,17 +270,18 @@ pub struct StrFormat {
}

/// Type that has a known upper boundary for the formatted length.
pub trait MaxWidth {
/// Upper boundary for the formatted length in bytes.
const MAX_WIDTH: usize;
pub trait MaxLength {
/// Upper boundary for the formatted length in bytes and chars.
const MAX_LENGTH: StrLength;
}

macro_rules! impl_max_width_for_uint {
($($uint:ty),+) => {
$(
impl MaxWidth for $uint {
const MAX_WIDTH: usize =
crate::ArgumentWrapper::new(Self::MAX).into_argument().formatted_len();
impl MaxLength for $uint {
const MAX_LENGTH: StrLength = StrLength::both(
crate::ArgumentWrapper::new(Self::MAX).into_argument().formatted_len(),
);
}

impl FormatArgument for $uint {
Expand All @@ -295,9 +297,10 @@ impl_max_width_for_uint!(u8, u16, u32, u64, u128, usize);
macro_rules! impl_max_width_for_int {
($($int:ty),+) => {
$(
impl MaxWidth for $int {
const MAX_WIDTH: usize =
crate::ArgumentWrapper::new(Self::MIN).into_argument().formatted_len();
impl MaxLength for $int {
const MAX_LENGTH: StrLength = StrLength::both(
crate::ArgumentWrapper::new(Self::MIN).into_argument().formatted_len(),
);
}

impl FormatArgument for $int {
Expand All @@ -310,8 +313,8 @@ macro_rules! impl_max_width_for_int {

impl_max_width_for_int!(i8, i16, i32, i64, i128, isize);

impl MaxWidth for char {
const MAX_WIDTH: usize = 4;
impl MaxLength for char {
const MAX_LENGTH: StrLength = StrLength { bytes: 4, chars: 1 };
}

impl FormatArgument for char {
Expand All @@ -327,19 +330,19 @@ mod tests {

#[test]
fn max_length_bound_is_correct() {
assert_eq!(u8::MAX_WIDTH, u8::MAX.to_string().len());
assert_eq!(u16::MAX_WIDTH, u16::MAX.to_string().len());
assert_eq!(u32::MAX_WIDTH, u32::MAX.to_string().len());
assert_eq!(u64::MAX_WIDTH, u64::MAX.to_string().len());
assert_eq!(u128::MAX_WIDTH, u128::MAX.to_string().len());
assert_eq!(usize::MAX_WIDTH, usize::MAX.to_string().len());

assert_eq!(i8::MAX_WIDTH, i8::MIN.to_string().len());
assert_eq!(i16::MAX_WIDTH, i16::MIN.to_string().len());
assert_eq!(i32::MAX_WIDTH, i32::MIN.to_string().len());
assert_eq!(i64::MAX_WIDTH, i64::MIN.to_string().len());
assert_eq!(i128::MAX_WIDTH, i128::MIN.to_string().len());
assert_eq!(isize::MAX_WIDTH, isize::MIN.to_string().len());
assert_eq!(u8::MAX_LENGTH.bytes, u8::MAX.to_string().len());
assert_eq!(u16::MAX_LENGTH.bytes, u16::MAX.to_string().len());
assert_eq!(u32::MAX_LENGTH.bytes, u32::MAX.to_string().len());
assert_eq!(u64::MAX_LENGTH.bytes, u64::MAX.to_string().len());
assert_eq!(u128::MAX_LENGTH.bytes, u128::MAX.to_string().len());
assert_eq!(usize::MAX_LENGTH.bytes, usize::MAX.to_string().len());

assert_eq!(i8::MAX_LENGTH.bytes, i8::MIN.to_string().len());
assert_eq!(i16::MAX_LENGTH.bytes, i16::MIN.to_string().len());
assert_eq!(i32::MAX_LENGTH.bytes, i32::MIN.to_string().len());
assert_eq!(i64::MAX_LENGTH.bytes, i64::MIN.to_string().len());
assert_eq!(i128::MAX_LENGTH.bytes, i128::MIN.to_string().len());
assert_eq!(isize::MAX_LENGTH.bytes, isize::MIN.to_string().len());
}

#[test]
Expand Down
66 changes: 65 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,47 @@
//! }
//!```
//!
//! ## 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):
//!
//! ```
//! # use compile_fmt::{compile_args, fmt, CompileArgs};
//! #[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.

#![no_std]
Expand Down Expand Up @@ -103,7 +144,7 @@ mod utils;
pub use crate::argument::{Argument, ArgumentWrapper};
pub use crate::{
argument::Ascii,
format::{clip, clip_ascii, fmt, Fmt, FormatArgument, MaxWidth},
format::{clip, clip_ascii, fmt, Fmt, FormatArgument, MaxLength, StrLength},
};
use crate::{format::StrFormat, utils::ClippedStr};

Expand All @@ -130,6 +171,19 @@ impl<const CAP: usize> AsRef<str> for CompileArgs<CAP> {
}

impl<const CAP: usize> CompileArgs<CAP> {
/// Capacity of these arguments in bytes.
pub const CAPACITY: usize = CAP;

#[doc(hidden)] // Implementation detail of the `compile_args` macro
#[track_caller]
pub const fn assert_capacity(required_capacity: usize) {
compile_assert!(
CAP >= required_capacity,
"Insufficient capacity (", CAP => fmt::<usize>(), " bytes) provided \
for `compile_args` macro; it requires at least ", required_capacity => fmt::<usize>(), " bytes"
);
}

const fn new() -> Self {
Self {
buffer: [0_u8; CAP],
Expand Down Expand Up @@ -230,5 +284,15 @@ impl<const CAP: usize> CompileArgs<CAP> {
}
}

impl<const CAP: usize> FormatArgument for &CompileArgs<CAP> {
type Details = ();
const MAX_BYTES_PER_CHAR: usize = 4;
}

impl<const CAP: usize> MaxLength for &CompileArgs<CAP> {
const MAX_LENGTH: StrLength = StrLength::both(CAP);
// ^ Here, the byte length is exact and the char length is the pessimistic upper boundary.
}

#[cfg(doctest)]
doc_comment::doctest!("../README.md");

0 comments on commit c6a41c8

Please sign in to comment.