pw_format: Add core::fmt macro helper
Change-Id: I5d3d1108a5674f6e1d2ca8b9b69a44353c1e9cfd
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/178537
Reviewed-by: Taylor Cramer <cramertj@google.com>
Commit-Queue: Erik Gilling <konkers@google.com>
diff --git a/pw_format/rust/pw_format/macros.rs b/pw_format/rust/pw_format/macros.rs
index 1c04947..5f96c6f 100644
--- a/pw_format/rust/pw_format/macros.rs
+++ b/pw_format/rust/pw_format/macros.rs
@@ -369,6 +369,10 @@
/// Process a string fragment.
///
/// **NOTE**: This string may contain unescaped `%` characters.
+ /// However, most implementations of this train can simply ignore string
+ /// fragments as they will be included (with properly escaped `%`
+ /// characters) as part of the format string passed to
+ /// [`PrintfFormatMacroGenerator::finalize`].
///
/// See [`FormatMacroGenerator::string_fragment`] for a disambiguation
/// between a string fragment and string conversion.
@@ -485,3 +489,117 @@
};
generate(generator, format_and_args)
}
+
+/// A specialized generator for proc macros that produce [`core::fmt`] style format strings.
+///
+/// For proc macros that need to translate a `pw_format` invocation into a
+/// [`core::fmt`] style format string, `CoreFmtFormatMacroGenerator` offer a
+/// specialized form of [`FormatMacroGenerator`] that builds the format string
+/// and provides it as an argument to
+/// [`finalize`](CoreFmtFormatMacroGenerator::finalize).
+///
+/// In cases where a generator needs to override the conversion specifier (i.e.
+/// `{}`, it can return it from its appropriate conversion method.
+pub trait CoreFmtFormatMacroGenerator {
+ /// Called by [`generate_core_fmt`] at the end of code generation.
+ ///
+ /// Works like [`FormatMacroGenerator::finalize`] with the addition of
+ /// being provided a [`core::fmt`] format string.
+ fn finalize(self, format_string: String) -> Result<TokenStream2>;
+
+ /// Process a string fragment.
+ ///
+ /// **NOTE**: This string may contain unescaped `{` and `}` characters.
+ /// However, most implementations of this train can simply ignore string
+ /// fragments as they will be included (with properly escaped `{` and `}`
+ /// characters) as part of the format string passed to
+ /// [`CoreFmtFormatMacroGenerator::finalize`].
+ ///
+ ///
+ /// See [`FormatMacroGenerator::string_fragment`] for a disambiguation
+ /// between a string fragment and string conversion.
+ fn string_fragment(&mut self, string: &str) -> Result<()>;
+
+ /// Process an integer conversion.
+ fn integer_conversion(&mut self, ty: Ident, expression: Expr) -> Result<Option<String>>;
+
+ /// Process a string conversion.
+ fn string_conversion(&mut self, expression: Expr) -> Result<Option<String>>;
+
+ /// Process a character conversion.
+ fn char_conversion(&mut self, expression: Expr) -> Result<Option<String>>;
+}
+
+// Wraps a `CoreFmtFormatMacroGenerator` in a `FormatMacroGenerator` that
+// generates the format string as it goes.
+struct CoreFmtGenerator<GENERATOR: CoreFmtFormatMacroGenerator> {
+ inner: GENERATOR,
+ format_string: String,
+}
+
+impl<GENERATOR: CoreFmtFormatMacroGenerator> FormatMacroGenerator for CoreFmtGenerator<GENERATOR> {
+ fn finalize(self) -> Result<TokenStream2> {
+ self.inner.finalize(self.format_string)
+ }
+
+ fn string_fragment(&mut self, string: &str) -> Result<()> {
+ // Escape '{' and '} characters.
+ let format_string = string.replace("{", "{{").replace("}", "}}");
+
+ self.format_string.push_str(&format_string);
+ self.inner.string_fragment(string)
+ }
+
+ fn integer_conversion(
+ &mut self,
+ display: IntegerDisplayType,
+ type_width: u8, // in bits
+ expression: Expr,
+ ) -> Result<()> {
+ let (conversion, ty) = match display {
+ IntegerDisplayType::Signed => ("{}", format_ident!("i{type_width}")),
+ IntegerDisplayType::Unsigned => ("{}", format_ident!("u{type_width}")),
+ IntegerDisplayType::Octal => ("{:o}", format_ident!("u{type_width}")),
+ IntegerDisplayType::Hex => ("{:x}", format_ident!("u{type_width}")),
+ IntegerDisplayType::UpperHex => ("{:X}", format_ident!("u{type_width}")),
+ };
+
+ match self.inner.integer_conversion(ty, expression)? {
+ Some(s) => self.format_string.push_str(&s),
+ None => self.format_string.push_str(conversion),
+ }
+
+ Ok(())
+ }
+
+ fn string_conversion(&mut self, expression: Expr) -> Result<()> {
+ match self.inner.string_conversion(expression)? {
+ Some(s) => self.format_string.push_str(&s),
+ None => self.format_string.push_str("{}"),
+ }
+ Ok(())
+ }
+
+ fn char_conversion(&mut self, expression: Expr) -> Result<()> {
+ match self.inner.char_conversion(expression)? {
+ Some(s) => self.format_string.push_str(&s),
+ None => self.format_string.push_str("{}"),
+ }
+ Ok(())
+ }
+}
+
+/// Generate code for a `pw_format` style proc macro that needs a [`core::fmt`] format string.
+///
+/// `generate_core_fmt` is a specialized version of [`generate`] which works with
+/// [`CoreFmtFormatMacroGenerator`]
+pub fn generate_core_fmt(
+ generator: impl CoreFmtFormatMacroGenerator,
+ format_and_args: FormatAndArgs,
+) -> core::result::Result<TokenStream2, syn::Error> {
+ let generator = CoreFmtGenerator {
+ inner: generator,
+ format_string: "".into(),
+ };
+ generate(generator, format_and_args)
+}
diff --git a/pw_format/rust/pw_format_test_macros.rs b/pw_format/rust/pw_format_test_macros.rs
index 0ccb9fa..5740dca 100644
--- a/pw_format/rust/pw_format_test_macros.rs
+++ b/pw_format/rust/pw_format_test_macros.rs
@@ -15,8 +15,8 @@
use proc_macro::TokenStream;
use proc_macro2::Ident;
use pw_format::macros::{
- generate, generate_printf, FormatAndArgs, FormatMacroGenerator, IntegerDisplayType,
- PrintfFormatMacroGenerator, Result,
+ generate, generate_core_fmt, generate_printf, CoreFmtFormatMacroGenerator, FormatAndArgs,
+ FormatMacroGenerator, IntegerDisplayType, PrintfFormatMacroGenerator, Result,
};
use quote::quote;
use syn::{parse_macro_input, Expr};
@@ -220,3 +220,126 @@
Err(e) => e.to_compile_error().into(),
}
}
+
+// Generator for testing `generate_core_fmt()`. Allows control over the return
+// value of the conversion functions.
+struct CoreFmtTestGenerator {
+ code_fragments: Vec<TokenStream2>,
+ integer_specifier_override: Option<String>,
+ string_specifier_override: Option<String>,
+ char_specifier_override: Option<String>,
+}
+
+impl CoreFmtTestGenerator {
+ pub fn new() -> Self {
+ Self {
+ code_fragments: Vec::new(),
+ integer_specifier_override: None,
+ string_specifier_override: None,
+ char_specifier_override: None,
+ }
+ }
+
+ pub fn with_integer_specifier(mut self, specifier: &str) -> Self {
+ self.integer_specifier_override = Some(specifier.to_string());
+ self
+ }
+
+ pub fn with_string_specifier(mut self, specifier: &str) -> Self {
+ self.string_specifier_override = Some(specifier.to_string());
+ self
+ }
+
+ pub fn with_char_specifier(mut self, specifier: &str) -> Self {
+ self.char_specifier_override = Some(specifier.to_string());
+ self
+ }
+}
+
+// Until they diverge, we reuse `PrintfTestGeneratorOps` here.
+impl CoreFmtFormatMacroGenerator for CoreFmtTestGenerator {
+ fn finalize(self, format_string: String) -> Result<TokenStream2> {
+ // Create locally scoped alias so we can refer to them in `quote!()`.
+ let code_fragments = self.code_fragments;
+
+ Ok(quote! {
+ {
+ use pw_format_test_macros_test::PrintfTestGeneratorOps;
+ let mut ops = Vec::new();
+ #(#code_fragments);*;
+ ops.push(PrintfTestGeneratorOps::Finalize);
+ (#format_string, ops)
+ }
+ })
+ }
+ fn string_fragment(&mut self, string: &str) -> Result<()> {
+ self.code_fragments.push(quote! {
+ ops.push(PrintfTestGeneratorOps::StringFragment(#string.to_string()));
+ });
+ Ok(())
+ }
+ fn integer_conversion(&mut self, ty: Ident, _expression: Expr) -> Result<Option<String>> {
+ let ty_str = format!("{ty}");
+ self.code_fragments.push(quote! {
+ ops.push(PrintfTestGeneratorOps::IntegerConversion{ ty: #ty_str.to_string() });
+ });
+ Ok(self.integer_specifier_override.clone())
+ }
+
+ fn string_conversion(&mut self, _expression: Expr) -> Result<Option<String>> {
+ self.code_fragments.push(quote! {
+ ops.push(PrintfTestGeneratorOps::StringConversion);
+ });
+ Ok(self.string_specifier_override.clone())
+ }
+
+ fn char_conversion(&mut self, _expression: Expr) -> Result<Option<String>> {
+ self.code_fragments.push(quote! {
+ ops.push(PrintfTestGeneratorOps::CharConversion);
+ });
+ Ok(self.char_specifier_override.clone())
+ }
+}
+
+#[proc_macro]
+pub fn core_fmt_generator_test_macro(tokens: TokenStream) -> TokenStream {
+ let format_and_args = parse_macro_input!(tokens as FormatAndArgs);
+ let generator = CoreFmtTestGenerator::new();
+ match generate_core_fmt(generator, format_and_args) {
+ Ok(token_stream) => token_stream.into(),
+ Err(e) => e.to_compile_error().into(),
+ }
+}
+
+// Causes the generator to substitute {} with {:?}.
+#[proc_macro]
+pub fn integer_sub_core_fmt_generator_test_macro(tokens: TokenStream) -> TokenStream {
+ let format_and_args = parse_macro_input!(tokens as FormatAndArgs);
+ let generator = CoreFmtTestGenerator::new().with_integer_specifier("{:?}");
+ match generate_core_fmt(generator, format_and_args) {
+ Ok(token_stream) => token_stream.into(),
+ Err(e) => e.to_compile_error().into(),
+ }
+}
+
+// Causes the generator to substitute {} with {:?}.
+#[proc_macro]
+pub fn string_sub_core_fmt_generator_test_macro(tokens: TokenStream) -> TokenStream {
+ let format_and_args = parse_macro_input!(tokens as FormatAndArgs);
+ let generator = CoreFmtTestGenerator::new().with_string_specifier("{:?}");
+ match generate_core_fmt(generator, format_and_args) {
+ Ok(token_stream) => token_stream.into(),
+ Err(e) => e.to_compile_error().into(),
+ }
+}
+
+// Causes the generator to substitute {} with {:?}.
+#[proc_macro]
+pub fn char_sub_core_fmt_generator_test_macro(tokens: TokenStream) -> TokenStream {
+ let format_and_args = parse_macro_input!(tokens as FormatAndArgs);
+ let generator = CoreFmtTestGenerator::new().with_char_specifier("{:?}");
+ match generate_core_fmt(generator, format_and_args) {
+ Ok(token_stream) => token_stream.into(),
+ Err(e) => e.to_compile_error().into(),
+ }
+}
diff --git a/pw_format/rust/pw_format_test_macros_test.rs b/pw_format/rust/pw_format_test_macros_test.rs
index 3ae1f54..3220262 100644
--- a/pw_format/rust/pw_format_test_macros_test.rs
+++ b/pw_format/rust/pw_format_test_macros_test.rs
@@ -41,8 +41,10 @@
mod tests {
use pw_format::macros::IntegerDisplayType;
use pw_format_test_macros::{
- char_sub_printf_generator_test_macro, generator_test_macro,
- integer_sub_printf_generator_test_macro, printf_generator_test_macro,
+ char_sub_core_fmt_generator_test_macro, char_sub_printf_generator_test_macro,
+ core_fmt_generator_test_macro, generator_test_macro,
+ integer_sub_core_fmt_generator_test_macro, integer_sub_printf_generator_test_macro,
+ printf_generator_test_macro, string_sub_core_fmt_generator_test_macro,
string_sub_printf_generator_test_macro,
};
@@ -165,4 +167,94 @@
)
);
}
+
+ #[test]
+ fn generate_core_fmt_calls_generator_correctly() {
+ assert_eq!(
+ core_fmt_generator_test_macro!("test %ld %s %c", 5, "test", 'c'),
+ (
+ "test {} {} {}",
+ vec![
+ PrintfTestGeneratorOps::StringFragment("test ".to_string()),
+ PrintfTestGeneratorOps::IntegerConversion {
+ ty: "i32".to_string(),
+ },
+ PrintfTestGeneratorOps::StringFragment(" ".to_string()),
+ PrintfTestGeneratorOps::StringConversion,
+ PrintfTestGeneratorOps::StringFragment(" ".to_string()),
+ PrintfTestGeneratorOps::CharConversion,
+ PrintfTestGeneratorOps::Finalize
+ ]
+ )
+ );
+ }
+
+ // Test that a generator returning an overridden integer conversion specifier
+ // changes that and only that conversion specifier in the format string.
+ #[test]
+ fn generate_core_fmt_substitutes_integer_conversion() {
+ assert_eq!(
+ integer_sub_core_fmt_generator_test_macro!("test %ld %s %c", 5, "test", 'c'),
+ (
+ "test {:?} {} {}",
+ vec![
+ PrintfTestGeneratorOps::StringFragment("test ".to_string()),
+ PrintfTestGeneratorOps::IntegerConversion {
+ ty: "i32".to_string(),
+ },
+ PrintfTestGeneratorOps::StringFragment(" ".to_string()),
+ PrintfTestGeneratorOps::StringConversion,
+ PrintfTestGeneratorOps::StringFragment(" ".to_string()),
+ PrintfTestGeneratorOps::CharConversion,
+ PrintfTestGeneratorOps::Finalize
+ ]
+ )
+ );
+ }
+
+ // Test that a generator returning an overridden string conversion specifier
+ // changes that and only that conversion specifier in the format string.
+ #[test]
+ fn generate_core_fmt_substitutes_string_conversion() {
+ assert_eq!(
+ string_sub_core_fmt_generator_test_macro!("test %ld %s %c", 5, "test", 'c'),
+ (
+ "test {} {:?} {}",
+ vec![
+ PrintfTestGeneratorOps::StringFragment("test ".to_string()),
+ PrintfTestGeneratorOps::IntegerConversion {
+ ty: "i32".to_string(),
+ },
+ PrintfTestGeneratorOps::StringFragment(" ".to_string()),
+ PrintfTestGeneratorOps::StringConversion,
+ PrintfTestGeneratorOps::StringFragment(" ".to_string()),
+ PrintfTestGeneratorOps::CharConversion,
+ PrintfTestGeneratorOps::Finalize
+ ]
+ )
+ );
+ }
+
+ // Test that a generator returning an overridden character conversion specifier
+ // changes that and only that conversion specifier in the format string.
+ #[test]
+ fn generate_core_fmt_substitutes_char_conversion() {
+ assert_eq!(
+ char_sub_core_fmt_generator_test_macro!("test %ld %s %c", 5, "test", 'c'),
+ (
+ "test {} {} {:?}",
+ vec![
+ PrintfTestGeneratorOps::StringFragment("test ".to_string()),
+ PrintfTestGeneratorOps::IntegerConversion {
+ ty: "i32".to_string(),
+ },
+ PrintfTestGeneratorOps::StringFragment(" ".to_string()),
+ PrintfTestGeneratorOps::StringConversion,
+ PrintfTestGeneratorOps::StringFragment(" ".to_string()),
+ PrintfTestGeneratorOps::CharConversion,
+ PrintfTestGeneratorOps::Finalize
+ ]
+ )
+ );
+ }
}