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

shnewto / bnf / 21339317542

25 Jan 2026 08:48PM UTC coverage: 96.149% (-0.6%) from 96.753%
21339317542

Pull #187

github

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

384 of 408 new or added lines in 4 files covered. (94.12%)

4 existing lines in 1 file now uncovered.

2422 of 2519 relevant lines covered (96.15%)

24451.55 hits per line

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

93.02
/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, GrammarParser};
18
///
19
/// let grammar: Grammar = "<dna> ::= <base> | <base> <dna>
20
/// <base> ::= 'A' | 'C' | 'G' | 'T'"
21
///     .parse()
22
///     .unwrap();
23
///
24
/// let parser = GrammarParser::new(&grammar)?;
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> {
407✔
43
        validate_nonterminals(grammar)?;
407✔
44
        let starting_term = grammar.starting_term().ok_or_else(|| {
323✔
NEW
45
            Error::ValidationError("Grammar must have at least one production".to_string())
×
NEW
46
        })?;
×
47
        let parse_grammar = Rc::new(ParseGrammar::new(grammar));
323✔
48
        Ok(Self {
323✔
49
            starting_term,
323✔
50
            parse_grammar,
323✔
51
        })
323✔
52
    }
407✔
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> {
407✔
79
    // Collect all nonterminals defined in LHS of productions
80
    let mut defined_nonterminals = HashSet::new();
407✔
81
    for production in grammar.productions_iter() {
1,137✔
82
        if let Term::Nonterminal(ref nt) = production.lhs {
1,137✔
83
            defined_nonterminals.insert(nt.clone());
1,137✔
84
        }
1,137✔
85
    }
86

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

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

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

132
    Ok(())
323✔
133
}
407✔
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 = GrammarParser::new(&grammar);
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_undefined_nonterminal() {
1✔
159
        let grammar: Grammar = "<dna> ::= <base> | <base> <dna>
1✔
160
        <base> ::= <undefined>"
1✔
161
            .parse()
1✔
162
            .unwrap();
1✔
163

164
        let parser = GrammarParser::new(&grammar);
1✔
165
        assert!(
1✔
166
            parser.is_err(),
1✔
167
            "Parser construction should fail with undefined nonterminal"
168
        );
169
        assert!(
1✔
170
            matches!(parser.unwrap_err(), Error::ValidationError(_)),
1✔
171
            "Error should be ValidationError"
172
        );
173
    }
1✔
174

175
    #[test]
176
    fn parser_can_parse_multiple_inputs() {
1✔
177
        let grammar: Grammar = "<dna> ::= <base> | <base> <dna>
1✔
178
        <base> ::= 'A' | 'C' | 'G' | 'T'"
1✔
179
            .parse()
1✔
180
            .unwrap();
1✔
181

182
        let parser = GrammarParser::new(&grammar).unwrap();
1✔
183

184
        let input1 = "GATTACA";
1✔
185
        let input2 = "ATCG";
1✔
186

187
        let parse_trees1: Vec<_> = parser.parse_input(input1).collect();
1✔
188
        let parse_trees2: Vec<_> = parser.parse_input(input2).collect();
1✔
189

190
        assert!(!parse_trees1.is_empty(), "Should parse first input");
1✔
191
        assert!(!parse_trees2.is_empty(), "Should parse second input");
1✔
192
    }
1✔
193

194
    #[test]
195
    fn parser_accepts_explicit_starting_nonterminal() {
1✔
196
        let grammar: Grammar = "<base> ::= 'A' | 'C' | 'G' | 'T'
1✔
197
        <dna> ::= <base> | <base> <dna>"
1✔
198
            .parse()
1✔
199
            .unwrap();
1✔
200

201
        let parser = GrammarParser::new(&grammar).unwrap();
1✔
202
        let input = "GATTACA";
1✔
203
        let start_term = Term::Nonterminal("dna".to_string());
1✔
204

205
        let parse_trees: Vec<_> = parser
1✔
206
            .parse_input_starting_with(input, &start_term)
1✔
207
            .collect();
1✔
208

209
        assert!(
1✔
210
            !parse_trees.is_empty(),
1✔
211
            "Should parse with explicit starting nonterminal"
212
        );
213
    }
1✔
214

215
    #[test]
216
    fn parser_accepts_explicit_starting_terminal() {
1✔
217
        let grammar: Grammar = "<base> ::= 'A' | 'C' | 'G' | 'T'
1✔
218
        <dna> ::= <base> | <base> <dna>"
1✔
219
            .parse()
1✔
220
            .unwrap();
1✔
221

222
        let parser = GrammarParser::new(&grammar).unwrap();
1✔
223
        let input = "G";
1✔
224
        let start_term = Term::Terminal("G".to_string());
1✔
225

226
        // Note: Starting with a terminal directly doesn't work with Earley parser
227
        // since it expects a nonterminal to have productions. This test verifies
228
        // the API accepts terminals, but they won't produce parse trees unless
229
        // there's a production with that terminal as LHS (which is invalid).
230
        let parse_trees: Vec<_> = parser
1✔
231
            .parse_input_starting_with(input, &start_term)
1✔
232
            .collect();
1✔
233

234
        // This will be empty since there's no production with a terminal as LHS
235
        // The API accepts it, but it won't produce results
236
        assert_eq!(
1✔
237
            parse_trees.len(),
1✔
238
            0,
239
            "Terminal starting term produces no parse trees"
240
        );
241
    }
1✔
242

243
    #[test]
244
    fn parser_is_order_independent() {
1✔
245
        // Create grammar with productions in one order
246
        let grammar1: Grammar = "<dna> ::= <base> | <base> <dna>
1✔
247
        <base> ::= 'A' | 'C' | 'G' | 'T'"
1✔
248
            .parse()
1✔
249
            .unwrap();
1✔
250

251
        // Create same grammar with productions in different order
252
        let grammar2: Grammar = "<base> ::= 'A' | 'C' | 'G' | 'T'
1✔
253
        <dna> ::= <base> | <base> <dna>"
1✔
254
            .parse()
1✔
255
            .unwrap();
1✔
256

257
        let parser1 = GrammarParser::new(&grammar1).unwrap();
1✔
258
        let parser2 = GrammarParser::new(&grammar2).unwrap();
1✔
259

260
        let input = "GATTACA";
1✔
261
        // Use explicit starting term to ensure both use the same starting point
262
        let start_term = Term::Nonterminal("dna".to_string());
1✔
263

264
        let parse_trees1: Vec<_> = parser1
1✔
265
            .parse_input_starting_with(input, &start_term)
1✔
266
            .collect();
1✔
267
        let parse_trees2: Vec<_> = parser2
1✔
268
            .parse_input_starting_with(input, &start_term)
1✔
269
            .collect();
1✔
270

271
        // Results should be identical regardless of production order when using
272
        // the same explicit starting term
273
        assert_eq!(
1✔
274
            parse_trees1.len(),
1✔
275
            parse_trees2.len(),
1✔
276
            "Should produce same number of parse trees regardless of order"
277
        );
278
    }
1✔
279

280
    // Helper: Generate a simple valid grammar with known structure
281
    #[derive(Debug, Clone)]
282
    struct SimpleValidGrammar(Grammar);
283
    impl Arbitrary for SimpleValidGrammar {
284
        fn arbitrary(g: &mut Gen) -> Self {
100✔
285
            // Generate 1-5 nonterminal names
286
            let num_nonterms = usize::arbitrary(g) % 5 + 1;
100✔
287
            let nonterms: Vec<String> = (0..num_nonterms).map(|i| format!("nt{}", i)).collect();
284✔
288

289
            let mut productions = Vec::new();
100✔
290

291
            // Create productions: each nonterminal references only defined ones
292
            for (idx, nt) in nonterms.iter().enumerate() {
284✔
293
                let mut expressions = Vec::new();
284✔
294

295
                // Each production has 1-3 alternatives
296
                let num_alternatives = usize::arbitrary(g) % 3 + 1;
284✔
297
                for _ in 0..num_alternatives {
284✔
298
                    let mut terms = Vec::new();
565✔
299

300
                    // Each alternative has 1-3 terms
301
                    let num_terms = usize::arbitrary(g) % 3 + 1;
565✔
302
                    for _ in 0..num_terms {
565✔
303
                        if bool::arbitrary(g) && idx > 0 {
1,070✔
304
                            // Reference a previously defined nonterminal
305
                            let ref_idx = usize::arbitrary(g) % idx;
367✔
306
                            if let Some(nt) = nonterms.get(ref_idx) {
367✔
307
                                terms.push(Term::Nonterminal(nt.clone()));
367✔
308
                            } else {
367✔
NEW
309
                                // Use a terminal if index is out of bounds
×
NEW
310
                                let term_str = String::arbitrary(g);
×
NEW
311
                                terms.push(Term::Terminal(term_str));
×
NEW
312
                            }
×
313
                        } else {
703✔
314
                            // Use a terminal
703✔
315
                            let term_str = String::arbitrary(g);
703✔
316
                            terms.push(Term::Terminal(term_str));
703✔
317
                        }
703✔
318
                    }
319

320
                    expressions.push(Expression::from_parts(terms));
565✔
321
                }
322

323
                productions.push(Production::from_parts(
284✔
324
                    Term::Nonterminal(nt.clone()),
284✔
325
                    expressions,
284✔
326
                ));
327
            }
328

329
            Self(Grammar::from_parts(productions))
100✔
330
        }
100✔
331
    }
332

333
    // Helper: Generate grammar that may have undefined nonterminals
334
    #[derive(Debug, Clone)]
335
    struct GrammarWithUndefined(Grammar);
336
    impl Arbitrary for GrammarWithUndefined {
337
        fn arbitrary(g: &mut Gen) -> Self {
200✔
338
            let num_nonterms = usize::arbitrary(g) % 4 + 1;
200✔
339
            let mut nonterms: Vec<String> = (0..num_nonterms).map(|i| format!("nt{}", i)).collect();
499✔
340

341
            // Add some undefined nonterminals
342
            let num_undefined = usize::arbitrary(g) % 3;
200✔
343
            for i in 0..num_undefined {
200✔
344
                nonterms.push(format!("undefined{}", i));
199✔
345
            }
199✔
346

347
            let mut productions = Vec::new();
200✔
348
            let defined_count = num_nonterms;
200✔
349

350
            for (idx, nt) in nonterms.iter().enumerate() {
698✔
351
                if idx >= defined_count {
698✔
352
                    // Don't define the undefined nonterminals
353
                    continue;
199✔
354
                }
499✔
355

356
                let mut expressions = Vec::new();
499✔
357
                let num_alternatives = usize::arbitrary(g) % 2 + 1;
499✔
358

359
                for _ in 0..num_alternatives {
499✔
360
                    let mut terms = Vec::new();
754✔
361
                    let num_terms = usize::arbitrary(g) % 2 + 1;
754✔
362

363
                    for _ in 0..num_terms {
754✔
364
                        if bool::arbitrary(g) && !nonterms.is_empty() {
1,154✔
365
                            // Reference any nonterminal (may be undefined)
366
                            let ref_idx = usize::arbitrary(g) % nonterms.len();
604✔
367
                            if let Some(nt) = nonterms.get(ref_idx) {
604✔
368
                                terms.push(Term::Nonterminal(nt.clone()));
604✔
369
                            } else {
604✔
NEW
370
                                terms.push(Term::Terminal(String::arbitrary(g)));
×
NEW
371
                            }
×
372
                        } else {
550✔
373
                            terms.push(Term::Terminal(String::arbitrary(g)));
550✔
374
                        }
550✔
375
                    }
376

377
                    expressions.push(Expression::from_parts(terms));
754✔
378
                }
379

380
                productions.push(Production::from_parts(
499✔
381
                    Term::Nonterminal(nt.clone()),
499✔
382
                    expressions,
499✔
383
                ));
384
            }
385

386
            Self(Grammar::from_parts(productions))
200✔
387
        }
200✔
388
    }
389

390
    // Property test: Parser construction fails if any nonterminal lacks definition
391
    fn prop_parser_fails_with_undefined_nonterminal(grammar: GrammarWithUndefined) -> TestResult {
100✔
392
        let grammar = grammar.0;
100✔
393

394
        // Collect all nonterminals defined in LHS
395
        let mut defined = std::collections::HashSet::new();
100✔
396
        for production in grammar.productions_iter() {
252✔
397
            if let Term::Nonterminal(nt) = &production.lhs {
252✔
398
                defined.insert(nt.clone());
252✔
399
            }
252✔
400
        }
401

402
        // Collect all nonterminals used in RHS
403
        let mut referenced = std::collections::HashSet::new();
100✔
404
        for production in grammar.productions_iter() {
252✔
405
            for expression in production.rhs_iter() {
385✔
406
                for term in expression.terms_iter() {
583✔
407
                    if let Term::Nonterminal(nt) = term {
583✔
408
                        referenced.insert(nt.clone());
299✔
409
                    }
299✔
410
                }
411
            }
412
        }
413

414
        // Find undefined nonterminals
415
        let undefined: Vec<_> = referenced.difference(&defined).cloned().collect();
100✔
416

417
        let parser_result = GrammarParser::new(&grammar);
100✔
418

419
        if undefined.is_empty() {
100✔
420
            // All nonterminals are defined, parser should succeed
421
            TestResult::from_bool(parser_result.is_ok())
56✔
422
        } else {
423
            // Some nonterminals are undefined, parser should fail
424
            TestResult::from_bool(
44✔
425
                parser_result.is_err()
44✔
426
                    && matches!(parser_result.unwrap_err(), Error::ValidationError(_)),
44✔
427
            )
428
        }
429
    }
100✔
430

431
    #[test]
432
    fn parser_fails_with_undefined_nonterminal() {
1✔
433
        QuickCheck::new().tests(100).quickcheck(
1✔
434
            prop_parser_fails_with_undefined_nonterminal as fn(GrammarWithUndefined) -> TestResult,
1✔
435
        );
436
    }
1✔
437

438
    // Helper: Generate valid grammar with at least 2 productions
439
    #[derive(Debug, Clone)]
440
    struct ValidGrammarWithMultipleProductions(Grammar);
441
    impl Arbitrary for ValidGrammarWithMultipleProductions {
442
        fn arbitrary(g: &mut Gen) -> Self {
50✔
443
            // Generate 2-5 nonterminals
444
            let num_nonterms = usize::arbitrary(g) % 4 + 2;
50✔
445
            let nonterms: Vec<String> = (0..num_nonterms).map(|i| format!("nt{}", i)).collect();
170✔
446

447
            let mut productions = Vec::new();
50✔
448

449
            for (idx, nt) in nonterms.iter().enumerate() {
170✔
450
                let mut expressions = Vec::new();
170✔
451
                let num_alternatives = usize::arbitrary(g) % 2 + 1;
170✔
452

453
                for _ in 0..num_alternatives {
170✔
454
                    let mut terms = Vec::new();
254✔
455
                    let num_terms = usize::arbitrary(g) % 2 + 1;
254✔
456

457
                    for _ in 0..num_terms {
254✔
458
                        if bool::arbitrary(g) && idx > 0 {
393✔
459
                            // Reference a previously defined nonterminal
460
                            let ref_idx = usize::arbitrary(g) % idx;
127✔
461
                            if let Some(nt) = nonterms.get(ref_idx) {
127✔
462
                                terms.push(Term::Nonterminal(nt.clone()));
127✔
463
                            } else {
127✔
NEW
464
                                terms.push(Term::Terminal(String::arbitrary(g)));
×
NEW
465
                            }
×
466
                        } else {
266✔
467
                            terms.push(Term::Terminal(String::arbitrary(g)));
266✔
468
                        }
266✔
469
                    }
470

471
                    expressions.push(Expression::from_parts(terms));
254✔
472
                }
473

474
                productions.push(Production::from_parts(
170✔
475
                    Term::Nonterminal(nt.clone()),
170✔
476
                    expressions,
170✔
477
                ));
478
            }
479

480
            Self(Grammar::from_parts(productions))
50✔
481
        }
50✔
482
    }
483

484
    // Property test: Parser results are identical regardless of production order
485
    fn prop_parser_order_independent(grammar: ValidGrammarWithMultipleProductions) -> TestResult {
50✔
486
        let grammar = grammar.0;
50✔
487

488
        // Create a shuffled version of the grammar
489
        let mut productions: Vec<_> = grammar.productions_iter().cloned().collect();
50✔
490
        let mut rng = rand::rng();
50✔
491
        rand::seq::SliceRandom::shuffle(productions.as_mut_slice(), &mut rng);
50✔
492

493
        let grammar1 = grammar;
50✔
494
        let grammar2 = Grammar::from_parts(productions);
50✔
495

496
        let parser1 = match GrammarParser::new(&grammar1) {
50✔
497
            Ok(p) => p,
50✔
NEW
498
            Err(_) => return TestResult::discard(),
×
499
        };
500
        let parser2 = match GrammarParser::new(&grammar2) {
50✔
501
            Ok(p) => p,
50✔
NEW
502
            Err(_) => return TestResult::discard(),
×
503
        };
504

505
        // Get starting term from first grammar
506
        let starting_term = match grammar1.starting_term() {
50✔
507
            Some(t) => t,
50✔
NEW
508
            None => return TestResult::discard(),
×
509
        };
510

511
        // Generate a test sentence
512
        let sentence = match grammar1.generate() {
50✔
513
            Ok(s) => s,
50✔
NEW
514
            Err(_) => return TestResult::discard(),
×
515
        };
516

517
        // Parse with both parsers using explicit starting term
518
        let parse_trees1: Vec<_> = parser1
50✔
519
            .parse_input_starting_with(&sentence, starting_term)
50✔
520
            .collect();
50✔
521
        let parse_trees2: Vec<_> = parser2
50✔
522
            .parse_input_starting_with(&sentence, starting_term)
50✔
523
            .collect();
50✔
524

525
        // Results should be identical
526
        TestResult::from_bool(parse_trees1.len() == parse_trees2.len())
50✔
527
    }
50✔
528

529
    #[test]
530
    fn parser_order_independent() {
1✔
531
        QuickCheck::new().tests(50).quickcheck(
1✔
532
            prop_parser_order_independent as fn(ValidGrammarWithMultipleProductions) -> TestResult,
1✔
533
        );
534
    }
1✔
535

536
    // Property test: Parser can be reused multiple times with same results
537
    fn prop_parser_reusable(grammar: SimpleValidGrammar) -> TestResult {
100✔
538
        let grammar = grammar.0;
100✔
539

540
        // Only test with grammars that can generate
541
        if !grammar.terminates() {
100✔
NEW
542
            return TestResult::discard();
×
543
        }
100✔
544

545
        let parser = match GrammarParser::new(&grammar) {
100✔
546
            Ok(p) => p,
100✔
NEW
547
            Err(_) => return TestResult::discard(),
×
548
        };
549

550
        // Generate a sentence
551
        let sentence = match grammar.generate() {
100✔
552
            Ok(s) => s,
100✔
NEW
553
            Err(_) => return TestResult::discard(),
×
554
        };
555

556
        // Parse the same sentence multiple times
557
        let parse_trees1: Vec<_> = parser.parse_input(&sentence).collect();
100✔
558
        let parse_trees2: Vec<_> = parser.parse_input(&sentence).collect();
100✔
559
        let parse_trees3: Vec<_> = parser.parse_input(&sentence).collect();
100✔
560

561
        // All results should be identical
562
        TestResult::from_bool(
100✔
563
            parse_trees1.len() == parse_trees2.len() && parse_trees2.len() == parse_trees3.len(),
100✔
564
        )
565
    }
100✔
566

567
    #[test]
568
    fn parser_reusable() {
1✔
569
        QuickCheck::new()
1✔
570
            .tests(100)
1✔
571
            .quickcheck(prop_parser_reusable as fn(SimpleValidGrammar) -> TestResult);
1✔
572
    }
1✔
573

574
    // Property test: Parser validation catches all undefined nonterminals
575
    // Simplified: Build grammars with known undefined nonterminals
576
    fn prop_validation_catches_all_undefined(grammar: GrammarWithUndefined) -> TestResult {
100✔
577
        let grammar = grammar.0;
100✔
578

579
        // Collect all nonterminals defined in LHS
580
        let mut defined = std::collections::HashSet::new();
100✔
581
        for production in grammar.productions_iter() {
247✔
582
            if let Term::Nonterminal(nt) = &production.lhs {
247✔
583
                defined.insert(nt.clone());
247✔
584
            }
247✔
585
        }
586

587
        // Collect all nonterminals used in RHS
588
        let mut referenced = std::collections::HashSet::new();
100✔
589
        for production in grammar.productions_iter() {
247✔
590
            for expression in production.rhs_iter() {
369✔
591
                for term in expression.terms_iter() {
571✔
592
                    if let Term::Nonterminal(nt) = term {
571✔
593
                        referenced.insert(nt.clone());
305✔
594
                    }
305✔
595
                }
596
            }
597
        }
598

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

601
        let parser_result = GrammarParser::new(&grammar);
100✔
602

603
        match parser_result {
39✔
604
            Ok(_) => {
605
                // Parser succeeded, so there should be no undefined nonterminals
606
                TestResult::from_bool(undefined.is_empty())
61✔
607
            }
608
            Err(Error::ValidationError(msg)) => {
39✔
609
                // Parser failed, error message should mention all undefined nonterminals
610
                let all_mentioned = undefined
39✔
611
                    .iter()
39✔
612
                    .all(|nt| msg.contains(&format!("<{nt}>")) || msg.contains(nt));
45✔
613
                TestResult::from_bool(!undefined.is_empty() && all_mentioned)
39✔
614
            }
NEW
615
            Err(_) => TestResult::error("Expected ValidationError"),
×
616
        }
617
    }
100✔
618

619
    #[test]
620
    fn validation_catches_all_undefined() {
1✔
621
        QuickCheck::new().tests(100).quickcheck(
1✔
622
            prop_validation_catches_all_undefined as fn(GrammarWithUndefined) -> TestResult,
1✔
623
        );
624
    }
1✔
625
}
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