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) ]
711mod 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
2845done
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