Skip to content

Commit d15f13e

Browse files
DaniPopesrplusq
authored andcommitted
refactor: rewrite the console.log format string parser (foundry-rs#8913)
* refactor: rewrite the console.log format string parser * chore: clippy
1 parent f5a09d5 commit d15f13e

1 file changed

Lines changed: 163 additions & 75 deletions

File tree

crates/common/fmt/src/console.rs

Lines changed: 163 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,175 @@
11
use super::UIfmt;
22
use alloy_primitives::{Address, Bytes, FixedBytes, I256, U256};
3-
use std::iter::Peekable;
3+
use std::fmt::{self, Write};
4+
5+
/// A piece is a portion of the format string which represents the next part to emit.
6+
#[derive(Clone, Debug, PartialEq, Eq)]
7+
pub enum Piece<'a> {
8+
/// A literal string which should directly be emitted.
9+
String(&'a str),
10+
/// A format specifier which should be replaced with the next argument.
11+
NextArgument(FormatSpec),
12+
}
413

514
/// A format specifier.
6-
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
15+
#[derive(Clone, Debug, Default, PartialEq, Eq)]
716
pub enum FormatSpec {
8-
/// %s format spec
17+
/// `%s`
918
#[default]
1019
String,
11-
/// %d format spec
20+
/// `%d`
1221
Number,
13-
/// %i format spec
22+
/// `%i`
1423
Integer,
15-
/// %o format spec
24+
/// `%o`
1625
Object,
17-
/// %e format spec with an optional precision
26+
/// `%e`, `%18e`
1827
Exponential(Option<usize>),
19-
/// %x format spec
28+
/// `%x`
2029
Hexadecimal,
2130
}
2231

23-
impl FormatSpec {
24-
fn from_chars<I>(iter: &mut Peekable<I>) -> Result<Self, String>
25-
where
26-
I: Iterator<Item = char>,
27-
{
28-
match iter.next().ok_or_else(String::new)? {
29-
's' => Ok(Self::String),
30-
'd' => Ok(Self::Number),
31-
'i' => Ok(Self::Integer),
32-
'o' => Ok(Self::Object),
33-
'e' => Ok(Self::Exponential(None)),
34-
'x' => Ok(Self::Hexadecimal),
35-
ch if ch.is_ascii_digit() => {
36-
let mut num = ch.to_string();
37-
while let Some(&ch) = iter.peek() {
38-
if ch.is_ascii_digit() {
39-
num.push(ch);
40-
iter.next();
41-
} else {
42-
break;
43-
}
32+
impl fmt::Display for FormatSpec {
33+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34+
f.write_str("%")?;
35+
match *self {
36+
Self::String => f.write_str("s"),
37+
Self::Number => f.write_str("d"),
38+
Self::Integer => f.write_str("i"),
39+
Self::Object => f.write_str("o"),
40+
Self::Exponential(Some(n)) => write!(f, "{n}e"),
41+
Self::Exponential(None) => f.write_str("e"),
42+
Self::Hexadecimal => f.write_str("x"),
43+
}
44+
}
45+
}
46+
47+
enum ParseArgError {
48+
/// Failed to parse the argument.
49+
Err,
50+
/// Escape `%%`.
51+
Skip,
52+
}
53+
54+
/// Parses a format string into a sequence of [pieces][Piece].
55+
#[derive(Debug)]
56+
pub struct Parser<'a> {
57+
input: &'a str,
58+
chars: std::str::CharIndices<'a>,
59+
}
60+
61+
impl<'a> Parser<'a> {
62+
/// Creates a new parser for the given input.
63+
pub fn new(input: &'a str) -> Self {
64+
Self { input, chars: input.char_indices() }
65+
}
66+
67+
/// Parses a string until the next format specifier.
68+
///
69+
/// `skip` is the number of format specifier characters (`%`) to ignore before returning the
70+
/// string.
71+
fn string(&mut self, start: usize, mut skip: usize) -> &'a str {
72+
while let Some((pos, c)) = self.peek() {
73+
if c == '%' {
74+
if skip == 0 {
75+
return &self.input[start..pos];
4476
}
45-
if let Some(&ch) = iter.peek() {
46-
if ch == 'e' {
47-
let num = num.parse().map_err(|_| num)?;
48-
iter.next();
49-
Ok(Self::Exponential(Some(num)))
50-
} else {
51-
Err(num)
52-
}
53-
} else {
54-
Err(num)
77+
skip -= 1;
78+
}
79+
self.chars.next();
80+
}
81+
&self.input[start..]
82+
}
83+
84+
/// Parses a format specifier.
85+
///
86+
/// If `Err` is returned, the internal iterator may have been advanced and it may be in an
87+
/// invalid state.
88+
fn argument(&mut self) -> Result<FormatSpec, ParseArgError> {
89+
let (start, ch) = self.peek().ok_or(ParseArgError::Err)?;
90+
let simple_spec = match ch {
91+
's' => Some(FormatSpec::String),
92+
'd' => Some(FormatSpec::Number),
93+
'i' => Some(FormatSpec::Integer),
94+
'o' => Some(FormatSpec::Object),
95+
'e' => Some(FormatSpec::Exponential(None)),
96+
'x' => Some(FormatSpec::Hexadecimal),
97+
// "%%" is a literal '%'.
98+
'%' => return Err(ParseArgError::Skip),
99+
_ => None,
100+
};
101+
if let Some(spec) = simple_spec {
102+
self.chars.next();
103+
return Ok(spec);
104+
}
105+
106+
// %<n>e
107+
if ch.is_ascii_digit() {
108+
let n = self.integer(start);
109+
if let Some((_, 'e')) = self.peek() {
110+
self.chars.next();
111+
return Ok(FormatSpec::Exponential(n));
112+
}
113+
}
114+
115+
Err(ParseArgError::Err)
116+
}
117+
118+
fn integer(&mut self, start: usize) -> Option<usize> {
119+
let mut end = start;
120+
while let Some((pos, ch)) = self.peek() {
121+
if !ch.is_ascii_digit() {
122+
end = pos;
123+
break;
124+
}
125+
self.chars.next();
126+
}
127+
self.input[start..end].parse().ok()
128+
}
129+
130+
fn current_pos(&mut self) -> usize {
131+
self.peek().map(|(n, _)| n).unwrap_or(self.input.len())
132+
}
133+
134+
fn peek(&mut self) -> Option<(usize, char)> {
135+
self.peek_n(0)
136+
}
137+
138+
fn peek_n(&mut self, n: usize) -> Option<(usize, char)> {
139+
self.chars.clone().nth(n)
140+
}
141+
}
142+
143+
impl<'a> Iterator for Parser<'a> {
144+
type Item = Piece<'a>;
145+
146+
fn next(&mut self) -> Option<Self::Item> {
147+
let (mut start, ch) = self.peek()?;
148+
let mut skip = 0;
149+
if ch == '%' {
150+
let prev = self.chars.clone();
151+
self.chars.next();
152+
match self.argument() {
153+
Ok(arg) => {
154+
debug_assert_eq!(arg.to_string(), self.input[start..self.current_pos()]);
155+
return Some(Piece::NextArgument(arg));
156+
}
157+
158+
// Skip the argument if we encountered "%%".
159+
Err(ParseArgError::Skip) => {
160+
start = self.current_pos();
161+
skip += 1;
162+
}
163+
164+
// Reset the iterator if we failed to parse the argument, and include any
165+
// parsed and unparsed specifier in `String`.
166+
Err(ParseArgError::Err) => {
167+
self.chars = prev;
168+
skip += 1;
55169
}
56170
}
57-
ch => Err(String::from(ch)),
58171
}
172+
Some(Piece::String(self.string(start, skip)))
59173
}
60174
}
61175

@@ -249,7 +363,7 @@ impl ConsoleFmt for [u8] {
249363
/// assert_eq!(formatted, "foo has 3 characters");
250364
/// ```
251365
pub fn console_format(spec: &str, values: &[&dyn ConsoleFmt]) -> String {
252-
let mut values = values.iter().copied().peekable();
366+
let mut values = values.iter().copied();
253367
let mut result = String::with_capacity(spec.len());
254368

255369
// for the first space
@@ -275,45 +389,19 @@ pub fn console_format(spec: &str, values: &[&dyn ConsoleFmt]) -> String {
275389

276390
fn format_spec<'a>(
277391
s: &str,
278-
values: &mut Peekable<impl Iterator<Item = &'a dyn ConsoleFmt>>,
392+
mut values: impl Iterator<Item = &'a dyn ConsoleFmt>,
279393
result: &mut String,
280394
) {
281-
let mut expect_fmt = false;
282-
let mut chars = s.chars().peekable();
283-
284-
while chars.peek().is_some() {
285-
if expect_fmt {
286-
expect_fmt = false;
287-
match FormatSpec::from_chars(&mut chars) {
288-
Ok(spec) => {
289-
let value = values.next().expect("value existence is checked");
290-
// format and write the value
395+
for piece in Parser::new(s) {
396+
match piece {
397+
Piece::String(s) => result.push_str(s),
398+
Piece::NextArgument(spec) => {
399+
if let Some(value) = values.next() {
291400
result.push_str(&value.fmt(spec));
292-
}
293-
Err(consumed) => {
294-
// on parser failure, write '%' and consumed characters
295-
result.push('%');
296-
result.push_str(&consumed);
297-
}
298-
}
299-
} else {
300-
let ch = chars.next().unwrap();
301-
if ch == '%' {
302-
if let Some(&next_ch) = chars.peek() {
303-
if next_ch == '%' {
304-
result.push('%');
305-
chars.next();
306-
} else if values.peek().is_some() {
307-
// only try formatting if there are values to format
308-
expect_fmt = true;
309-
} else {
310-
result.push(ch);
311-
}
312401
} else {
313-
result.push(ch);
402+
// Write the format specifier as-is if there are no more values.
403+
write!(result, "{spec}").unwrap();
314404
}
315-
} else {
316-
result.push(ch);
317405
}
318406
}
319407
}

0 commit comments

Comments
 (0)