diff --git a/diskann-utils/src/lazystring.rs b/diskann-utils/src/lazystring.rs index 72ec2c6d1..999c3307e 100644 --- a/diskann-utils/src/lazystring.rs +++ b/diskann-utils/src/lazystring.rs @@ -3,29 +3,103 @@ * Licensed under the MIT license. */ -use std::fmt::{Display, Error, Formatter}; +use std::fmt::{Debug, Display, Formatter, Result}; -/// A struct used to lazily defer creation of custom async logging messages until we know -/// that the message is actually needed. +/// A macro that behaves like `format!` but constructs a [`LazyString`] to defer string +/// formatting until the result is actually displayed. If the [`LazyString`] is never +/// displayed, this construct has minimal overhead. /// -/// # Context +/// ```rust +/// use diskann_utils::lazy_format; /// -/// Logging in the async context explicitly requires passing of a context pointer to enable -/// CDB to determine the source of error message. To that end, a custom logging function is -/// used. +/// let a: f32 = 10.5; +/// let b: usize = 20; +/// +/// let lazy_string = lazy_format!("This is a test. A = {}, B = {}", a, b); +/// assert_eq!(lazy_string.to_string(), "This is a test. A = 10.5, B = 20"); +/// +/// // Formatting of captured members is deferred until the created `LazyString` is formatted. +/// #[derive(Default)] +/// struct Formatted(std::cell::Cell); +/// +/// impl Formatted { +/// fn was_formatted(&self) -> bool { +/// self.0.get() +/// } +/// +/// fn mark_as_formatted(&self) { +/// self.0.set(true) +/// } +/// } +/// +/// impl std::fmt::Display for Formatted { +/// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +/// if self.was_formatted() { +/// f.write_str("yes") +/// } else { +/// self.mark_as_formatted(); +/// f.write_str("not yet") +/// } +/// } +/// } +/// +/// let f = Formatted::default(); +/// let lazy = lazy_format!("Was this formatted: {f}"); +/// +/// assert!(!f.was_formatted(), "string formatting should be deferred"); +/// assert_eq!(lazy.to_string(), "Was this formatted: not yet"); +/// +/// assert!(f.was_formatted()); +/// assert_eq!(lazy.to_string(), "Was this formatted: yes"); +/// ``` +/// +/// # Creating lazily formatted `'static` error messages /// -/// The `LazyString` captures a lambda that constructs the logging message and implements -/// `std::fmt::Display`, allowing string formatting to only be performed once we know a -/// message needs to be logged. +/// The default [`LazyString`] created by this macro borrows from its formatted arguments +/// and thus has a lifetime constrained to its arguments. +/// +/// If a lazily formatted `'static` compliant variation is needed, the "move" variant +/// can be used (assuming all captured arguments are `'static`): +/// +/// ```rust +/// use diskann_utils::lazy_format; +/// +/// fn assert_static(_: &T) {} +/// +/// let x = 10; +/// +/// let lazy = lazy_format!(move, "x = {x}"); +/// assert_static(&lazy); +/// assert_eq!(lazy.to_string(), "x = 10"); +/// ``` +#[macro_export] +macro_rules! lazy_format { + (move, $($arg:tt)*) => { + $crate::LazyString::new(move |f: &mut std::fmt::Formatter<'_>| { + ::core::write!(f, $($arg)*) + }) + }; + ($($arg:tt)*) => { + $crate::LazyString::new(|f: &mut std::fmt::Formatter<'_>| { + ::core::write!(f, $($arg)*) + }) + }; +} + +/// A struct used to lazily defer string formatting until needed. This is used to implement +/// [`lazy_format!`]: a lazy version of the standard `format!` macro. +/// +/// See [`lazy_format!`] for usage. pub struct LazyString(F) where - F: Fn(&mut Formatter<'_>) -> Result<(), Error>; + F: Fn(&mut Formatter<'_>) -> Result; impl LazyString where - F: Fn(&mut Formatter<'_>) -> Result<(), Error>, + F: Fn(&mut Formatter<'_>) -> Result, { /// Construct a new `LazyString` around the provided lambda. + #[doc(hidden)] pub fn new(f: F) -> Self { Self(f) } @@ -33,43 +107,36 @@ where impl Display for LazyString where - F: Fn(&mut Formatter<'_>) -> Result<(), Error>, + F: Fn(&mut Formatter<'_>) -> Result, { #[inline] - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { (self.0)(f) } } -/// A macro that behaves like `format!` but constructs a `diskann::utils::LazyString` -/// to enable deferred evaluation of the error message. -/// -/// Invoking this macro has the following equivalence: -/// ```ignore -/// let a: f32 = 10.5; -/// let b: usize = 20; -/// // Macro form -/// let lazy_from_macro = lazy_format("This is a test. A = {}, B = {}", a, b); -/// -/// // Direct form -/// let lazy_direct = crate::utils::LazyString::new(|f: &mut std::fmt::Formatter<'_>| { -/// write!(f, "ihis is a test. A = {}, B = {}", a, b) -/// }); -/// ``` -#[macro_export] -macro_rules! lazy_format { - ($($arg:tt)*) => { - // Must be a full path and only available inside `DiskANN`. - $crate::LazyString::new(|f: &mut std::fmt::Formatter<'_>| { - write!(f, $($arg)*) - }) +impl Debug for LazyString +where + F: Fn(&mut Formatter<'_>) -> Result, +{ + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + f.debug_tuple("LazyString") + .field(&format_args!("{self}")) + .finish() } } +/////////// +// Tests // +/////////// + #[cfg(test)] mod test { use super::*; + fn assert_static(_: &T) {} + #[test] fn test_lazy_string() { let x: f32 = 10.5; @@ -82,5 +149,15 @@ mod test { let lazy = lazy_format!("Lazy Message: x = {x}, y = {y}"); assert_eq!(lazy.to_string(), "Lazy Message: x = 10.5, y = 20"); + + let lazy = lazy_format!(move, "Lazy Message: x = {}, y = {y}", x); + assert_static(&lazy); + assert_eq!(lazy.to_string(), "Lazy Message: x = 10.5, y = 20"); + + // Verify that `Debug` at least runs. + assert_eq!( + format!("{:?}", lazy), + "LazyString(Lazy Message: x = 10.5, y = 20)", + ); } }