Skip to content

Commit fe11c4d

Browse files
committed
chore: Extend the benchmarks
1 parent 58dca4d commit fe11c4d

1 file changed

Lines changed: 218 additions & 13 deletions

File tree

brush-parser/benches/parser.rs

Lines changed: 218 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
//! Benchmarks for the brush-parser crate.
2+
//!
3+
//! Compares parsing approaches:
4+
//! 1. PEG parser (tokenize + peg parse)
5+
//! 2. `Winnow_str` parser (direct string parse) - when winnow-parser feature enabled
26
37
#![allow(missing_docs)]
48
#![allow(clippy::unwrap_used)]
59

610
#[cfg(unix)]
711
mod unix {
8-
use brush_parser::{Token, parse_tokens};
12+
use brush_parser::Token;
913
use criterion::Criterion;
1014

1115
fn uncached_tokenize(content: &str) -> Vec<brush_parser::Token> {
@@ -18,8 +22,21 @@ mod unix {
1822
.unwrap()
1923
}
2024

21-
fn parse(tokens: &[Token]) -> brush_parser::ast::Program {
22-
parse_tokens(tokens, &brush_parser::ParserOptions::default()).unwrap()
25+
fn parse_peg(tokens: &[Token]) -> brush_parser::ast::Program {
26+
brush_parser::parse_tokens(tokens, &brush_parser::ParserOptions::default()).unwrap()
27+
}
28+
29+
#[cfg(feature = "winnow-parser")]
30+
fn parse_winnow_str(content: &str) -> brush_parser::ast::Program {
31+
use brush_parser::{ParserOptions, SourceInfo, winnow_str};
32+
winnow_str::parse_program(content, &ParserOptions::default(), &SourceInfo::default())
33+
.unwrap()
34+
}
35+
36+
// Combined tokenize + parse functions for full pipeline comparison
37+
fn tokenize_and_parse_peg(content: &str) -> brush_parser::ast::Program {
38+
let tokens = uncached_tokenize(content);
39+
parse_peg(&tokens)
2340
}
2441

2542
const SAMPLE_SCRIPT: &str = r#"
@@ -28,36 +45,224 @@ for f in A B C; do
2845
done
2946
"#;
3047

48+
const SIMPLE_SCRIPT: &str = "echo hello world";
49+
50+
const PIPELINE_SCRIPT: &str = "cat file.txt | grep pattern | wc -l";
51+
52+
const COMPLEX_SCRIPT: &str = r#"
53+
#!/bin/bash
54+
# Complex script with multiple constructs
55+
56+
function process_file() {
57+
local file="$1"
58+
if [[ -f "$file" ]]; then
59+
while read -r line; do
60+
case "$line" in
61+
start*)
62+
echo "Starting: $line"
63+
;;
64+
end*)
65+
echo "Ending: $line"
66+
;;
67+
*)
68+
echo "Processing: $line"
69+
;;
70+
esac
71+
done < "$file"
72+
fi
73+
}
74+
75+
for i in {1..10}; do
76+
if (( i % 2 == 0 )); then
77+
echo "$i is even" | tee -a output.txt
78+
else
79+
echo "$i is odd" >> output.txt
80+
fi
81+
done
82+
83+
process_file "input.txt" && echo "Success" || echo "Failed"
84+
"#;
85+
86+
const NESTED_EXPANSIONS_SCRIPT: &str = r"
87+
# Script with deeply nested expansions (tests balanced delimiter parsing)
88+
result=$(echo $(echo $((1 + (2 * (3 - 4))))))
89+
fallback=${foo:-${bar:-${baz}}}
90+
arithmetic=$((1 + (2 * (3 + (4 - 5)))))
91+
command_subst=$(ls $(pwd))
92+
mixed=$(echo $((1 + 2)) | cat)
93+
backtick=`echo (nested parens)`
94+
";
95+
96+
// Extended test expression benchmarks - various patterns
97+
#[allow(dead_code)]
98+
const EXTENDED_TEST_SIMPLE: &str = "[[ -f file.txt ]]";
99+
#[allow(dead_code)]
100+
const EXTENDED_TEST_BINARY: &str = "[[ $a == $b ]]";
101+
#[allow(dead_code)]
102+
const EXTENDED_TEST_REGEX: &str = "[[ $str =~ ^[0-9]+$ ]]";
103+
#[allow(dead_code)]
104+
const EXTENDED_TEST_COMPLEX_REGEX: &str = "[[ $input =~ ^(foo|bar)[0-9]+(baz|qux)$ ]]";
105+
#[allow(dead_code)]
106+
const EXTENDED_TEST_LOGICAL: &str = "[[ -f file.txt && -r file.txt || -w other.txt ]]";
107+
#[allow(dead_code)]
108+
const EXTENDED_TEST_NESTED: &str = "[[ ( -f $file && -r $file ) || ( -d $dir && -x $dir ) ]]";
109+
#[allow(dead_code)]
110+
const EXTENDED_TEST_COMPLEX: &str =
111+
"[[ ! ( $a -eq 5 && $b -gt 10 ) || ( $c =~ pattern && -f $file ) ]]";
112+
31113
fn benchmark_parsing_script_using_caches(c: &mut Criterion, script_path: &std::path::Path) {
32114
let contents = std::fs::read_to_string(script_path).unwrap();
115+
let filename = script_path.file_name().unwrap().to_string_lossy();
33116

34-
c.bench_function(
35-
std::format!(
36-
"parse_{}",
37-
script_path.file_name().unwrap().to_string_lossy()
38-
)
39-
.as_str(),
40-
|b| b.iter(|| parse(&cacheable_tokenize(contents.as_str()))),
41-
);
117+
c.bench_function(std::format!("parse_peg_{filename}").as_str(), |b| {
118+
b.iter(|| parse_peg(&cacheable_tokenize(contents.as_str())));
119+
});
42120
}
43121

44122
pub(crate) fn criterion_benchmark(c: &mut Criterion) {
45123
const POSSIBLE_BASH_COMPLETION_SCRIPT_PATH: &str =
46124
"/usr/share/bash-completion/bash_completion";
47125

126+
// Tokenization benchmark (applies to both parsers)
48127
c.bench_function("tokenize_sample_script", |b| {
49128
b.iter(|| uncached_tokenize(SAMPLE_SCRIPT));
50129
});
51130

52-
let tokens = uncached_tokenize(SAMPLE_SCRIPT);
53-
c.bench_function("parse_sample_script", |b| b.iter(|| parse(&tokens)));
131+
// Simple script benchmarks
132+
let simple_tokens = uncached_tokenize(SIMPLE_SCRIPT);
133+
c.bench_function("parse_peg_simple", |b| b.iter(|| parse_peg(&simple_tokens)));
134+
#[cfg(feature = "winnow-parser")]
135+
c.bench_function("parse_winnow_str_simple", |b| {
136+
b.iter(|| parse_winnow_str(SIMPLE_SCRIPT));
137+
});
138+
139+
// Pipeline script benchmarks
140+
let pipeline_tokens = uncached_tokenize(PIPELINE_SCRIPT);
141+
c.bench_function("parse_peg_pipeline", |b| {
142+
b.iter(|| parse_peg(&pipeline_tokens));
143+
});
144+
#[cfg(feature = "winnow-parser")]
145+
c.bench_function("parse_winnow_str_pipeline", |b| {
146+
b.iter(|| parse_winnow_str(PIPELINE_SCRIPT));
147+
});
148+
149+
// Sample script (for loop) benchmarks
150+
let sample_tokens = uncached_tokenize(SAMPLE_SCRIPT);
151+
c.bench_function("parse_peg_for_loop", |b| {
152+
b.iter(|| parse_peg(&sample_tokens));
153+
});
154+
#[cfg(feature = "winnow-parser")]
155+
c.bench_function("parse_winnow_str_for_loop", |b| {
156+
b.iter(|| parse_winnow_str(SAMPLE_SCRIPT));
157+
});
158+
159+
// Complex script benchmarks
160+
let complex_tokens = uncached_tokenize(COMPLEX_SCRIPT);
161+
c.bench_function("parse_peg_complex", |b| {
162+
b.iter(|| parse_peg(&complex_tokens));
163+
});
164+
#[cfg(feature = "winnow-parser")]
165+
c.bench_function("parse_winnow_str_complex", |b| {
166+
b.iter(|| parse_winnow_str(COMPLEX_SCRIPT));
167+
});
54168

169+
// Real-world bash completion script (if available)
55170
let well_known_complicated_script =
56171
std::path::PathBuf::from(POSSIBLE_BASH_COMPLETION_SCRIPT_PATH);
57172

58173
if well_known_complicated_script.exists() {
59174
benchmark_parsing_script_using_caches(c, &well_known_complicated_script);
60175
}
176+
177+
// ========================================================================
178+
// FULL PIPELINE BENCHMARKS (tokenize + parse)
179+
// ========================================================================
180+
// These benchmarks measure the complete parsing pipeline from string to AST,
181+
// allowing fair comparison between different approaches:
182+
// - tokenize_and_parse_peg: Legacy tokenizer + PEG parser
183+
// - parse_winnow_str: Direct string parsing (no separate tokenization)
184+
185+
// Simple script full pipeline
186+
c.bench_function("full_peg_simple", |b| {
187+
b.iter(|| tokenize_and_parse_peg(SIMPLE_SCRIPT));
188+
});
189+
#[cfg(feature = "winnow-parser")]
190+
c.bench_function("full_winnow_str_simple", |b| {
191+
b.iter(|| parse_winnow_str(SIMPLE_SCRIPT));
192+
});
193+
194+
// Pipeline script full pipeline
195+
c.bench_function("full_peg_pipeline", |b| {
196+
b.iter(|| tokenize_and_parse_peg(PIPELINE_SCRIPT));
197+
});
198+
#[cfg(feature = "winnow-parser")]
199+
c.bench_function("full_winnow_str_pipeline", |b| {
200+
b.iter(|| parse_winnow_str(PIPELINE_SCRIPT));
201+
});
202+
203+
// For loop full pipeline
204+
c.bench_function("full_peg_for_loop", |b| {
205+
b.iter(|| tokenize_and_parse_peg(SAMPLE_SCRIPT));
206+
});
207+
#[cfg(feature = "winnow-parser")]
208+
c.bench_function("full_winnow_str_for_loop", |b| {
209+
b.iter(|| parse_winnow_str(SAMPLE_SCRIPT));
210+
});
211+
212+
// Complex script full pipeline
213+
c.bench_function("full_peg_complex", |b| {
214+
b.iter(|| tokenize_and_parse_peg(COMPLEX_SCRIPT));
215+
});
216+
#[cfg(feature = "winnow-parser")]
217+
c.bench_function("full_winnow_str_complex", |b| {
218+
b.iter(|| parse_winnow_str(COMPLEX_SCRIPT));
219+
});
220+
221+
// Nested expansions (balanced delimiter parsing stress test)
222+
c.bench_function("full_peg_nested_expansions", |b| {
223+
b.iter(|| tokenize_and_parse_peg(NESTED_EXPANSIONS_SCRIPT));
224+
});
225+
#[cfg(feature = "winnow-parser")]
226+
c.bench_function("full_winnow_str_nested_expansions", |b| {
227+
b.iter(|| parse_winnow_str(NESTED_EXPANSIONS_SCRIPT));
228+
});
229+
230+
// ========================================================================
231+
// EXTENDED TEST EXPRESSION BENCHMARKS
232+
// ========================================================================
233+
// Benchmarks for the refactored extended test ([[ ]]) parser
234+
// Tests various patterns: simple, binary, regex, logical operators, nesting
235+
236+
#[cfg(feature = "winnow-parser")]
237+
{
238+
c.bench_function("extended_test_simple", |b| {
239+
b.iter(|| parse_winnow_str(EXTENDED_TEST_SIMPLE));
240+
});
241+
242+
c.bench_function("extended_test_binary", |b| {
243+
b.iter(|| parse_winnow_str(EXTENDED_TEST_BINARY));
244+
});
245+
246+
c.bench_function("extended_test_regex", |b| {
247+
b.iter(|| parse_winnow_str(EXTENDED_TEST_REGEX));
248+
});
249+
250+
c.bench_function("extended_test_complex_regex", |b| {
251+
b.iter(|| parse_winnow_str(EXTENDED_TEST_COMPLEX_REGEX));
252+
});
253+
254+
c.bench_function("extended_test_logical", |b| {
255+
b.iter(|| parse_winnow_str(EXTENDED_TEST_LOGICAL));
256+
});
257+
258+
c.bench_function("extended_test_nested", |b| {
259+
b.iter(|| parse_winnow_str(EXTENDED_TEST_NESTED));
260+
});
261+
262+
c.bench_function("extended_test_complex", |b| {
263+
b.iter(|| parse_winnow_str(EXTENDED_TEST_COMPLEX));
264+
});
265+
}
61266
}
62267
}
63268

0 commit comments

Comments
 (0)