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

shnewto / bnf / 21571858283

01 Feb 2026 10:58PM UTC coverage: 98.108% (+0.08%) from 98.026%
21571858283

push

github

web-flow
add a public grammar validation API to check for undefined nonterminals (#196)

196 of 197 new or added lines in 7 files covered. (99.49%)

1 existing line in 1 file now uncovered.

2904 of 2960 relevant lines covered (98.11%)

21067.42 hits per line

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

95.26
/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::rc::Rc;
9

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

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

52
    /// Construct a parser without validating that all nonterminals are defined.
53
    /// Used only by deprecated `Grammar::parse_input` / `parse_input_starting_with`.
54
    pub(crate) fn new_unchecked(grammar: &'gram Grammar) -> Self {
257✔
55
        let starting_term = grammar
257✔
56
            .starting_term()
257✔
57
            .expect("Grammar must have at least one production");
257✔
58
        let parse_grammar = Rc::new(ParseGrammar::new_unchecked(grammar));
257✔
59
        Self {
257✔
60
            starting_term,
257✔
61
            parse_grammar,
257✔
62
        }
257✔
63
    }
257✔
64

65
    /// Parse an input string using the grammar's starting nonterminal.
66
    ///
67
    /// Returns an iterator over all possible parse trees for the input.
68
    pub fn parse_input<'p: 'gram>(
1,220✔
69
        &'p self,
1,220✔
70
        input: &'gram str,
1,220✔
71
    ) -> impl Iterator<Item = ParseTree<'gram>> + use<'p, 'gram> {
1,220✔
72
        self.parse_input_starting_with(input, self.starting_term)
1,220✔
73
    }
1,220✔
74

75
    /// Parse an input string starting with the given term (nonterminal or terminal).
76
    ///
77
    /// Returns an iterator over all possible parse trees for the input.
78
    pub fn parse_input_starting_with<'p: 'gram>(
1,324✔
79
        &'p self,
1,324✔
80
        input: &'gram str,
1,324✔
81
        start: &'gram Term,
1,324✔
82
    ) -> impl Iterator<Item = ParseTree<'gram>> + use<'p, 'gram> {
1,324✔
83
        crate::earley::parse(self, input, Some(start))
1,324✔
84
    }
1,324✔
85
}
86

87
#[cfg(test)]
88
mod tests {
89
    use super::*;
90
    use crate::Grammar;
91
    use crate::expression::Expression;
92
    use crate::production::Production;
93
    use quickcheck::{Arbitrary, Gen, QuickCheck, TestResult};
94

95
    #[test]
96
    fn parser_construction_with_valid_grammar() {
1✔
97
        let grammar: Grammar = "<dna> ::= <base> | <base> <dna>
1✔
98
        <base> ::= 'A' | 'C' | 'G' | 'T'"
1✔
99
            .parse()
1✔
100
            .unwrap();
1✔
101

102
        let parser = grammar.build_parser();
1✔
103
        assert!(
1✔
104
            parser.is_ok(),
1✔
105
            "Parser should be constructible from valid grammar"
106
        );
107
    }
1✔
108

109
    #[test]
110
    fn parser_construction_fails_with_empty_grammar() {
1✔
111
        let grammar = Grammar::from_parts(vec![]);
1✔
112
        let parser = grammar.build_parser();
1✔
113
        assert!(
1✔
114
            parser.is_err(),
1✔
115
            "Parser construction should fail with empty grammar"
116
        );
117
        assert!(
1✔
118
            matches!(parser.unwrap_err(), Error::ValidationError(_)),
1✔
119
            "Error should be ValidationError about missing productions"
120
        );
121
    }
1✔
122

123
    #[test]
124
    fn parser_validation_with_group_containing_undefined() {
1✔
125
        // Test that validation fails when a grouped term references an undefined nonterminal
126
        let grammar: Grammar = "<start> ::= (<undefined>)".parse().unwrap();
1✔
127
        let parser = grammar.build_parser();
1✔
128
        assert!(
1✔
129
            parser.is_err(),
1✔
130
            "Parser should fail when group contains undefined nonterminal"
131
        );
132
        assert!(matches!(parser.unwrap_err(), Error::ValidationError(_)));
1✔
133
    }
1✔
134

135
    #[test]
136
    fn parser_validation_with_group_containing_defined() {
1✔
137
        // Test that validation succeeds when a group contains a defined nonterminal
138
        let grammar: Grammar = r#"<start> ::= (<base>)
1✔
139
<base> ::= 'A'"#
1✔
140
            .parse()
1✔
141
            .unwrap();
1✔
142
        let parser = grammar.build_parser();
1✔
143
        assert!(
1✔
144
            parser.is_ok(),
1✔
145
            "Parser should succeed when group contains defined nonterminal"
146
        );
147
    }
1✔
148

149
    #[test]
150
    fn normalization_groups_and_optionals_produce_named_nonterminals() {
1✔
151
        // Regression: extended syntax ( ) and [ ] is normalized to __anon* nonterminals
152
        let grammar: Grammar = r#"<s> ::= (<a> | <b>) [<c>]
1✔
153
<a> ::= 'a'
1✔
154
<b> ::= 'b'
1✔
155
<c> ::= 'c'"#
1✔
156
            .parse()
1✔
157
            .unwrap();
1✔
158
        for prod in grammar.productions_iter() {
6✔
159
            for expr in prod.rhs_iter() {
8✔
160
                for term in expr.terms_iter() {
9✔
161
                    match term {
9✔
162
                        crate::Term::Terminal(_) | crate::Term::Nonterminal(_) => {}
9✔
163
                    }
164
                }
165
            }
166
        }
167
        let parser = grammar.build_parser().unwrap();
1✔
168
        assert!(parser.parse_input("a").next().is_some());
1✔
169
        assert!(parser.parse_input("ac").next().is_some());
1✔
170
        assert!(parser.parse_input("").next().is_none());
1✔
171
    }
1✔
172

173
    #[test]
174
    fn parse_empty_optional_bnf() {
1✔
175
        // BNF optional [A] normalizes to __anon* with '' alternative; "" and "x" both parse
176
        let grammar: Grammar = r#"<s> ::= [<x>]
1✔
177
<x> ::= 'x'"#
1✔
178
            .parse()
1✔
179
            .unwrap();
1✔
180
        let parser = grammar.build_parser().unwrap();
1✔
181
        assert!(parser.parse_input("").next().is_some());
1✔
182
        assert!(parser.parse_input("x").next().is_some());
1✔
183
    }
1✔
184

185
    #[test]
186
    fn user_defined_anon_name_no_collision() {
1✔
187
        // User-defined <__anon0> should not collide with generated anon names for groups
188
        let grammar: Grammar = r#"<__anon0> ::= 'a'
1✔
189
<s> ::= (<__anon0>)"#
1✔
190
            .parse()
1✔
191
            .unwrap();
1✔
192
        let parser = grammar.build_parser().unwrap();
1✔
193
        assert!(parser.parse_input("a").next().is_some());
1✔
194
        // Grammar should contain both user's __anon0 and a generated anon for the group
195
        let lhs_names: Vec<_> = grammar
1✔
196
            .productions_iter()
1✔
197
            .map(|p| match &p.lhs {
3✔
198
                crate::Term::Nonterminal(n) => n.as_str(),
3✔
199
                _ => "",
×
200
            })
3✔
201
            .collect();
1✔
202
        assert!(lhs_names.contains(&"__anon0"));
1✔
203
        assert!(lhs_names.iter().any(|n| n.starts_with("__anon")));
1✔
204
    }
1✔
205

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

369
                    expressions.push(Expression::from_parts(terms));
511✔
370
                }
371

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

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

382
    /// Generates a grammar that always has at least one undefined nonterminal
383
    /// (referenced in a production RHS but never defined).
384
    ///
385
    /// Structure: `[nt0, nt1, ..., undefined0, undefined1, ...]`
386
    /// - First `defined_count` nonterminals get productions
387
    /// - Remaining "undefined" nonterminals are referenced but never defined
388
    /// - We force at least one undefined reference so the grammar is invalid
389
    #[derive(Debug, Clone)]
390
    struct GrammarWithUndefined(Grammar);
391
    impl Arbitrary for GrammarWithUndefined {
392
        fn arbitrary(g: &mut Gen) -> Self {
200✔
393
            let defined_count = usize::arbitrary(g) % 4 + 1;
200✔
394
            let num_undefined = usize::arbitrary(g) % 3 + 1;
200✔
395

396
            let defined_nonterms: Vec<String> =
200✔
397
                (0..defined_count).map(|i| format!("nt{}", i)).collect();
501✔
398
            let undefined_nonterms: Vec<String> = (0..num_undefined)
200✔
399
                .map(|i| format!("undefined{}", i))
380✔
400
                .collect();
200✔
401
            let all_nonterms: Vec<String> = defined_nonterms
200✔
402
                .iter()
200✔
403
                .chain(undefined_nonterms.iter())
200✔
404
                .cloned()
200✔
405
                .collect();
200✔
406

407
            let mut productions = Vec::new();
200✔
408
            let mut has_undefined_reference = false;
200✔
409

410
            for (idx, nt) in defined_nonterms.iter().enumerate() {
501✔
411
                let mut expressions = Vec::new();
501✔
412
                let num_alternatives = usize::arbitrary(g) % 2 + 1;
501✔
413

414
                for alt_idx in 0..num_alternatives {
757✔
415
                    let mut terms = Vec::new();
757✔
416
                    let num_terms = usize::arbitrary(g) % 2 + 1;
757✔
417

418
                    // Invariant: first production's first alternative must reference undefined
419
                    let is_first_alt_of_first_prod = idx == 0 && alt_idx == 0;
757✔
420
                    let must_insert_undefined =
757✔
421
                        is_first_alt_of_first_prod && !has_undefined_reference;
757✔
422

423
                    for term_idx in 0..num_terms {
1,168✔
424
                        let use_nonterminal = must_insert_undefined && term_idx == 0
1,168✔
425
                            || (bool::arbitrary(g) && !all_nonterms.is_empty());
968✔
426

427
                        if use_nonterminal {
1,168✔
428
                            let ref_idx = if must_insert_undefined && term_idx == 0 {
690✔
429
                                has_undefined_reference = true;
200✔
430
                                defined_count + usize::arbitrary(g) % num_undefined
200✔
431
                            } else {
432
                                usize::arbitrary(g) % all_nonterms.len()
490✔
433
                            };
434
                            if let Some(ref_nt) = all_nonterms.get(ref_idx) {
690✔
435
                                terms.push(Term::Nonterminal(ref_nt.clone()));
690✔
436
                            } else {
690✔
437
                                terms.push(Term::Terminal(String::arbitrary(g)));
×
438
                            }
×
439
                        } else {
478✔
440
                            terms.push(Term::Terminal(String::arbitrary(g)));
478✔
441
                        }
478✔
442
                    }
443

444
                    expressions.push(Expression::from_parts(terms));
757✔
445
                }
446

447
                productions.push(Production::from_parts(
501✔
448
                    Term::Nonterminal(nt.clone()),
501✔
449
                    expressions,
501✔
450
                ));
451
            }
452

453
            Self(Grammar::from_parts(productions))
200✔
454
        }
200✔
455
    }
456

457
    // Property test: Parser construction fails if any nonterminal lacks definition
458
    fn prop_parser_fails_with_undefined_nonterminal(grammar: GrammarWithUndefined) -> TestResult {
100✔
459
        let grammar = grammar.0;
100✔
460
        let parser = grammar.build_parser();
100✔
461
        let is_validation_error = matches!(parser, Err(Error::ValidationError(_)));
100✔
462
        TestResult::from_bool(is_validation_error)
100✔
463
    }
100✔
464

465
    #[test]
466
    fn parser_fails_with_undefined_nonterminal() {
1✔
467
        QuickCheck::new().tests(100).quickcheck(
1✔
468
            prop_parser_fails_with_undefined_nonterminal as fn(GrammarWithUndefined) -> TestResult,
1✔
469
        );
470
    }
1✔
471

472
    // Helper: Generate valid grammar with at least 2 productions
473
    #[derive(Debug, Clone)]
474
    struct ValidGrammarWithMultipleProductions(Grammar);
475
    impl Arbitrary for ValidGrammarWithMultipleProductions {
476
        fn arbitrary(g: &mut Gen) -> Self {
50✔
477
            // Generate 2-5 nonterminals
478
            let num_nonterms = usize::arbitrary(g) % 4 + 2;
50✔
479
            let nonterms: Vec<String> = (0..num_nonterms).map(|i| format!("nt{}", i)).collect();
190✔
480

481
            let mut productions = Vec::new();
50✔
482

483
            for (idx, nt) in nonterms.iter().enumerate() {
190✔
484
                let mut expressions = Vec::new();
190✔
485
                let num_alternatives = usize::arbitrary(g) % 2 + 1;
190✔
486

487
                for _ in 0..num_alternatives {
190✔
488
                    let mut terms = Vec::new();
288✔
489
                    let num_terms = usize::arbitrary(g) % 2 + 1;
288✔
490

491
                    for _ in 0..num_terms {
288✔
492
                        if bool::arbitrary(g) && idx > 0 {
445✔
493
                            // Reference a previously defined nonterminal
494
                            let ref_idx = usize::arbitrary(g) % idx;
158✔
495
                            if let Some(nt) = nonterms.get(ref_idx) {
158✔
496
                                terms.push(Term::Nonterminal(nt.clone()));
158✔
497
                            } else {
158✔
498
                                terms.push(Term::Terminal(String::arbitrary(g)));
×
499
                            }
×
500
                        } else {
287✔
501
                            terms.push(Term::Terminal(String::arbitrary(g)));
287✔
502
                        }
287✔
503
                    }
504

505
                    expressions.push(Expression::from_parts(terms));
288✔
506
                }
507

508
                productions.push(Production::from_parts(
190✔
509
                    Term::Nonterminal(nt.clone()),
190✔
510
                    expressions,
190✔
511
                ));
512
            }
513

514
            Self(Grammar::from_parts(productions))
50✔
515
        }
50✔
516
    }
517

518
    // Property test: Parser results are identical regardless of production order
519
    fn prop_parser_order_independent(grammar: ValidGrammarWithMultipleProductions) -> TestResult {
50✔
520
        let grammar = grammar.0;
50✔
521

522
        // Create a shuffled version of the grammar
523
        let mut productions: Vec<_> = grammar.productions_iter().cloned().collect();
50✔
524
        let mut rng = rand::rng();
50✔
525
        rand::seq::SliceRandom::shuffle(productions.as_mut_slice(), &mut rng);
50✔
526

527
        let grammar1 = grammar;
50✔
528
        let grammar2 = Grammar::from_parts(productions);
50✔
529

530
        let parser1 = match grammar1.build_parser() {
50✔
531
            Ok(p) => p,
50✔
532
            Err(_) => return TestResult::discard(),
×
533
        };
534
        let parser2 = match grammar2.build_parser() {
50✔
535
            Ok(p) => p,
50✔
536
            Err(_) => return TestResult::discard(),
×
537
        };
538

539
        // Get starting term from first grammar
540
        let starting_term = match grammar1.starting_term() {
50✔
541
            Some(t) => t,
50✔
542
            None => return TestResult::discard(),
×
543
        };
544

545
        // Generate a test sentence
546
        let sentence = match grammar1.generate() {
50✔
547
            Ok(s) => s,
50✔
548
            Err(_) => return TestResult::discard(),
×
549
        };
550

551
        // Parse with both parsers using explicit starting term
552
        let parse_trees1: Vec<_> = parser1
50✔
553
            .parse_input_starting_with(&sentence, starting_term)
50✔
554
            .collect();
50✔
555
        let parse_trees2: Vec<_> = parser2
50✔
556
            .parse_input_starting_with(&sentence, starting_term)
50✔
557
            .collect();
50✔
558

559
        // Results should be identical
560
        TestResult::from_bool(parse_trees1.len() == parse_trees2.len())
50✔
561
    }
50✔
562

563
    #[test]
564
    fn parser_order_independent() {
1✔
565
        QuickCheck::new().tests(50).quickcheck(
1✔
566
            prop_parser_order_independent as fn(ValidGrammarWithMultipleProductions) -> TestResult,
1✔
567
        );
568
    }
1✔
569

570
    // Property test: Parser can be reused multiple times with same results
571
    fn prop_parser_reusable(grammar: SimpleValidGrammar) -> TestResult {
100✔
572
        let grammar = grammar.0;
100✔
573

574
        // Only test with grammars that can generate
575
        if !grammar.terminates() {
100✔
576
            return TestResult::discard();
×
577
        }
100✔
578

579
        let parser = match grammar.build_parser() {
100✔
580
            Ok(p) => p,
100✔
581
            Err(_) => return TestResult::discard(),
×
582
        };
583

584
        // Generate a sentence
585
        let sentence = match grammar.generate() {
100✔
586
            Ok(s) => s,
100✔
587
            Err(_) => return TestResult::discard(),
×
588
        };
589

590
        // Parse the same sentence multiple times
591
        let parse_trees1: Vec<_> = parser.parse_input(&sentence).collect();
100✔
592
        let parse_trees2: Vec<_> = parser.parse_input(&sentence).collect();
100✔
593
        let parse_trees3: Vec<_> = parser.parse_input(&sentence).collect();
100✔
594

595
        // All results should be identical
596
        TestResult::from_bool(
100✔
597
            parse_trees1.len() == parse_trees2.len() && parse_trees2.len() == parse_trees3.len(),
100✔
598
        )
599
    }
100✔
600

601
    #[test]
602
    fn parser_reusable() {
1✔
603
        QuickCheck::new()
1✔
604
            .tests(100)
1✔
605
            .quickcheck(prop_parser_reusable as fn(SimpleValidGrammar) -> TestResult);
1✔
606
    }
1✔
607

608
    // Property test: Parser validation catches all undefined nonterminals
609
    // Simplified: Build grammars with known undefined nonterminals
610
    fn prop_validation_catches_all_undefined(grammar: GrammarWithUndefined) -> TestResult {
100✔
611
        let grammar = grammar.0;
100✔
612

613
        // Collect all nonterminals defined in LHS
614
        let mut defined = crate::HashSet::new();
100✔
615
        for production in grammar.productions_iter() {
253✔
616
            if let Term::Nonterminal(nt) = &production.lhs {
253✔
617
                defined.insert(nt.clone());
253✔
618
            }
253✔
619
        }
620

621
        // Collect all nonterminals used in RHS
622
        let mut referenced = crate::HashSet::new();
100✔
623
        for production in grammar.productions_iter() {
253✔
624
            for expression in production.rhs_iter() {
386✔
625
                for term in expression.terms_iter() {
598✔
626
                    if let Term::Nonterminal(nt) = term {
598✔
627
                        referenced.insert(nt.clone());
347✔
628
                    }
347✔
629
                }
630
            }
631
        }
632

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

635
        let parser_result = grammar.build_parser();
100✔
636

637
        match parser_result {
100✔
638
            Ok(_) => {
639
                // Parser succeeded, so there should be no undefined nonterminals
UNCOV
640
                TestResult::from_bool(undefined.is_empty())
×
641
            }
642
            Err(Error::ValidationError(msg)) => {
100✔
643
                // Parser failed, error message should mention at least one undefined nonterminal
644
                let any_mentioned = undefined
100✔
645
                    .iter()
100✔
646
                    .any(|nt| msg.contains(&format!("<{nt}>")) || msg.contains(nt));
110✔
647
                TestResult::from_bool(!undefined.is_empty() && any_mentioned)
100✔
648
            }
649
            Err(_) => TestResult::error("Expected ValidationError"),
×
650
        }
651
    }
100✔
652

653
    #[test]
654
    fn validation_catches_all_undefined() {
1✔
655
        QuickCheck::new().tests(100).quickcheck(
1✔
656
            prop_validation_catches_all_undefined as fn(GrammarWithUndefined) -> TestResult,
1✔
657
        );
658
    }
1✔
659
}
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