Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions src/parsers/bnf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,62 @@ mod tests {
let (_, actual) = grammar::<BNF>(input).unwrap();
assert_eq!(expected, actual);
}

#[test]
fn production_with_comment_suffix() {
let input = "<a> ::= 'x' ; only a comment\n";
let expected = Production::from_parts(
Term::Nonterminal("a".to_string()),
vec![Expression::from_parts(vec![Term::Terminal(
"x".to_string(),
)])],
);

let (_, actual) = production::<BNF>(input).unwrap();
assert_eq!(expected, actual);
}

#[test]
fn grammar_with_comment_only_line() {
let input = "<a> ::= 'x'\n; comment\n<b> ::= 'y'\n";
let expected = Grammar::from_parts(vec![
Production::from_parts(
Term::Nonterminal("a".to_string()),
vec![Expression::from_parts(vec![Term::Terminal(
"x".to_string(),
)])],
),
Production::from_parts(
Term::Nonterminal("b".to_string()),
vec![Expression::from_parts(vec![Term::Terminal(
"y".to_string(),
)])],
),
]);

let (_, actual) = grammar::<BNF>(input).unwrap();
assert_eq!(expected, actual);
}

#[test]
fn grammar_with_comment_to_eof() {
let input = "<a> ::= 'x'\n<b> ::= 'y' ; last line comment";
let expected = Grammar::from_parts(vec![
Production::from_parts(
Term::Nonterminal("a".to_string()),
vec![Expression::from_parts(vec![Term::Terminal(
"x".to_string(),
)])],
),
Production::from_parts(
Term::Nonterminal("b".to_string()),
vec![Expression::from_parts(vec![Term::Terminal(
"y".to_string(),
)])],
),
]);

let (_, actual) = grammar::<BNF>(input).unwrap();
assert_eq!(expected, actual);
}
}
21 changes: 21 additions & 0 deletions src/parsers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,27 @@ fn normalize_parsed_grammar(parsed: ParsedGrammar) -> Grammar {
pub mod tests {
use super::*;

#[test]
fn whitespace_plus_comments_skips_comment_then_rest() {
let input = " ; comment\n rest";
let (remaining, _) = whitespace_plus_comments(input).unwrap();
assert_eq!(remaining, "rest");
}

#[test]
fn whitespace_plus_comments_comment_to_eof() {
let input = " ; comment";
let (remaining, _) = whitespace_plus_comments(input).unwrap();
assert_eq!(remaining, "");
}

#[test]
fn whitespace_plus_comments_skips_only_whitespace_without_semicolon() {
let input = " x";
let (remaining, _) = whitespace_plus_comments(input).unwrap();
assert!(remaining.starts_with('x'));
}

#[test]
fn terminal_match() {
let input = "\"hello world\"";
Expand Down
18 changes: 18 additions & 0 deletions src/production.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,24 @@ mod tests {
assert_eq!(prod, crate::production!(<base> ::= 'A'));
}

#[test]
fn parse_comment_with_text_then_newline() {
let prod = Production::from_str("<a> ::= 'x' ; this is a comment\n").unwrap();
assert_eq!(prod, crate::production!(<a> ::= 'x'));
}

#[test]
fn parse_comment_to_eof() {
let prod = Production::from_str("<a> ::= 'x' ; comment").unwrap();
assert_eq!(prod, crate::production!(<a> ::= 'x'));
}

#[test]
fn parse_comment_between_alternatives() {
let prod = Production::from_str("<a> ::= 'x' ; comment\n | 'y'").unwrap();
assert_eq!(prod, crate::production!(<a> ::= 'x' | 'y'));
}

#[test]
fn parse_incomplete() {
let result = Production::from_str("");
Expand Down
68 changes: 68 additions & 0 deletions tests/from_str.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,71 @@ mod custom_trait {
assert!(nonterminal.is_ok())
}
}

mod comments {
use bnf::{Grammar, Term};

#[test]
fn grammar_with_comments_throughout() {
let input = "<a> ::= 'x' ; end of first rule
; comment-only line
<b> ::= 'y' ; end of second rule";
let grammar: Grammar = input.parse().expect("parse");
assert_eq!(
grammar.productions_iter().count(),
2,
"parsed grammar must have two productions"
);
let mut prods = grammar.productions_iter();
let first = prods.next().unwrap();
assert_eq!(first.lhs, Term::Nonterminal("a".into()));
assert_eq!(first.rhs_iter().next().unwrap().to_string(), "'x'");
let second = prods.next().unwrap();
assert_eq!(second.lhs, Term::Nonterminal("b".into()));
assert_eq!(second.rhs_iter().next().unwrap().to_string(), "'y'");
}

#[test]
fn comment_does_not_break_parsing() {
let input = "<a> ::= 'x' ; note\n<b> ::= 'y'";
let grammar: Grammar = input.parse().expect("parse");
assert_eq!(
grammar.productions_iter().count(),
2,
"parsed grammar must have two productions"
);
let mut prods = grammar.productions_iter();
let first = prods.next().unwrap();
assert_eq!(first.lhs, Term::Nonterminal("a".into()));
assert_eq!(first.rhs_iter().next().unwrap().to_string(), "'x'");
let second = prods.next().unwrap();
assert_eq!(second.lhs, Term::Nonterminal("b".into()));
assert_eq!(second.rhs_iter().next().unwrap().to_string(), "'y'");
}

/// Full annotated DNA grammar: leading comment, inline comment, trailing comment.
/// Comments are stripped; the grammar parses to the same structure as the uncommented version.
#[test]
fn annotated_dna_grammar_with_comments() {
let grammar_str = "; the building blocks of life!
<dna> ::= <base> | <base> <dna>
<base> ::= 'A' | 'C' | 'G' | 'T' ;(Adenine, Cytosine, Guanine, and Thymine)
; the end 📖";
let grammar: Grammar = grammar_str.parse().expect("parse annotated DNA grammar");

assert_eq!(
grammar.productions_iter().count(),
2,
"annotated grammar must have two productions (dna, base)"
);
let mut prods = grammar.productions_iter();
let dna = prods.next().unwrap();
assert_eq!(dna.lhs, Term::Nonterminal("dna".into()));
let dna_rhs: Vec<_> = dna.rhs_iter().map(|e| e.to_string()).collect();
assert_eq!(dna_rhs, ["<base>", "<base> <dna>"]);
let base = prods.next().unwrap();
assert_eq!(base.lhs, Term::Nonterminal("base".into()));
let base_rhs: Vec<_> = base.rhs_iter().map(|e| e.to_string()).collect();
assert_eq!(base_rhs, ["'A'", "'C'", "'G'", "'T'"]);
}
}
16 changes: 16 additions & 0 deletions tests/parse_input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,22 @@ fn dna_right_recursive() {
assert_snapshot!(parses.join("\n"));
}

#[test]
fn dna_annotated_grammar_parses_input() {
// Same DNA grammar as dna_right_recursive but with BNF comments throughout.
// Comments are stripped; parsing "GATTACA" yields the same parse trees.
let grammar: Grammar = "; the building blocks of life!
<dna> ::= <base> | <base> <dna>
<base> ::= 'A' | 'C' | 'G' | 'T' ;(Adenine, Cytosine, Guanine, and Thymine)
; the end 📖"
.parse()
.unwrap();

let input = "GATTACA";
let parses: Vec<_> = grammar.parse_input(input).map(|a| a.to_string()).collect();
assert_snapshot!(parses.join("\n"));
}

#[test]
fn ambiguous() {
let grammar: Grammar = "<start> ::= <a> | <b>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
source: tests/parse_input.rs
expression: "parses.join(\"\\n\")"
---
<dna> ::= <base> <dna>
├── <base> ::= "G"
│ └── "G"
└── <dna> ::= <base> <dna>
├── <base> ::= "A"
│ └── "A"
└── <dna> ::= <base> <dna>
├── <base> ::= "T"
│ └── "T"
└── <dna> ::= <base> <dna>
├── <base> ::= "T"
│ └── "T"
└── <dna> ::= <base> <dna>
├── <base> ::= "A"
│ └── "A"
└── <dna> ::= <base> <dna>
├── <base> ::= "C"
│ └── "C"
└── <dna> ::= <base>
└── <base> ::= "A"
└── "A"