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

shnewto / bnf / 21340572542

25 Jan 2026 10:20PM UTC coverage: 97.607% (+0.9%) from 96.753%
21340572542

Pull #187

github

web-flow
Merge c5c3e937c into fbcc780ec
Pull Request #187: add explicit parser

603 of 620 new or added lines in 10 files covered. (97.26%)

1 existing line in 1 file now uncovered.

2529 of 2591 relevant lines covered (97.61%)

23354.58 hits per line

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

95.74
/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> {
410✔
43
        validate_nonterminals(grammar)?;
410✔
44
        let starting_term = grammar.starting_term().ok_or_else(|| {
337✔
45
            Error::ValidationError("Grammar must have at least one production".to_string())
1✔
46
        })?;
1✔
47
        let parse_grammar = Rc::new(ParseGrammar::new(grammar));
336✔
48
        Ok(Self {
336✔
49
            starting_term,
336✔
50
            parse_grammar,
336✔
51
        })
336✔
52
    }
410✔
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>> {
302✔
58
        self.parse_input_starting_with(input, self.starting_term)
302✔
59
    }
302✔
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(
406✔
65
        &self,
406✔
66
        input: &'gram str,
406✔
67
        start: &'gram Term,
406✔
68
    ) -> impl Iterator<Item = ParseTree<'gram>> {
406✔
69
        crate::earley::parse_starting_with_grammar(&self.parse_grammar, input, start)
406✔
70
    }
406✔
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> {
410✔
79
    // Collect all nonterminals defined in LHS of productions
80
    let mut defined_nonterminals = HashSet::new();
410✔
81
    for production in grammar.productions_iter() {
1,114✔
82
        if let Term::Nonterminal(ref nt) = production.lhs {
1,114✔
83
            defined_nonterminals.insert(nt.clone());
1,114✔
84
        }
1,114✔
85
    }
86

87
    // Collect all nonterminals used in RHS of all productions
88
    let mut referenced_nonterminals = HashSet::new();
410✔
89
    for production in grammar.productions_iter() {
1,114✔
90
        for expression in production.rhs_iter() {
1,846✔
91
            for term in expression.terms_iter() {
2,974✔
92
                match term {
2,974✔
93
                    Term::Nonterminal(nt) => {
1,179✔
94
                        referenced_nonterminals.insert(nt.clone());
1,179✔
95
                    }
1,179✔
96
                    Term::AnonymousNonterminal(exprs) => {
2✔
97
                        // For anonymous nonterminals, check the expressions they contain
98
                        for expr in exprs {
2✔
99
                            for inner_term in expr.terms_iter() {
2✔
100
                                if let Term::Nonterminal(nt) = inner_term {
2✔
101
                                    referenced_nonterminals.insert(nt.clone());
2✔
102
                                }
2✔
103
                            }
104
                        }
105
                    }
106
                    Term::Terminal(_) => {
1,793✔
107
                        // Terminals don't need definitions
1,793✔
108
                    }
1,793✔
109
                }
110
            }
111
        }
112
    }
113

114
    // Find undefined nonterminals
115
    let undefined: Vec<String> = referenced_nonterminals
410✔
116
        .difference(&defined_nonterminals)
410✔
117
        .cloned()
410✔
118
        .collect();
410✔
119

120
    if !undefined.is_empty() {
410✔
121
        let message = format!(
73✔
122
            "Undefined nonterminals: {}",
123
            undefined
73✔
124
                .iter()
73✔
125
                .map(|nt| format!("<{nt}>"))
79✔
126
                .collect::<Vec<_>>()
73✔
127
                .join(", ")
73✔
128
        );
129
        return Err(Error::ValidationError(message));
73✔
130
    }
337✔
131

132
    Ok(())
337✔
133
}
410✔
134

135
#[cfg(test)]
136
mod tests {
137
    use super::*;
138
    use crate::Grammar;
139
    use crate::expression::Expression;
140
    use crate::production::Production;
141
    use quickcheck::{Arbitrary, Gen, QuickCheck, TestResult};
142

143
    #[test]
144
    fn parser_construction_with_valid_grammar() {
1✔
145
        let grammar: Grammar = "<dna> ::= <base> | <base> <dna>
1✔
146
        <base> ::= 'A' | 'C' | 'G' | 'T'"
1✔
147
            .parse()
1✔
148
            .unwrap();
1✔
149

150
        let parser = grammar.build_parser();
1✔
151
        assert!(
1✔
152
            parser.is_ok(),
1✔
153
            "Parser should be constructible from valid grammar"
154
        );
155
    }
1✔
156

157
    #[test]
158
    fn parser_construction_fails_with_empty_grammar() {
1✔
159
        let grammar = Grammar::from_parts(vec![]);
1✔
160
        let parser = grammar.build_parser();
1✔
161
        assert!(
1✔
162
            parser.is_err(),
1✔
163
            "Parser construction should fail with empty grammar"
164
        );
165
        assert!(
1✔
166
            matches!(parser.unwrap_err(), Error::ValidationError(_)),
1✔
167
            "Error should be ValidationError about missing productions"
168
        );
169
    }
1✔
170

171
    #[test]
172
    fn parser_validation_with_anonymous_nonterminal_containing_undefined() {
1✔
173
        // Test that validation checks nonterminals inside anonymous nonterminals
174
        let expr = crate::expression!(<undefined>);
1✔
175
        let anon = Term::AnonymousNonterminal(vec![expr]);
1✔
176
        let production = Production::from_parts(
1✔
177
            crate::term!(<start>),
1✔
178
            vec![Expression::from_parts(vec![anon])],
1✔
179
        );
180
        let grammar = Grammar::from_parts(vec![production]);
1✔
181
        let parser = grammar.build_parser();
1✔
182
        assert!(
1✔
183
            parser.is_err(),
1✔
184
            "Parser should fail when anonymous nonterminal contains undefined nonterminal"
185
        );
186
        assert!(matches!(parser.unwrap_err(), Error::ValidationError(_)));
1✔
187
    }
1✔
188

189
    #[test]
190
    fn parser_validation_with_anonymous_nonterminal_containing_defined() {
1✔
191
        // Test that validation works correctly when anonymous nonterminal contains defined nonterminal
192
        let expr = crate::expression!(<base>);
1✔
193
        let anon = Term::AnonymousNonterminal(vec![expr]);
1✔
194
        let production1 = Production::from_parts(
1✔
195
            crate::term!(<start>),
1✔
196
            vec![Expression::from_parts(vec![anon])],
1✔
197
        );
198
        let production2 = crate::production!(<base> ::= 'A');
1✔
199
        let grammar = Grammar::from_parts(vec![production1, production2]);
1✔
200
        let parser = grammar.build_parser();
1✔
201
        assert!(
1✔
202
            parser.is_ok(),
1✔
203
            "Parser should succeed when anonymous nonterminal contains defined nonterminal"
204
        );
205
    }
1✔
206

207
    #[test]
208
    fn parser_construction_fails_with_undefined_nonterminal() {
1✔
209
        let grammar: Grammar = "<dna> ::= <base> | <base> <dna>
1✔
210
        <base> ::= <undefined>"
1✔
211
            .parse()
1✔
212
            .unwrap();
1✔
213

214
        let parser = grammar.build_parser();
1✔
215
        assert!(
1✔
216
            parser.is_err(),
1✔
217
            "Parser construction should fail with undefined nonterminal"
218
        );
219
        assert!(
1✔
220
            matches!(parser.unwrap_err(), Error::ValidationError(_)),
1✔
221
            "Error should be ValidationError"
222
        );
223
    }
1✔
224

225
    #[test]
226
    fn parser_can_parse_multiple_inputs() {
1✔
227
        let grammar: Grammar = "<dna> ::= <base> | <base> <dna>
1✔
228
        <base> ::= 'A' | 'C' | 'G' | 'T'"
1✔
229
            .parse()
1✔
230
            .unwrap();
1✔
231

232
        let parser = grammar.build_parser().unwrap();
1✔
233

234
        let input1 = "GATTACA";
1✔
235
        let input2 = "ATCG";
1✔
236

237
        let parse_trees1: Vec<_> = parser.parse_input(input1).collect();
1✔
238
        let parse_trees2: Vec<_> = parser.parse_input(input2).collect();
1✔
239

240
        assert!(!parse_trees1.is_empty(), "Should parse first input");
1✔
241
        assert!(!parse_trees2.is_empty(), "Should parse second input");
1✔
242
    }
1✔
243

244
    #[test]
245
    fn parser_accepts_explicit_starting_nonterminal() {
1✔
246
        let grammar: Grammar = "<base> ::= 'A' | 'C' | 'G' | 'T'
1✔
247
        <dna> ::= <base> | <base> <dna>"
1✔
248
            .parse()
1✔
249
            .unwrap();
1✔
250

251
        let parser = grammar.build_parser().unwrap();
1✔
252
        let input = "GATTACA";
1✔
253
        let start_term = crate::term!(<dna>);
1✔
254

255
        let parse_trees: Vec<_> = parser
1✔
256
            .parse_input_starting_with(input, &start_term)
1✔
257
            .collect();
1✔
258

259
        assert!(
1✔
260
            !parse_trees.is_empty(),
1✔
261
            "Should parse with explicit starting nonterminal"
262
        );
263
    }
1✔
264

265
    #[test]
266
    fn parser_accepts_explicit_starting_terminal() {
1✔
267
        let grammar: Grammar = "<base> ::= 'A' | 'C' | 'G' | 'T'
1✔
268
        <dna> ::= <base> | <base> <dna>"
1✔
269
            .parse()
1✔
270
            .unwrap();
1✔
271

272
        let parser = grammar.build_parser().unwrap();
1✔
273
        let input = "G";
1✔
274
        let start_term = crate::term!("G");
1✔
275

276
        // Note: Starting with a terminal directly doesn't work with Earley parser
277
        // since it expects a nonterminal to have productions. This test verifies
278
        // the API accepts terminals, but they won't produce parse trees unless
279
        // there's a production with that terminal as LHS (which is invalid).
280
        let parse_trees: Vec<_> = parser
1✔
281
            .parse_input_starting_with(input, &start_term)
1✔
282
            .collect();
1✔
283

284
        // This will be empty since there's no production with a terminal as LHS
285
        // The API accepts it, but it won't produce results
286
        assert_eq!(
1✔
287
            parse_trees.len(),
1✔
288
            0,
289
            "Terminal starting term produces no parse trees"
290
        );
291
    }
1✔
292

293
    #[test]
294
    fn parser_is_order_independent() {
1✔
295
        // Create grammar with productions in one order
296
        let grammar1: Grammar = "<dna> ::= <base> | <base> <dna>
1✔
297
        <base> ::= 'A' | 'C' | 'G' | 'T'"
1✔
298
            .parse()
1✔
299
            .unwrap();
1✔
300

301
        // Create same grammar with productions in different order
302
        let grammar2: Grammar = "<base> ::= 'A' | 'C' | 'G' | 'T'
1✔
303
        <dna> ::= <base> | <base> <dna>"
1✔
304
            .parse()
1✔
305
            .unwrap();
1✔
306

307
        let parser1 = grammar1.build_parser().unwrap();
1✔
308
        let parser2 = grammar2.build_parser().unwrap();
1✔
309

310
        let input = "GATTACA";
1✔
311
        // Use explicit starting term to ensure both use the same starting point
312
        let start_term = crate::term!(<dna>);
1✔
313

314
        let parse_trees1: Vec<_> = parser1
1✔
315
            .parse_input_starting_with(input, &start_term)
1✔
316
            .collect();
1✔
317
        let parse_trees2: Vec<_> = parser2
1✔
318
            .parse_input_starting_with(input, &start_term)
1✔
319
            .collect();
1✔
320

321
        // Results should be identical regardless of production order when using
322
        // the same explicit starting term
323
        assert_eq!(
1✔
324
            parse_trees1.len(),
1✔
325
            parse_trees2.len(),
1✔
326
            "Should produce same number of parse trees regardless of order"
327
        );
328
    }
1✔
329

330
    // Helper: Generate a simple valid grammar with known structure
331
    #[derive(Debug, Clone)]
332
    struct SimpleValidGrammar(Grammar);
333
    impl Arbitrary for SimpleValidGrammar {
334
        fn arbitrary(g: &mut Gen) -> Self {
100✔
335
            // Generate 1-5 nonterminal names
336
            let num_nonterms = usize::arbitrary(g) % 5 + 1;
100✔
337
            let nonterms: Vec<String> = (0..num_nonterms).map(|i| format!("nt{}", i)).collect();
275✔
338

339
            let mut productions = Vec::new();
100✔
340

341
            // Create productions: each nonterminal references only defined ones
342
            for (idx, nt) in nonterms.iter().enumerate() {
275✔
343
                let mut expressions = Vec::new();
275✔
344

345
                // Each production has 1-3 alternatives
346
                let num_alternatives = usize::arbitrary(g) % 3 + 1;
275✔
347
                for _ in 0..num_alternatives {
275✔
348
                    let mut terms = Vec::new();
533✔
349

350
                    // Each alternative has 1-3 terms
351
                    let num_terms = usize::arbitrary(g) % 3 + 1;
533✔
352
                    for _ in 0..num_terms {
533✔
353
                        if bool::arbitrary(g) && idx > 0 {
1,010✔
354
                            // Reference a previously defined nonterminal
355
                            let ref_idx = usize::arbitrary(g) % idx;
329✔
356
                            if let Some(nt) = nonterms.get(ref_idx) {
329✔
357
                                terms.push(Term::Nonterminal(nt.clone()));
329✔
358
                            } else {
329✔
NEW
359
                                // Use a terminal if index is out of bounds
×
NEW
360
                                let term_str = String::arbitrary(g);
×
NEW
361
                                terms.push(Term::Terminal(term_str));
×
NEW
362
                            }
×
363
                        } else {
681✔
364
                            // Use a terminal
681✔
365
                            let term_str = String::arbitrary(g);
681✔
366
                            terms.push(Term::Terminal(term_str));
681✔
367
                        }
681✔
368
                    }
369

370
                    expressions.push(Expression::from_parts(terms));
533✔
371
                }
372

373
                productions.push(Production::from_parts(
275✔
374
                    Term::Nonterminal(nt.clone()),
275✔
375
                    expressions,
275✔
376
                ));
377
            }
378

379
            Self(Grammar::from_parts(productions))
100✔
380
        }
100✔
381
    }
382

383
    // Helper: Generate grammar that may have undefined nonterminals
384
    #[derive(Debug, Clone)]
385
    struct GrammarWithUndefined(Grammar);
386
    impl Arbitrary for GrammarWithUndefined {
387
        fn arbitrary(g: &mut Gen) -> Self {
200✔
388
            let num_nonterms = usize::arbitrary(g) % 4 + 1;
200✔
389
            let mut nonterms: Vec<String> = (0..num_nonterms).map(|i| format!("nt{}", i)).collect();
492✔
390

391
            // Add some undefined nonterminals
392
            let num_undefined = usize::arbitrary(g) % 3;
200✔
393
            for i in 0..num_undefined {
200✔
394
                nonterms.push(format!("undefined{}", i));
172✔
395
            }
172✔
396

397
            let mut productions = Vec::new();
200✔
398
            let defined_count = num_nonterms;
200✔
399

400
            for (idx, nt) in nonterms.iter().enumerate() {
664✔
401
                if idx >= defined_count {
664✔
402
                    // Don't define the undefined nonterminals
403
                    continue;
172✔
404
                }
492✔
405

406
                let mut expressions = Vec::new();
492✔
407
                let num_alternatives = usize::arbitrary(g) % 2 + 1;
492✔
408

409
                for _ in 0..num_alternatives {
492✔
410
                    let mut terms = Vec::new();
743✔
411
                    let num_terms = usize::arbitrary(g) % 2 + 1;
743✔
412

413
                    for _ in 0..num_terms {
743✔
414
                        if bool::arbitrary(g) && !nonterms.is_empty() {
1,117✔
415
                            // Reference any nonterminal (may be undefined)
416
                            let ref_idx = usize::arbitrary(g) % nonterms.len();
550✔
417
                            if let Some(nt) = nonterms.get(ref_idx) {
550✔
418
                                terms.push(Term::Nonterminal(nt.clone()));
550✔
419
                            } else {
550✔
NEW
420
                                terms.push(Term::Terminal(String::arbitrary(g)));
×
NEW
421
                            }
×
422
                        } else {
567✔
423
                            terms.push(Term::Terminal(String::arbitrary(g)));
567✔
424
                        }
567✔
425
                    }
426

427
                    expressions.push(Expression::from_parts(terms));
743✔
428
                }
429

430
                productions.push(Production::from_parts(
492✔
431
                    Term::Nonterminal(nt.clone()),
492✔
432
                    expressions,
492✔
433
                ));
434
            }
435

436
            Self(Grammar::from_parts(productions))
200✔
437
        }
200✔
438
    }
439

440
    // Property test: Parser construction fails if any nonterminal lacks definition
441
    fn prop_parser_fails_with_undefined_nonterminal(grammar: GrammarWithUndefined) -> TestResult {
100✔
442
        let grammar = grammar.0;
100✔
443

444
        // Collect all nonterminals defined in LHS
445
        let mut defined = std::collections::HashSet::new();
100✔
446
        for production in grammar.productions_iter() {
248✔
447
            if let Term::Nonterminal(nt) = &production.lhs {
248✔
448
                defined.insert(nt.clone());
248✔
449
            }
248✔
450
        }
451

452
        // Collect all nonterminals used in RHS
453
        let mut referenced = std::collections::HashSet::new();
100✔
454
        for production in grammar.productions_iter() {
248✔
455
            for expression in production.rhs_iter() {
375✔
456
                for term in expression.terms_iter() {
545✔
457
                    if let Term::Nonterminal(nt) = term {
545✔
458
                        referenced.insert(nt.clone());
283✔
459
                    }
283✔
460
                }
461
            }
462
        }
463

464
        // Find undefined nonterminals
465
        let undefined: Vec<_> = referenced.difference(&defined).cloned().collect();
100✔
466

467
        let parser_result = grammar.build_parser();
100✔
468

469
        if undefined.is_empty() {
100✔
470
            // All nonterminals are defined, parser should succeed
471
            TestResult::from_bool(parser_result.is_ok())
64✔
472
        } else {
473
            // Some nonterminals are undefined, parser should fail
474
            TestResult::from_bool(
36✔
475
                parser_result.is_err()
36✔
476
                    && matches!(parser_result.unwrap_err(), Error::ValidationError(_)),
36✔
477
            )
478
        }
479
    }
100✔
480

481
    #[test]
482
    fn parser_fails_with_undefined_nonterminal() {
1✔
483
        QuickCheck::new().tests(100).quickcheck(
1✔
484
            prop_parser_fails_with_undefined_nonterminal as fn(GrammarWithUndefined) -> TestResult,
1✔
485
        );
486
    }
1✔
487

488
    // Helper: Generate valid grammar with at least 2 productions
489
    #[derive(Debug, Clone)]
490
    struct ValidGrammarWithMultipleProductions(Grammar);
491
    impl Arbitrary for ValidGrammarWithMultipleProductions {
492
        fn arbitrary(g: &mut Gen) -> Self {
50✔
493
            // Generate 2-5 nonterminals
494
            let num_nonterms = usize::arbitrary(g) % 4 + 2;
50✔
495
            let nonterms: Vec<String> = (0..num_nonterms).map(|i| format!("nt{}", i)).collect();
165✔
496

497
            let mut productions = Vec::new();
50✔
498

499
            for (idx, nt) in nonterms.iter().enumerate() {
165✔
500
                let mut expressions = Vec::new();
165✔
501
                let num_alternatives = usize::arbitrary(g) % 2 + 1;
165✔
502

503
                for _ in 0..num_alternatives {
165✔
504
                    let mut terms = Vec::new();
264✔
505
                    let num_terms = usize::arbitrary(g) % 2 + 1;
264✔
506

507
                    for _ in 0..num_terms {
264✔
508
                        if bool::arbitrary(g) && idx > 0 {
399✔
509
                            // Reference a previously defined nonterminal
510
                            let ref_idx = usize::arbitrary(g) % idx;
139✔
511
                            if let Some(nt) = nonterms.get(ref_idx) {
139✔
512
                                terms.push(Term::Nonterminal(nt.clone()));
139✔
513
                            } else {
139✔
NEW
514
                                terms.push(Term::Terminal(String::arbitrary(g)));
×
NEW
515
                            }
×
516
                        } else {
260✔
517
                            terms.push(Term::Terminal(String::arbitrary(g)));
260✔
518
                        }
260✔
519
                    }
520

521
                    expressions.push(Expression::from_parts(terms));
264✔
522
                }
523

524
                productions.push(Production::from_parts(
165✔
525
                    Term::Nonterminal(nt.clone()),
165✔
526
                    expressions,
165✔
527
                ));
528
            }
529

530
            Self(Grammar::from_parts(productions))
50✔
531
        }
50✔
532
    }
533

534
    // Property test: Parser results are identical regardless of production order
535
    fn prop_parser_order_independent(grammar: ValidGrammarWithMultipleProductions) -> TestResult {
50✔
536
        let grammar = grammar.0;
50✔
537

538
        // Create a shuffled version of the grammar
539
        let mut productions: Vec<_> = grammar.productions_iter().cloned().collect();
50✔
540
        let mut rng = rand::rng();
50✔
541
        rand::seq::SliceRandom::shuffle(productions.as_mut_slice(), &mut rng);
50✔
542

543
        let grammar1 = grammar;
50✔
544
        let grammar2 = Grammar::from_parts(productions);
50✔
545

546
        let parser1 = match grammar1.build_parser() {
50✔
547
            Ok(p) => p,
50✔
NEW
548
            Err(_) => return TestResult::discard(),
×
549
        };
550
        let parser2 = match grammar2.build_parser() {
50✔
551
            Ok(p) => p,
50✔
NEW
552
            Err(_) => return TestResult::discard(),
×
553
        };
554

555
        // Get starting term from first grammar
556
        let starting_term = match grammar1.starting_term() {
50✔
557
            Some(t) => t,
50✔
NEW
558
            None => return TestResult::discard(),
×
559
        };
560

561
        // Generate a test sentence
562
        let sentence = match grammar1.generate() {
50✔
563
            Ok(s) => s,
50✔
NEW
564
            Err(_) => return TestResult::discard(),
×
565
        };
566

567
        // Parse with both parsers using explicit starting term
568
        let parse_trees1: Vec<_> = parser1
50✔
569
            .parse_input_starting_with(&sentence, starting_term)
50✔
570
            .collect();
50✔
571
        let parse_trees2: Vec<_> = parser2
50✔
572
            .parse_input_starting_with(&sentence, starting_term)
50✔
573
            .collect();
50✔
574

575
        // Results should be identical
576
        TestResult::from_bool(parse_trees1.len() == parse_trees2.len())
50✔
577
    }
50✔
578

579
    #[test]
580
    fn parser_order_independent() {
1✔
581
        QuickCheck::new().tests(50).quickcheck(
1✔
582
            prop_parser_order_independent as fn(ValidGrammarWithMultipleProductions) -> TestResult,
1✔
583
        );
584
    }
1✔
585

586
    // Property test: Parser can be reused multiple times with same results
587
    fn prop_parser_reusable(grammar: SimpleValidGrammar) -> TestResult {
100✔
588
        let grammar = grammar.0;
100✔
589

590
        // Only test with grammars that can generate
591
        if !grammar.terminates() {
100✔
NEW
592
            return TestResult::discard();
×
593
        }
100✔
594

595
        let parser = match grammar.build_parser() {
100✔
596
            Ok(p) => p,
100✔
NEW
597
            Err(_) => return TestResult::discard(),
×
598
        };
599

600
        // Generate a sentence
601
        let sentence = match grammar.generate() {
100✔
602
            Ok(s) => s,
100✔
NEW
603
            Err(_) => return TestResult::discard(),
×
604
        };
605

606
        // Parse the same sentence multiple times
607
        let parse_trees1: Vec<_> = parser.parse_input(&sentence).collect();
100✔
608
        let parse_trees2: Vec<_> = parser.parse_input(&sentence).collect();
100✔
609
        let parse_trees3: Vec<_> = parser.parse_input(&sentence).collect();
100✔
610

611
        // All results should be identical
612
        TestResult::from_bool(
100✔
613
            parse_trees1.len() == parse_trees2.len() && parse_trees2.len() == parse_trees3.len(),
100✔
614
        )
615
    }
100✔
616

617
    #[test]
618
    fn parser_reusable() {
1✔
619
        QuickCheck::new()
1✔
620
            .tests(100)
1✔
621
            .quickcheck(prop_parser_reusable as fn(SimpleValidGrammar) -> TestResult);
1✔
622
    }
1✔
623

624
    // Property test: Parser validation catches all undefined nonterminals
625
    // Simplified: Build grammars with known undefined nonterminals
626
    fn prop_validation_catches_all_undefined(grammar: GrammarWithUndefined) -> TestResult {
100✔
627
        let grammar = grammar.0;
100✔
628

629
        // Collect all nonterminals defined in LHS
630
        let mut defined = std::collections::HashSet::new();
100✔
631
        for production in grammar.productions_iter() {
244✔
632
            if let Term::Nonterminal(nt) = &production.lhs {
244✔
633
                defined.insert(nt.clone());
244✔
634
            }
244✔
635
        }
636

637
        // Collect all nonterminals used in RHS
638
        let mut referenced = std::collections::HashSet::new();
100✔
639
        for production in grammar.productions_iter() {
244✔
640
            for expression in production.rhs_iter() {
368✔
641
                for term in expression.terms_iter() {
572✔
642
                    if let Term::Nonterminal(nt) = term {
572✔
643
                        referenced.insert(nt.clone());
267✔
644
                    }
305✔
645
                }
646
            }
647
        }
648

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

651
        let parser_result = grammar.build_parser();
100✔
652

653
        match parser_result {
35✔
654
            Ok(_) => {
655
                // Parser succeeded, so there should be no undefined nonterminals
656
                TestResult::from_bool(undefined.is_empty())
65✔
657
            }
658
            Err(Error::ValidationError(msg)) => {
35✔
659
                // Parser failed, error message should mention all undefined nonterminals
660
                let all_mentioned = undefined
35✔
661
                    .iter()
35✔
662
                    .all(|nt| msg.contains(&format!("<{nt}>")) || msg.contains(nt));
38✔
663
                TestResult::from_bool(!undefined.is_empty() && all_mentioned)
35✔
664
            }
NEW
665
            Err(_) => TestResult::error("Expected ValidationError"),
×
666
        }
667
    }
100✔
668

669
    #[test]
670
    fn validation_catches_all_undefined() {
1✔
671
        QuickCheck::new().tests(100).quickcheck(
1✔
672
            prop_validation_catches_all_undefined as fn(GrammarWithUndefined) -> TestResult,
1✔
673
        );
674
    }
1✔
675
}
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