• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

shnewto / bnf / 21568168972

01 Feb 2026 06:40PM UTC coverage: 98.241% (+0.8%) from 97.491%
21568168972

push

github

web-flow
hide anonymous nonterminals for clean public api (#193)

313 of 315 new or added lines in 3 files covered. (99.37%)

1 existing line in 1 file now uncovered.

2736 of 2785 relevant lines covered (98.24%)

21884.83 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.76
/src/parser/mod.rs
1
pub(crate) mod grammar;
2

3
use crate::ParseTree;
4
use crate::error::Error;
5
use crate::grammar::Grammar;
6
use crate::term::Term;
7
use grammar::ParseGrammar;
8
use std::collections::HashSet;
9
use std::rc::Rc;
10

11
/// A reusable parser built from a `Grammar` that validates all nonterminals are defined
12
/// at construction time.
13
///
14
/// # Example
15
///
16
/// ```rust
17
/// use bnf::Grammar;
18
///
19
/// let grammar: Grammar = "<dna> ::= <base> | <base> <dna>
20
/// <base> ::= 'A' | 'C' | 'G' | 'T'"
21
///     .parse()
22
///     .unwrap();
23
///
24
/// let parser = grammar.build_parser()?;
25
/// let parse_trees: Vec<_> = parser.parse_input("GATTACA").collect();
26
/// # Ok::<(), bnf::Error>(())
27
/// ```
28
#[derive(Debug)]
29
pub struct GrammarParser<'gram> {
30
    starting_term: &'gram Term,
31
    parse_grammar: Rc<ParseGrammar<'gram>>,
32
}
33

34
impl<'gram> GrammarParser<'gram> {
35
    /// Construct a new `GrammarParser` from a `Grammar`, validating that all
36
    /// nonterminals referenced in productions have definitions.
37
    ///
38
    /// # Errors
39
    ///
40
    /// Returns `Error::ValidationError` if any nonterminal used in the RHS of
41
    /// productions lacks a definition in the grammar.
42
    pub fn new(grammar: &'gram Grammar) -> Result<Self, Error> {
416✔
43
        validate_nonterminals(grammar)?;
416✔
44
        let starting_term = grammar.starting_term().ok_or_else(|| {
323✔
45
            Error::ValidationError("Grammar must have at least one production".to_string())
1✔
46
        })?;
1✔
47
        let parse_grammar = Rc::new(ParseGrammar::new(grammar));
322✔
48
        Ok(Self {
322✔
49
            starting_term,
322✔
50
            parse_grammar,
322✔
51
        })
322✔
52
    }
416✔
53

54
    /// Parse an input string using the grammar's starting nonterminal.
55
    ///
56
    /// Returns an iterator over all possible parse trees for the input.
57
    pub fn parse_input(&self, input: &'gram str) -> impl Iterator<Item = ParseTree<'gram>> {
319✔
58
        self.parse_input_starting_with(input, self.starting_term)
319✔
59
    }
319✔
60

61
    /// Parse an input string starting with the given term (nonterminal or terminal).
62
    ///
63
    /// Returns an iterator over all possible parse trees for the input.
64
    pub fn parse_input_starting_with(
423✔
65
        &self,
423✔
66
        input: &'gram str,
423✔
67
        start: &'gram Term,
423✔
68
    ) -> impl Iterator<Item = ParseTree<'gram>> {
423✔
69
        crate::earley::parse_starting_with_grammar(&self.parse_grammar, input, start)
423✔
70
    }
423✔
71
}
72

73
/// Validate that all nonterminals referenced in the grammar have definitions.
74
///
75
/// # Errors
76
///
77
/// Returns `Error::ValidationError` with a message listing all undefined nonterminals.
78
fn validate_nonterminals(grammar: &Grammar) -> Result<(), Error> {
416✔
79
    // Collect all nonterminals defined in LHS of productions
80
    let mut defined_nonterminals = HashSet::new();
416✔
81
    for production in grammar.productions_iter() {
1,189✔
82
        if let Term::Nonterminal(ref nt) = production.lhs {
1,189✔
83
            defined_nonterminals.insert(nt.clone());
1,189✔
84
        }
1,189✔
85
    }
86

87
    // Collect all nonterminals used in RHS of all productions
88
    let mut referenced_nonterminals = HashSet::new();
416✔
89
    for production in grammar.productions_iter() {
1,189✔
90
        for expression in production.rhs_iter() {
1,966✔
91
            for term in expression.terms_iter() {
3,156✔
92
                if let Term::Nonterminal(nt) = term {
3,156✔
93
                    referenced_nonterminals.insert(nt.clone());
1,281✔
94
                }
1,875✔
95
            }
96
        }
97
    }
98

99
    // Find undefined nonterminals
100
    let undefined: Vec<String> = referenced_nonterminals
416✔
101
        .difference(&defined_nonterminals)
416✔
102
        .cloned()
416✔
103
        .collect();
416✔
104

105
    if !undefined.is_empty() {
416✔
106
        let message = format!(
93✔
107
            "Undefined nonterminals: {}",
108
            undefined
93✔
109
                .iter()
93✔
110
                .map(|nt| format!("<{nt}>"))
102✔
111
                .collect::<Vec<_>>()
93✔
112
                .join(", ")
93✔
113
        );
114
        return Err(Error::ValidationError(message));
93✔
115
    }
323✔
116

117
    Ok(())
323✔
118
}
416✔
119

120
#[cfg(test)]
121
mod tests {
122
    use super::*;
123
    use crate::Grammar;
124
    use crate::expression::Expression;
125
    use crate::production::Production;
126
    use quickcheck::{Arbitrary, Gen, QuickCheck, TestResult};
127

128
    #[test]
129
    fn parser_construction_with_valid_grammar() {
1✔
130
        let grammar: Grammar = "<dna> ::= <base> | <base> <dna>
1✔
131
        <base> ::= 'A' | 'C' | 'G' | 'T'"
1✔
132
            .parse()
1✔
133
            .unwrap();
1✔
134

135
        let parser = grammar.build_parser();
1✔
136
        assert!(
1✔
137
            parser.is_ok(),
1✔
138
            "Parser should be constructible from valid grammar"
139
        );
140
    }
1✔
141

142
    #[test]
143
    fn parser_construction_fails_with_empty_grammar() {
1✔
144
        let grammar = Grammar::from_parts(vec![]);
1✔
145
        let parser = grammar.build_parser();
1✔
146
        assert!(
1✔
147
            parser.is_err(),
1✔
148
            "Parser construction should fail with empty grammar"
149
        );
150
        assert!(
1✔
151
            matches!(parser.unwrap_err(), Error::ValidationError(_)),
1✔
152
            "Error should be ValidationError about missing productions"
153
        );
154
    }
1✔
155

156
    #[test]
157
    fn parser_validation_with_group_containing_undefined() {
1✔
158
        // Test that validation fails when a grouped term references an undefined nonterminal
159
        let grammar: Grammar = "<start> ::= (<undefined>)".parse().unwrap();
1✔
160
        let parser = grammar.build_parser();
1✔
161
        assert!(
1✔
162
            parser.is_err(),
1✔
163
            "Parser should fail when group contains undefined nonterminal"
164
        );
165
        assert!(matches!(parser.unwrap_err(), Error::ValidationError(_)));
1✔
166
    }
1✔
167

168
    #[test]
169
    fn parser_validation_with_group_containing_defined() {
1✔
170
        // Test that validation succeeds when a group contains a defined nonterminal
171
        let grammar: Grammar = r#"<start> ::= (<base>)
1✔
172
<base> ::= 'A'"#
1✔
173
            .parse()
1✔
174
            .unwrap();
1✔
175
        let parser = grammar.build_parser();
1✔
176
        assert!(
1✔
177
            parser.is_ok(),
1✔
178
            "Parser should succeed when group contains defined nonterminal"
179
        );
180
    }
1✔
181

182
    #[test]
183
    fn normalization_groups_and_optionals_produce_named_nonterminals() {
1✔
184
        // Regression: extended syntax ( ) and [ ] is normalized to __anon* nonterminals
185
        let grammar: Grammar = r#"<s> ::= (<a> | <b>) [<c>]
1✔
186
<a> ::= 'a'
1✔
187
<b> ::= 'b'
1✔
188
<c> ::= 'c'"#
1✔
189
            .parse()
1✔
190
            .unwrap();
1✔
191
        for prod in grammar.productions_iter() {
6✔
192
            for expr in prod.rhs_iter() {
8✔
193
                for term in expr.terms_iter() {
9✔
194
                    match term {
9✔
195
                        crate::Term::Terminal(_) | crate::Term::Nonterminal(_) => {}
9✔
196
                    }
197
                }
198
            }
199
        }
200
        let parser = grammar.build_parser().unwrap();
1✔
201
        assert!(parser.parse_input("a").next().is_some());
1✔
202
        assert!(parser.parse_input("ac").next().is_some());
1✔
203
        assert!(parser.parse_input("").next().is_none());
1✔
204
    }
1✔
205

206
    #[test]
207
    fn parse_empty_optional_bnf() {
1✔
208
        // BNF optional [A] normalizes to __anon* with '' alternative; "" and "x" both parse
209
        let grammar: Grammar = r#"<s> ::= [<x>]
1✔
210
<x> ::= 'x'"#
1✔
211
            .parse()
1✔
212
            .unwrap();
1✔
213
        let parser = grammar.build_parser().unwrap();
1✔
214
        assert!(parser.parse_input("").next().is_some());
1✔
215
        assert!(parser.parse_input("x").next().is_some());
1✔
216
    }
1✔
217

218
    #[test]
219
    fn user_defined_anon_name_no_collision() {
1✔
220
        // User-defined <__anon0> should not collide with generated anon names for groups
221
        let grammar: Grammar = r#"<__anon0> ::= 'a'
1✔
222
<s> ::= (<__anon0>)"#
1✔
223
            .parse()
1✔
224
            .unwrap();
1✔
225
        let parser = grammar.build_parser().unwrap();
1✔
226
        assert!(parser.parse_input("a").next().is_some());
1✔
227
        // Grammar should contain both user's __anon0 and a generated anon for the group
228
        let lhs_names: Vec<_> = grammar
1✔
229
            .productions_iter()
1✔
230
            .map(|p| match &p.lhs {
3✔
231
                crate::Term::Nonterminal(n) => n.as_str(),
3✔
NEW
232
                _ => "",
×
233
            })
3✔
234
            .collect();
1✔
235
        assert!(lhs_names.contains(&"__anon0"));
1✔
236
        assert!(lhs_names.iter().any(|n| n.starts_with("__anon")));
1✔
237
    }
1✔
238

239
    #[test]
240
    fn parser_construction_fails_with_undefined_nonterminal() {
1✔
241
        let grammar: Grammar = "<dna> ::= <base> | <base> <dna>
1✔
242
        <base> ::= <undefined>"
1✔
243
            .parse()
1✔
244
            .unwrap();
1✔
245

246
        let parser = grammar.build_parser();
1✔
247
        assert!(
1✔
248
            parser.is_err(),
1✔
249
            "Parser construction should fail with undefined nonterminal"
250
        );
251
        assert!(
1✔
252
            matches!(parser.unwrap_err(), Error::ValidationError(_)),
1✔
253
            "Error should be ValidationError"
254
        );
255
    }
1✔
256

257
    #[test]
258
    fn parser_can_parse_multiple_inputs() {
1✔
259
        let grammar: Grammar = "<dna> ::= <base> | <base> <dna>
1✔
260
        <base> ::= 'A' | 'C' | 'G' | 'T'"
1✔
261
            .parse()
1✔
262
            .unwrap();
1✔
263

264
        let parser = grammar.build_parser().unwrap();
1✔
265

266
        let input1 = "GATTACA";
1✔
267
        let input2 = "ATCG";
1✔
268

269
        let parse_trees1: Vec<_> = parser.parse_input(input1).collect();
1✔
270
        let parse_trees2: Vec<_> = parser.parse_input(input2).collect();
1✔
271

272
        assert!(!parse_trees1.is_empty(), "Should parse first input");
1✔
273
        assert!(!parse_trees2.is_empty(), "Should parse second input");
1✔
274
    }
1✔
275

276
    #[test]
277
    fn parser_accepts_explicit_starting_nonterminal() {
1✔
278
        let grammar: Grammar = "<base> ::= 'A' | 'C' | 'G' | 'T'
1✔
279
        <dna> ::= <base> | <base> <dna>"
1✔
280
            .parse()
1✔
281
            .unwrap();
1✔
282

283
        let parser = grammar.build_parser().unwrap();
1✔
284
        let input = "GATTACA";
1✔
285
        let start_term = crate::term!(<dna>);
1✔
286

287
        let parse_trees: Vec<_> = parser
1✔
288
            .parse_input_starting_with(input, &start_term)
1✔
289
            .collect();
1✔
290

291
        assert!(
1✔
292
            !parse_trees.is_empty(),
1✔
293
            "Should parse with explicit starting nonterminal"
294
        );
295
    }
1✔
296

297
    #[test]
298
    fn parser_accepts_explicit_starting_terminal() {
1✔
299
        let grammar: Grammar = "<base> ::= 'A' | 'C' | 'G' | 'T'
1✔
300
        <dna> ::= <base> | <base> <dna>"
1✔
301
            .parse()
1✔
302
            .unwrap();
1✔
303

304
        let parser = grammar.build_parser().unwrap();
1✔
305
        let input = "G";
1✔
306
        let start_term = crate::term!("G");
1✔
307

308
        // Note: Starting with a terminal directly doesn't work with Earley parser
309
        // since it expects a nonterminal to have productions. This test verifies
310
        // the API accepts terminals, but they won't produce parse trees unless
311
        // there's a production with that terminal as LHS (which is invalid).
312
        let parse_trees: Vec<_> = parser
1✔
313
            .parse_input_starting_with(input, &start_term)
1✔
314
            .collect();
1✔
315

316
        // This will be empty since there's no production with a terminal as LHS
317
        // The API accepts it, but it won't produce results
318
        assert_eq!(
1✔
319
            parse_trees.len(),
1✔
320
            0,
321
            "Terminal starting term produces no parse trees"
322
        );
323
    }
1✔
324

325
    #[test]
326
    fn parser_is_order_independent() {
1✔
327
        // Create grammar with productions in one order
328
        let grammar1: Grammar = "<dna> ::= <base> | <base> <dna>
1✔
329
        <base> ::= 'A' | 'C' | 'G' | 'T'"
1✔
330
            .parse()
1✔
331
            .unwrap();
1✔
332

333
        // Create same grammar with productions in different order
334
        let grammar2: Grammar = "<base> ::= 'A' | 'C' | 'G' | 'T'
1✔
335
        <dna> ::= <base> | <base> <dna>"
1✔
336
            .parse()
1✔
337
            .unwrap();
1✔
338

339
        let parser1 = grammar1.build_parser().unwrap();
1✔
340
        let parser2 = grammar2.build_parser().unwrap();
1✔
341

342
        let input = "GATTACA";
1✔
343
        // Use explicit starting term to ensure both use the same starting point
344
        let start_term = crate::term!(<dna>);
1✔
345

346
        let parse_trees1: Vec<_> = parser1
1✔
347
            .parse_input_starting_with(input, &start_term)
1✔
348
            .collect();
1✔
349
        let parse_trees2: Vec<_> = parser2
1✔
350
            .parse_input_starting_with(input, &start_term)
1✔
351
            .collect();
1✔
352

353
        // Results should be identical regardless of production order when using
354
        // the same explicit starting term
355
        assert_eq!(
1✔
356
            parse_trees1.len(),
1✔
357
            parse_trees2.len(),
1✔
358
            "Should produce same number of parse trees regardless of order"
359
        );
360
    }
1✔
361

362
    // Helper: Generate a simple valid grammar with known structure
363
    #[derive(Debug, Clone)]
364
    struct SimpleValidGrammar(Grammar);
365
    impl Arbitrary for SimpleValidGrammar {
366
        fn arbitrary(g: &mut Gen) -> Self {
100✔
367
            // Generate 1-5 nonterminal names
368
            let num_nonterms = usize::arbitrary(g) % 5 + 1;
100✔
369
            let nonterms: Vec<String> = (0..num_nonterms).map(|i| format!("nt{}", i)).collect();
300✔
370

371
            let mut productions = Vec::new();
100✔
372

373
            // Create productions: each nonterminal references only defined ones
374
            for (idx, nt) in nonterms.iter().enumerate() {
300✔
375
                let mut expressions = Vec::new();
300✔
376

377
                // Each production has 1-3 alternatives
378
                let num_alternatives = usize::arbitrary(g) % 3 + 1;
300✔
379
                for _ in 0..num_alternatives {
300✔
380
                    let mut terms = Vec::new();
580✔
381

382
                    // Each alternative has 1-3 terms
383
                    let num_terms = usize::arbitrary(g) % 3 + 1;
580✔
384
                    for _ in 0..num_terms {
580✔
385
                        if bool::arbitrary(g) && idx > 0 {
1,140✔
386
                            // Reference a previously defined nonterminal
387
                            let ref_idx = usize::arbitrary(g) % idx;
359✔
388
                            if let Some(nt) = nonterms.get(ref_idx) {
359✔
389
                                terms.push(Term::Nonterminal(nt.clone()));
359✔
390
                            } else {
359✔
391
                                // Use a terminal if index is out of bounds
×
392
                                let term_str = String::arbitrary(g);
×
393
                                terms.push(Term::Terminal(term_str));
×
394
                            }
×
395
                        } else {
781✔
396
                            // Use a terminal
781✔
397
                            let term_str = String::arbitrary(g);
781✔
398
                            terms.push(Term::Terminal(term_str));
781✔
399
                        }
781✔
400
                    }
401

402
                    expressions.push(Expression::from_parts(terms));
580✔
403
                }
404

405
                productions.push(Production::from_parts(
300✔
406
                    Term::Nonterminal(nt.clone()),
300✔
407
                    expressions,
300✔
408
                ));
409
            }
410

411
            Self(Grammar::from_parts(productions))
100✔
412
        }
100✔
413
    }
414

415
    // Helper: Generate grammar that may have undefined nonterminals
416
    #[derive(Debug, Clone)]
417
    struct GrammarWithUndefined(Grammar);
418
    impl Arbitrary for GrammarWithUndefined {
419
        fn arbitrary(g: &mut Gen) -> Self {
200✔
420
            let num_nonterms = usize::arbitrary(g) % 4 + 1;
200✔
421
            let mut nonterms: Vec<String> = (0..num_nonterms).map(|i| format!("nt{}", i)).collect();
497✔
422

423
            // Add some undefined nonterminals
424
            let num_undefined = usize::arbitrary(g) % 3;
200✔
425
            for i in 0..num_undefined {
200✔
426
                nonterms.push(format!("undefined{}", i));
185✔
427
            }
185✔
428

429
            let mut productions = Vec::new();
200✔
430
            let defined_count = num_nonterms;
200✔
431

432
            for (idx, nt) in nonterms.iter().enumerate() {
682✔
433
                if idx >= defined_count {
682✔
434
                    // Don't define the undefined nonterminals
435
                    continue;
185✔
436
                }
497✔
437

438
                let mut expressions = Vec::new();
497✔
439
                let num_alternatives = usize::arbitrary(g) % 2 + 1;
497✔
440

441
                for _ in 0..num_alternatives {
497✔
442
                    let mut terms = Vec::new();
761✔
443
                    let num_terms = usize::arbitrary(g) % 2 + 1;
761✔
444

445
                    for _ in 0..num_terms {
761✔
446
                        if bool::arbitrary(g) && !nonterms.is_empty() {
1,129✔
447
                            // Reference any nonterminal (may be undefined)
448
                            let ref_idx = usize::arbitrary(g) % nonterms.len();
576✔
449
                            if let Some(nt) = nonterms.get(ref_idx) {
576✔
450
                                terms.push(Term::Nonterminal(nt.clone()));
576✔
451
                            } else {
576✔
452
                                terms.push(Term::Terminal(String::arbitrary(g)));
×
453
                            }
×
454
                        } else {
553✔
455
                            terms.push(Term::Terminal(String::arbitrary(g)));
553✔
456
                        }
553✔
457
                    }
458

459
                    expressions.push(Expression::from_parts(terms));
761✔
460
                }
461

462
                productions.push(Production::from_parts(
497✔
463
                    Term::Nonterminal(nt.clone()),
497✔
464
                    expressions,
497✔
465
                ));
466
            }
467

468
            Self(Grammar::from_parts(productions))
200✔
469
        }
200✔
470
    }
471

472
    // Property test: Parser construction fails if any nonterminal lacks definition
473
    fn prop_parser_fails_with_undefined_nonterminal(grammar: GrammarWithUndefined) -> TestResult {
100✔
474
        let grammar = grammar.0;
100✔
475

476
        // Collect all nonterminals defined in LHS
477
        let mut defined = std::collections::HashSet::new();
100✔
478
        for production in grammar.productions_iter() {
256✔
479
            if let Term::Nonterminal(nt) = &production.lhs {
256✔
480
                defined.insert(nt.clone());
256✔
481
            }
256✔
482
        }
483

484
        // Collect all nonterminals used in RHS
485
        let mut referenced = std::collections::HashSet::new();
100✔
486
        for production in grammar.productions_iter() {
256✔
487
            for expression in production.rhs_iter() {
395✔
488
                for term in expression.terms_iter() {
576✔
489
                    if let Term::Nonterminal(nt) = term {
576✔
490
                        referenced.insert(nt.clone());
285✔
491
                    }
291✔
492
                }
493
            }
494
        }
495

496
        // Find undefined nonterminals
497
        let undefined: Vec<_> = referenced.difference(&defined).cloned().collect();
100✔
498

499
        let parser_result = grammar.build_parser();
100✔
500

501
        if undefined.is_empty() {
100✔
502
            // All nonterminals are defined, parser should succeed
503
            TestResult::from_bool(parser_result.is_ok())
53✔
504
        } else {
505
            // Some nonterminals are undefined, parser should fail
506
            TestResult::from_bool(
47✔
507
                parser_result.is_err()
47✔
508
                    && matches!(parser_result.unwrap_err(), Error::ValidationError(_)),
47✔
509
            )
510
        }
511
    }
100✔
512

513
    #[test]
514
    fn parser_fails_with_undefined_nonterminal() {
1✔
515
        QuickCheck::new().tests(100).quickcheck(
1✔
516
            prop_parser_fails_with_undefined_nonterminal as fn(GrammarWithUndefined) -> TestResult,
1✔
517
        );
518
    }
1✔
519

520
    // Helper: Generate valid grammar with at least 2 productions
521
    #[derive(Debug, Clone)]
522
    struct ValidGrammarWithMultipleProductions(Grammar);
523
    impl Arbitrary for ValidGrammarWithMultipleProductions {
524
        fn arbitrary(g: &mut Gen) -> Self {
50✔
525
            // Generate 2-5 nonterminals
526
            let num_nonterms = usize::arbitrary(g) % 4 + 2;
50✔
527
            let nonterms: Vec<String> = (0..num_nonterms).map(|i| format!("nt{}", i)).collect();
172✔
528

529
            let mut productions = Vec::new();
50✔
530

531
            for (idx, nt) in nonterms.iter().enumerate() {
172✔
532
                let mut expressions = Vec::new();
172✔
533
                let num_alternatives = usize::arbitrary(g) % 2 + 1;
172✔
534

535
                for _ in 0..num_alternatives {
172✔
536
                    let mut terms = Vec::new();
272✔
537
                    let num_terms = usize::arbitrary(g) % 2 + 1;
272✔
538

539
                    for _ in 0..num_terms {
272✔
540
                        if bool::arbitrary(g) && idx > 0 {
398✔
541
                            // Reference a previously defined nonterminal
542
                            let ref_idx = usize::arbitrary(g) % idx;
150✔
543
                            if let Some(nt) = nonterms.get(ref_idx) {
150✔
544
                                terms.push(Term::Nonterminal(nt.clone()));
150✔
545
                            } else {
150✔
546
                                terms.push(Term::Terminal(String::arbitrary(g)));
×
547
                            }
×
548
                        } else {
248✔
549
                            terms.push(Term::Terminal(String::arbitrary(g)));
248✔
550
                        }
248✔
551
                    }
552

553
                    expressions.push(Expression::from_parts(terms));
272✔
554
                }
555

556
                productions.push(Production::from_parts(
172✔
557
                    Term::Nonterminal(nt.clone()),
172✔
558
                    expressions,
172✔
559
                ));
560
            }
561

562
            Self(Grammar::from_parts(productions))
50✔
563
        }
50✔
564
    }
565

566
    // Property test: Parser results are identical regardless of production order
567
    fn prop_parser_order_independent(grammar: ValidGrammarWithMultipleProductions) -> TestResult {
50✔
568
        let grammar = grammar.0;
50✔
569

570
        // Create a shuffled version of the grammar
571
        let mut productions: Vec<_> = grammar.productions_iter().cloned().collect();
50✔
572
        let mut rng = rand::rng();
50✔
573
        rand::seq::SliceRandom::shuffle(productions.as_mut_slice(), &mut rng);
50✔
574

575
        let grammar1 = grammar;
50✔
576
        let grammar2 = Grammar::from_parts(productions);
50✔
577

578
        let parser1 = match grammar1.build_parser() {
50✔
579
            Ok(p) => p,
50✔
580
            Err(_) => return TestResult::discard(),
×
581
        };
582
        let parser2 = match grammar2.build_parser() {
50✔
583
            Ok(p) => p,
50✔
584
            Err(_) => return TestResult::discard(),
×
585
        };
586

587
        // Get starting term from first grammar
588
        let starting_term = match grammar1.starting_term() {
50✔
589
            Some(t) => t,
50✔
590
            None => return TestResult::discard(),
×
591
        };
592

593
        // Generate a test sentence
594
        let sentence = match grammar1.generate() {
50✔
595
            Ok(s) => s,
50✔
596
            Err(_) => return TestResult::discard(),
×
597
        };
598

599
        // Parse with both parsers using explicit starting term
600
        let parse_trees1: Vec<_> = parser1
50✔
601
            .parse_input_starting_with(&sentence, starting_term)
50✔
602
            .collect();
50✔
603
        let parse_trees2: Vec<_> = parser2
50✔
604
            .parse_input_starting_with(&sentence, starting_term)
50✔
605
            .collect();
50✔
606

607
        // Results should be identical
608
        TestResult::from_bool(parse_trees1.len() == parse_trees2.len())
50✔
609
    }
50✔
610

611
    #[test]
612
    fn parser_order_independent() {
1✔
613
        QuickCheck::new().tests(50).quickcheck(
1✔
614
            prop_parser_order_independent as fn(ValidGrammarWithMultipleProductions) -> TestResult,
1✔
615
        );
616
    }
1✔
617

618
    // Property test: Parser can be reused multiple times with same results
619
    fn prop_parser_reusable(grammar: SimpleValidGrammar) -> TestResult {
100✔
620
        let grammar = grammar.0;
100✔
621

622
        // Only test with grammars that can generate
623
        if !grammar.terminates() {
100✔
624
            return TestResult::discard();
×
625
        }
100✔
626

627
        let parser = match grammar.build_parser() {
100✔
628
            Ok(p) => p,
100✔
629
            Err(_) => return TestResult::discard(),
×
630
        };
631

632
        // Generate a sentence
633
        let sentence = match grammar.generate() {
100✔
634
            Ok(s) => s,
100✔
635
            Err(_) => return TestResult::discard(),
×
636
        };
637

638
        // Parse the same sentence multiple times
639
        let parse_trees1: Vec<_> = parser.parse_input(&sentence).collect();
100✔
640
        let parse_trees2: Vec<_> = parser.parse_input(&sentence).collect();
100✔
641
        let parse_trees3: Vec<_> = parser.parse_input(&sentence).collect();
100✔
642

643
        // All results should be identical
644
        TestResult::from_bool(
100✔
645
            parse_trees1.len() == parse_trees2.len() && parse_trees2.len() == parse_trees3.len(),
100✔
646
        )
647
    }
100✔
648

649
    #[test]
650
    fn parser_reusable() {
1✔
651
        QuickCheck::new()
1✔
652
            .tests(100)
1✔
653
            .quickcheck(prop_parser_reusable as fn(SimpleValidGrammar) -> TestResult);
1✔
654
    }
1✔
655

656
    // Property test: Parser validation catches all undefined nonterminals
657
    // Simplified: Build grammars with known undefined nonterminals
658
    fn prop_validation_catches_all_undefined(grammar: GrammarWithUndefined) -> TestResult {
100✔
659
        let grammar = grammar.0;
100✔
660

661
        // Collect all nonterminals defined in LHS
662
        let mut defined = std::collections::HashSet::new();
100✔
663
        for production in grammar.productions_iter() {
241✔
664
            if let Term::Nonterminal(nt) = &production.lhs {
241✔
665
                defined.insert(nt.clone());
241✔
666
            }
241✔
667
        }
668

669
        // Collect all nonterminals used in RHS
670
        let mut referenced = std::collections::HashSet::new();
100✔
671
        for production in grammar.productions_iter() {
241✔
672
            for expression in production.rhs_iter() {
366✔
673
                for term in expression.terms_iter() {
553✔
674
                    if let Term::Nonterminal(nt) = term {
553✔
675
                        referenced.insert(nt.clone());
291✔
676
                    }
291✔
677
                }
678
            }
679
        }
680

681
        let undefined: Vec<_> = referenced.difference(&defined).cloned().collect();
100✔
682

683
        let parser_result = grammar.build_parser();
100✔
684

685
        match parser_result {
44✔
686
            Ok(_) => {
687
                // Parser succeeded, so there should be no undefined nonterminals
688
                TestResult::from_bool(undefined.is_empty())
56✔
689
            }
690
            Err(Error::ValidationError(msg)) => {
44✔
691
                // Parser failed, error message should mention all undefined nonterminals
692
                let all_mentioned = undefined
44✔
693
                    .iter()
44✔
694
                    .all(|nt| msg.contains(&format!("<{nt}>")) || msg.contains(nt));
47✔
695
                TestResult::from_bool(!undefined.is_empty() && all_mentioned)
44✔
696
            }
697
            Err(_) => TestResult::error("Expected ValidationError"),
×
698
        }
699
    }
100✔
700

701
    #[test]
702
    fn validation_catches_all_undefined() {
1✔
703
        QuickCheck::new().tests(100).quickcheck(
1✔
704
            prop_validation_catches_all_undefined as fn(GrammarWithUndefined) -> TestResult,
1✔
705
        );
706
    }
1✔
707
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc