--- a/crates/jrsonnet-evaluator/src/error.rs +++ b/crates/jrsonnet-evaluator/src/error.rs @@ -378,13 +378,13 @@ return Err($w$(::$i)*$({$($tt)*})?.into()) }; ($l:literal$(, $($tt:tt)*)?) => { - return Err($crate::error::ErrorKind::RuntimeError(format!($l$(, $($tt)*)?).into()).into()) + return Err($crate::error::ErrorKind::RuntimeError($crate::jrsonnet_macros::format_istr!($l$(, $($tt)*)?)).into()) }; } #[macro_export] macro_rules! runtime_error { ($l:literal$(, $($tt:tt)*)?) => { - $crate::error::Error::from($crate::error::ErrorKind::RuntimeError(format!($l$(, $($tt)*)?).into())) + $crate::error::Error::from($crate::error::ErrorKind::RuntimeError($crate::jrsonnet_macros::format_istr!($l$(, $($tt)*)?))) }; } --- a/crates/jrsonnet-evaluator/src/lib.rs +++ b/crates/jrsonnet-evaluator/src/lib.rs @@ -84,6 +84,8 @@ pub use import::*; use jrsonnet_gcmodule::{Cc, Trace}; pub use jrsonnet_interner::{IBytes, IStr}; +#[doc(hidden)] +pub use jrsonnet_macros; pub use jrsonnet_parser as parser; use jrsonnet_parser::*; pub use obj::*; --- a/crates/jrsonnet-macros/src/lib.rs +++ b/crates/jrsonnet-macros/src/lib.rs @@ -7,7 +7,7 @@ punctuated::Punctuated, spanned::Spanned, token::{self, Comma}, - Attribute, DeriveInput, Error, FnArg, GenericArgument, Ident, ItemFn, LitStr, Pat, Path, + Attribute, DeriveInput, Error, Expr, FnArg, GenericArgument, Ident, ItemFn, LitStr, Pat, Path, PathArguments, Result, ReturnType, Token, Type, }; @@ -677,3 +677,102 @@ }; }) } + +struct FormatInput { + formatting: LitStr, + arguments: Vec, +} +impl Parse for FormatInput { + fn parse(input: ParseStream) -> Result { + let formatting = input.parse()?; + let mut arguments = Vec::new(); + + while input.peek(Token![,]) { + input.parse::()?; + if input.is_empty() { + // Trailing comma + break; + } + let expr = input.parse()?; + arguments.push(expr); + } + + if !input.is_empty() { + return Err(syn::Error::new(input.span(), "unexpected trailing input")); + } + + Ok(Self { + formatting, + arguments, + }) + } +} +fn is_format_str(i: &str) -> bool { + let mut is_plain = true; + // -1 = { + // +1 = } + let mut is_bracket = 0i8; + for ele in i.chars() { + match ele { + '{' if is_bracket == -1 => { + is_bracket = 0; + } + '}' if is_bracket == -1 => { + is_plain = false; + break; + } + '}' if is_bracket == 1 => { + is_bracket = 0; + } + '{' if is_bracket == 1 => { + is_plain = false; + break; + } + '{' => { + is_bracket = -1; + } + '}' => { + is_bracket = 1; + } + _ if is_bracket != 0 => { + is_plain = false; + break; + } + _ => {} + } + } + !is_plain || is_bracket != 0 +} +impl FormatInput { + fn expand(self) -> TokenStream { + let format = self.formatting; + if is_format_str(&format.value()) { + let args = self.arguments; + quote! { + ::jrsonnet_evaluator::IStr::from(format!(#format #(, #args)*)) + } + } else { + if let Some(first) = self.arguments.first() { + return syn::Error::new( + first.span(), + "string has no formatting codes, it should not have the arguments", + ) + .into_compile_error(); + } + quote! { + ::jrsonnet_evaluator::IStr::from(#format) + } + } + } +} + +/// IStr formatting helper +/// +/// Using `format!("literal with no codes").into()` is slower than just `"literal with no codes".into()` +/// This macro looks for formatting codes in the input string, and uses +/// `format!()` only when necessary +#[proc_macro] +pub fn format_istr(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(input as FormatInput); + input.expand().into() +}