compile_fmt/
argument.rs

1//! [`Argument`] and related types.
2
3use core::fmt;
4
5use crate::{
6    format::{Fmt, FormatArgument, Pad, StrFormat, StrLength},
7    utils::{assert_is_ascii, count_chars, ClippedStr},
8    CompileArgs,
9};
10
11#[derive(Debug, Clone, Copy)]
12enum ArgumentInner<'a> {
13    Str(&'a str, Option<StrFormat>),
14    Char(char),
15    Int(i128),
16    UnsignedInt(u128),
17}
18
19impl ArgumentInner<'_> {
20    const fn formatted_len(&self) -> StrLength {
21        match self {
22            Self::Str(s, None) => StrLength::for_str(s),
23            Self::Str(s, Some(fmt)) => match ClippedStr::new(s, fmt.clip_at) {
24                ClippedStr::Full(_) => StrLength::for_str(s),
25                ClippedStr::Clipped(bytes) => StrLength {
26                    bytes: bytes.len() + fmt.using.len(),
27                    chars: fmt.clip_at + count_chars(fmt.using),
28                },
29            },
30            Self::Char(c) => StrLength::for_char(*c),
31            Self::Int(value) => {
32                let bytes = (*value < 0) as usize + log_10_ceil(value.unsigned_abs());
33                StrLength::both(bytes)
34            }
35            Self::UnsignedInt(value) => StrLength::both(log_10_ceil(*value)),
36        }
37    }
38}
39
40/// Generalized argument in crate macros.
41#[doc(hidden)] // implementation detail of crate macros
42#[derive(Debug, Clone, Copy)]
43pub struct Argument<'a> {
44    inner: ArgumentInner<'a>,
45    pad: Option<Pad>,
46}
47
48impl Argument<'_> {
49    /// Returns the formatted length of the argument in bytes.
50    pub const fn formatted_len(&self) -> usize {
51        let non_padded_len = self.inner.formatted_len();
52        if let Some(pad) = &self.pad {
53            if pad.width > non_padded_len.chars {
54                let pad_char_count = pad.width - non_padded_len.chars;
55                pad_char_count * pad.using.len_utf8() + non_padded_len.bytes
56            } else {
57                // The non-padded string is longer than the pad width; it won't be padded
58                non_padded_len.bytes
59            }
60        } else {
61            non_padded_len.bytes
62        }
63    }
64}
65
66const fn log_10_ceil(mut value: u128) -> usize {
67    if value == 0 {
68        return 1;
69    }
70
71    let mut log = 0;
72    while value > 0 {
73        value /= 10;
74        log += 1;
75    }
76    log
77}
78
79impl<const CAP: usize> CompileArgs<CAP> {
80    const fn write_u128(self, mut value: u128) -> Self {
81        let new_len = self.len + log_10_ceil(value);
82        let mut buffer = self.buffer;
83        let mut pos = new_len - 1;
84
85        loop {
86            buffer[pos] = b'0' + (value % 10) as u8;
87            if pos == self.len {
88                break;
89            }
90            value /= 10;
91            pos -= 1;
92        }
93        Self {
94            buffer,
95            len: new_len,
96        }
97    }
98
99    const fn write_i128(self, value: i128) -> Self {
100        let this = if value < 0 {
101            self.write_char('-')
102        } else {
103            self
104        };
105        this.write_u128(value.unsigned_abs())
106    }
107
108    pub(crate) const fn format_arg(mut self, arg: Argument) -> Self {
109        let pad_after = 'compute_pad: {
110            if let Some(pad) = &arg.pad {
111                // Check if the argument must be padded.
112                let non_padded_len = arg.inner.formatted_len();
113                if pad.width > non_padded_len.chars {
114                    let (pad_before, pad_after) = pad.compute_padding(non_padded_len.chars);
115                    let mut count = 0;
116                    while count < pad_before {
117                        self = self.write_char(pad.using);
118                        count += 1;
119                    }
120                    break 'compute_pad Some((pad_after, pad.using));
121                }
122            }
123            None
124        };
125
126        self = match arg.inner {
127            ArgumentInner::Str(s, fmt) => self.write_str(s, fmt),
128            // chars and ints are not affected by format so far (i.e., not clipped)
129            ArgumentInner::Char(c) => self.write_char(c),
130            ArgumentInner::Int(value) => self.write_i128(value),
131            ArgumentInner::UnsignedInt(value) => self.write_u128(value),
132        };
133        if let Some((pad_after, using)) = pad_after {
134            let mut count = 0;
135            while count < pad_after {
136                self = self.write_char(using);
137                count += 1;
138            }
139        }
140        self
141    }
142}
143
144/// ASCII string wrapper.
145///
146/// This wrapper is useful for non-constant strings if it can be ensured that a string consists
147/// entirely of ASCII chars. This allows decreasing capacity requirements for [`CompileArgs`]
148/// involving such strings. In the general case, `CompileArgs` logic must assume that each char
149/// in a string can require up to 4 bytes; in case of `Ascii` strings, this is reduced to
150/// 1 byte per char.
151///
152/// # Examples
153///
154/// ```
155/// use compile_fmt::{clip, clip_ascii, compile_args, Ascii, CompileArgs};
156///
157/// let s: CompileArgs<10> = compile_args!(
158///     "[", Ascii::new("test") => clip_ascii(8, "").pad_left(8, ' '), "]"
159/// );
160/// assert_eq!(s.as_str(), "[test    ]");
161///
162/// // The necessary capacity for generic UTF-8 strings is greater
163/// // (34 bytes instead of 10):
164/// let s: CompileArgs<34> = compile_args!(
165///     "[", "test" => clip(8, "").pad_left(8, ' '), "]"
166/// );
167/// assert_eq!(s.as_str(), "[test    ]");
168/// ```
169#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
170pub struct Ascii<'a>(pub(crate) &'a str);
171
172impl<'a> Ascii<'a> {
173    /// Wraps the provided string if it consists entirely of ASCII chars.
174    ///
175    /// # Panics
176    ///
177    /// Panics if the string contains non-ASCII chars.
178    pub const fn new(s: &'a str) -> Self {
179        assert_is_ascii(s);
180        Self(s)
181    }
182}
183
184/// Wrapper for an admissible argument type allowing to convert it to an [`Argument`] in compile time.
185#[doc(hidden)] // implementation detail of crate macros
186pub struct ArgumentWrapper<T: FormatArgument> {
187    value: T,
188    fmt: Option<Fmt<T>>,
189}
190
191impl<T> fmt::Debug for ArgumentWrapper<T>
192where
193    T: FormatArgument + fmt::Debug,
194    T::Details: fmt::Debug,
195{
196    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
197        formatter
198            .debug_struct("ArgumentWrapper")
199            .field("value", &self.value)
200            .field("fmt", &self.fmt)
201            .finish()
202    }
203}
204
205impl<T: FormatArgument> ArgumentWrapper<T> {
206    pub const fn new(value: T) -> Self {
207        Self { value, fmt: None }
208    }
209
210    #[must_use]
211    pub const fn with_fmt(mut self, fmt: Fmt<T>) -> Self {
212        self.fmt = Some(fmt);
213        self
214    }
215}
216
217impl<'a> ArgumentWrapper<&'a str> {
218    /// Performs the conversion.
219    pub const fn into_argument(self) -> Argument<'a> {
220        let (str_fmt, pad) = match self.fmt {
221            Some(Fmt { details, pad, .. }) => (Some(details), pad),
222            None => (None, None),
223        };
224        Argument {
225            inner: ArgumentInner::Str(self.value, str_fmt),
226            pad,
227        }
228    }
229}
230
231impl<'a> ArgumentWrapper<Ascii<'a>> {
232    /// Performs the conversion.
233    pub const fn into_argument(self) -> Argument<'a> {
234        let (str_fmt, pad) = match self.fmt {
235            Some(Fmt { details, pad, .. }) => (Some(details), pad),
236            None => (None, None),
237        };
238        Argument {
239            inner: ArgumentInner::Str(self.value.0, str_fmt),
240            pad,
241        }
242    }
243}
244
245impl<'a, const CAP: usize> ArgumentWrapper<&'a CompileArgs<CAP>> {
246    /// Performs the conversion.
247    pub const fn into_argument(self) -> Argument<'a> {
248        Argument {
249            inner: ArgumentInner::Str(self.value.as_str(), None),
250            pad: None,
251        }
252    }
253}
254
255impl ArgumentWrapper<i128> {
256    /// Performs the conversion.
257    pub const fn into_argument(self) -> Argument<'static> {
258        let pad = match self.fmt {
259            Some(Fmt { pad, .. }) => pad,
260            None => None,
261        };
262        Argument {
263            inner: ArgumentInner::Int(self.value),
264            pad,
265        }
266    }
267}
268
269macro_rules! impl_argument_wrapper_for_int {
270    ($int:ty) => {
271        impl ArgumentWrapper<$int> {
272            /// Performs the conversion.
273            pub const fn into_argument(self) -> Argument<'static> {
274                let pad = match self.fmt {
275                    Some(Fmt { pad, .. }) => pad,
276                    None => None,
277                };
278                Argument {
279                    inner: ArgumentInner::Int(self.value as i128),
280                    pad,
281                }
282            }
283        }
284    };
285}
286
287impl_argument_wrapper_for_int!(i8);
288impl_argument_wrapper_for_int!(i16);
289impl_argument_wrapper_for_int!(i32);
290impl_argument_wrapper_for_int!(i64);
291impl_argument_wrapper_for_int!(isize);
292
293impl ArgumentWrapper<u128> {
294    /// Performs the conversion.
295    pub const fn into_argument(self) -> Argument<'static> {
296        let pad = match self.fmt {
297            Some(Fmt { pad, .. }) => pad,
298            None => None,
299        };
300        Argument {
301            inner: ArgumentInner::UnsignedInt(self.value),
302            pad,
303        }
304    }
305}
306
307macro_rules! impl_argument_wrapper_for_uint {
308    ($uint:ty) => {
309        impl ArgumentWrapper<$uint> {
310            /// Performs the conversion.
311            pub const fn into_argument(self) -> Argument<'static> {
312                let pad = match self.fmt {
313                    Some(Fmt { pad, .. }) => pad,
314                    None => None,
315                };
316                Argument {
317                    inner: ArgumentInner::UnsignedInt(self.value as u128),
318                    pad,
319                }
320            }
321        }
322    };
323}
324
325impl_argument_wrapper_for_uint!(u8);
326impl_argument_wrapper_for_uint!(u16);
327impl_argument_wrapper_for_uint!(u32);
328impl_argument_wrapper_for_uint!(u64);
329impl_argument_wrapper_for_uint!(usize);
330
331impl ArgumentWrapper<char> {
332    /// Performs the conversion.
333    pub const fn into_argument(self) -> Argument<'static> {
334        let pad = match self.fmt {
335            Some(Fmt { pad, .. }) => pad,
336            None => None,
337        };
338        Argument {
339            inner: ArgumentInner::Char(self.value),
340            pad,
341        }
342    }
343}
344
345#[cfg(test)]
346mod tests {
347    use rand::{rngs::StdRng, Rng, SeedableRng};
348
349    use super::*;
350    use core::fmt::Alignment;
351    use rand::distr::uniform::{UniformSampler, UniformUsize};
352    use std::string::ToString;
353
354    #[test]
355    fn length_estimation_for_small_ints() {
356        for i in 0_u8..=u8::MAX {
357            assert_eq!(
358                ArgumentWrapper::new(i).into_argument().formatted_len(),
359                i.to_string().len(),
360                "Formatted length estimated incorrectly for {i}"
361            );
362        }
363        for i in 0_u16..=u16::MAX {
364            assert_eq!(
365                ArgumentWrapper::new(i).into_argument().formatted_len(),
366                i.to_string().len(),
367                "Formatted length estimated incorrectly for {i}"
368            );
369        }
370        for i in i8::MIN..=i8::MAX {
371            assert_eq!(
372                ArgumentWrapper::new(i).into_argument().formatted_len(),
373                i.to_string().len(),
374                "Formatted length estimated incorrectly for {i}"
375            );
376        }
377        for i in i16::MIN..=i16::MAX {
378            assert_eq!(
379                ArgumentWrapper::new(i).into_argument().formatted_len(),
380                i.to_string().len(),
381                "Formatted length estimated incorrectly for {i}"
382            );
383        }
384    }
385
386    #[test]
387    fn length_estimation_for_large_ints() {
388        const RNG_SEED: u64 = 123;
389        const SAMPLE_COUNT: usize = 100_000;
390
391        let mut rng = StdRng::seed_from_u64(RNG_SEED);
392        for _ in 0..SAMPLE_COUNT {
393            let i: u32 = rng.random();
394            assert_eq!(
395                ArgumentWrapper::new(i).into_argument().formatted_len(),
396                i.to_string().len(),
397                "Formatted length estimated incorrectly for {i}"
398            );
399        }
400        for _ in 0..SAMPLE_COUNT {
401            let i: u64 = rng.random();
402            assert_eq!(
403                ArgumentWrapper::new(i).into_argument().formatted_len(),
404                i.to_string().len(),
405                "Formatted length estimated incorrectly for {i}"
406            );
407        }
408        for _ in 0..SAMPLE_COUNT {
409            let i: u128 = rng.random();
410            assert_eq!(
411                ArgumentWrapper::new(i).into_argument().formatted_len(),
412                i.to_string().len(),
413                "Formatted length estimated incorrectly for {i}"
414            );
415        }
416
417        let sampler = UniformUsize::new_inclusive(0, usize::MAX).unwrap();
418        for _ in 0..SAMPLE_COUNT {
419            let i = sampler.sample(&mut rng);
420            assert_eq!(
421                ArgumentWrapper::new(i).into_argument().formatted_len(),
422                i.to_string().len(),
423                "Formatted length estimated incorrectly for {i}"
424            );
425        }
426
427        for _ in 0..SAMPLE_COUNT {
428            let i: i32 = rng.random();
429            assert_eq!(
430                ArgumentWrapper::new(i).into_argument().formatted_len(),
431                i.to_string().len(),
432                "Formatted length estimated incorrectly for {i}"
433            );
434        }
435        for _ in 0..SAMPLE_COUNT {
436            let i: i64 = rng.random();
437            assert_eq!(
438                ArgumentWrapper::new(i).into_argument().formatted_len(),
439                i.to_string().len(),
440                "Formatted length estimated incorrectly for {i}"
441            );
442        }
443        for _ in 0..SAMPLE_COUNT {
444            let i: i128 = rng.random();
445            assert_eq!(
446                ArgumentWrapper::new(i).into_argument().formatted_len(),
447                i.to_string().len(),
448                "Formatted length estimated incorrectly for {i}"
449            );
450        }
451
452        for _ in 0..SAMPLE_COUNT {
453            let i = sampler.sample(&mut rng).cast_signed();
454            assert_eq!(
455                ArgumentWrapper::new(i).into_argument().formatted_len(),
456                i.to_string().len(),
457                "Formatted length estimated incorrectly for {i}"
458            );
459        }
460    }
461
462    #[test]
463    fn formatted_len_for_clipped_strings() {
464        let arg = ArgumentInner::Str(
465            "teßt",
466            Some(StrFormat {
467                clip_at: 2,
468                using: "",
469            }),
470        );
471        assert_eq!(arg.formatted_len(), StrLength::for_str("te"));
472
473        let arg = ArgumentInner::Str(
474            "teßt",
475            Some(StrFormat {
476                clip_at: 2,
477                using: "...",
478            }),
479        );
480        assert_eq!(arg.formatted_len(), StrLength::for_str("te..."));
481
482        let arg = ArgumentInner::Str(
483            "teßt",
484            Some(StrFormat {
485                clip_at: 2,
486                using: "…",
487            }),
488        );
489        assert_eq!(arg.formatted_len(), StrLength::for_str("te…"));
490
491        let arg = ArgumentInner::Str(
492            "teßt",
493            Some(StrFormat {
494                clip_at: 3,
495                using: "",
496            }),
497        );
498        assert_eq!(arg.formatted_len(), StrLength::for_str("teß"));
499
500        let arg = ArgumentInner::Str(
501            "teßt",
502            Some(StrFormat {
503                clip_at: 3,
504                using: "…",
505            }),
506        );
507        assert_eq!(arg.formatted_len(), StrLength::for_str("teß…"));
508
509        let arg = ArgumentInner::Str(
510            "teßt",
511            Some(StrFormat {
512                clip_at: 3,
513                using: "-",
514            }),
515        );
516        assert_eq!(arg.formatted_len(), StrLength::for_str("teß-"));
517
518        for clip_at in [4, 5, 16] {
519            for using in ["", "...", "…"] {
520                let arg = ArgumentInner::Str("teßt", Some(StrFormat { clip_at, using }));
521                assert_eq!(arg.formatted_len(), StrLength::for_str("teßt"));
522            }
523        }
524    }
525
526    #[test]
527    fn formatted_len_with_padding() {
528        let argument = Argument {
529            inner: ArgumentInner::Str("teßt", None),
530            pad: Some(Pad {
531                align: Alignment::Left,
532                width: 8,
533                using: ' ',
534            }),
535        };
536        assert_eq!(argument.formatted_len(), "teßt    ".len());
537
538        let argument = Argument {
539            inner: ArgumentInner::Str("teßt", None),
540            pad: Some(Pad {
541                align: Alignment::Left,
542                width: 8,
543                using: '💣',
544            }),
545        };
546        assert_eq!(argument.formatted_len(), "teßt💣💣💣💣".len());
547
548        for pad_width in 1..=4 {
549            let argument = Argument {
550                inner: ArgumentInner::Str("teßt", None),
551                pad: Some(Pad {
552                    align: Alignment::Left,
553                    width: pad_width,
554                    using: ' ',
555                }),
556            };
557            assert_eq!(argument.formatted_len(), "teßt".len());
558        }
559    }
560
561    #[test]
562    fn formatted_len_with_padding_and_clipping() {
563        let inner = ArgumentInner::Str(
564            "teßt",
565            Some(StrFormat {
566                clip_at: 3,
567                using: "…",
568            }),
569        );
570        let argument = Argument {
571            inner,
572            pad: Some(Pad {
573                align: Alignment::Left,
574                width: 8,
575                using: ' ',
576            }),
577        };
578        assert_eq!(argument.formatted_len(), "teß…    ".len());
579
580        let argument = Argument {
581            inner,
582            pad: Some(Pad {
583                align: Alignment::Left,
584                width: 8,
585                using: '💣',
586            }),
587        };
588        assert_eq!(argument.formatted_len(), "teß…💣💣💣💣".len());
589
590        for pad_width in 1..=4 {
591            let argument = Argument {
592                inner,
593                pad: Some(Pad {
594                    align: Alignment::Left,
595                    width: pad_width,
596                    using: ' ',
597                }),
598            };
599            assert_eq!(argument.formatted_len(), "teß…".len());
600        }
601    }
602
603    #[test]
604    #[should_panic(expected = "String 'teß…' contains non-ASCII chars; first at position 2")]
605    fn ascii_panic() {
606        Ascii::new("teß…");
607    }
608}