use super::candidates::{accepts_token, reject_candidates, Candidate};
use super::error::GrammarError;
use super::grammars::{ARITHMETIC_GBNF, JAPANESE_GBNF, JSON_GBNF, LIST_GBNF, SHIPPED_GRAMMARS};
use super::machine::Grammar;
use super::utf8::PartialUtf8;
fn grammar(src: &str) -> Grammar {
Grammar::from_str_with_root(src, "root")
.unwrap_or_else(|e| panic!("failed to build a machine for the grammar: {e}"))
}
fn feed(g: &mut Grammar, text: &str) -> Result<(), GrammarError> {
g.accept_str(text)
}
fn accepts_whole(src: &str, text: &str) -> bool {
let mut g = grammar(src);
match feed(&mut g, text) {
Ok(()) => g.allows_eog(),
Err(_) => false,
}
}
#[test]
fn json_grammar_tracks_brace_depth() {
assert!(accepts_whole(JSON_GBNF, r#"{"a": 1}"#));
assert!(accepts_whole(JSON_GBNF, r#"{"a": {"b": [1, 2, null]}}"#));
assert!(accepts_whole(JSON_GBNF, "{}"));
assert!(
!accepts_whole(JSON_GBNF, r#"{"a": 1}}"#),
"extra close brace"
);
assert!(!accepts_whole(JSON_GBNF, r#"{"a": 1"#), "unclosed object");
assert!(
!accepts_whole(JSON_GBNF, r#"{"a": [1, 2}"#),
"bracket closed by a brace"
);
assert!(!accepts_whole(JSON_GBNF, r#"{"a" 1}"#), "missing colon");
assert!(!accepts_whole(JSON_GBNF, r#"{"a": ,}"#), "missing value");
assert!(!accepts_whole(JSON_GBNF, r#"{a: 1}"#), "unquoted key");
}
#[test]
fn json_grammar_refuses_a_close_brace_as_the_very_first_character() {
let mut g = grammar(JSON_GBNF);
let err = feed(&mut g, "}").unwrap_err();
assert!(matches!(err, GrammarError::NoViableStack { .. }), "{err:?}");
assert!(g.is_dead());
}
#[test]
fn a_complete_json_object_allows_end_of_generation_and_an_incomplete_one_does_not() {
let mut g = grammar(JSON_GBNF);
feed(&mut g, "{").unwrap();
assert!(!g.allows_eog(), "an open object is not a finished parse");
assert!(g.accept_eog().is_err());
feed(&mut g, "}").unwrap();
assert!(g.allows_eog(), "a closed object is");
assert!(g.accept_eog().is_ok());
}
#[test]
fn arithmetic_grammar_accepts_its_own_examples() {
assert!(accepts_whole(ARITHMETIC_GBNF, "1+2=3\n"));
assert!(accepts_whole(ARITHMETIC_GBNF, "x = (a+b)\n"));
assert!(
!accepts_whole(ARITHMETIC_GBNF, "1+2=3"),
"no trailing newline"
);
assert!(!accepts_whole(ARITHMETIC_GBNF, "1+2\n"), "no '='");
assert!(
!accepts_whole(ARITHMETIC_GBNF, "1+2=(3\n"),
"unclosed paren"
);
assert!(!accepts_whole(ARITHMETIC_GBNF, "x = (a+b) * 2\n"));
}
#[test]
fn list_grammar_needs_the_bullet_and_the_newline() {
assert!(accepts_whole(LIST_GBNF, "- one\n- two\n"));
assert!(!accepts_whole(LIST_GBNF, "one\n"), "no bullet");
assert!(!accepts_whole(LIST_GBNF, "- one"), "no newline");
assert!(!accepts_whole(LIST_GBNF, "- a\rb\n"), "\\r is excluded");
assert!(
!accepts_whole(LIST_GBNF, "- a\u{0b}b\n"),
"\\x0b is excluded"
);
assert!(
!accepts_whole(LIST_GBNF, "- a\u{2028}b\n"),
"U+2028 is excluded"
);
assert!(accepts_whole(LIST_GBNF, "- a\u{2027}b\n"), "U+2027 is not");
}
#[test]
fn every_shipped_grammar_builds_a_machine_with_at_least_one_stack() {
for (name, src, _) in SHIPPED_GRAMMARS {
let g = Grammar::from_str_with_root(src, "root").unwrap_or_else(|e| panic!("{name}: {e}"));
assert!(!g.stacks().is_empty(), "{name} starts with no viable stack");
}
}
#[test]
fn a_negated_class_excludes_every_member_not_just_the_first() {
assert!(!accepts_whole("root ::= [^ab]", "a"));
assert!(!accepts_whole("root ::= [^ab]", "b"));
assert!(accepts_whole("root ::= [^ab]", "c"));
}
#[test]
fn a_negated_range_excludes_the_whole_range() {
assert!(!accepts_whole("root ::= [^1-3]", "1"));
assert!(!accepts_whole("root ::= [^1-3]", "2"));
assert!(!accepts_whole("root ::= [^1-3]", "3"));
assert!(accepts_whole("root ::= [^1-3]", "4"));
assert!(accepts_whole("root ::= [^1-3]", "0"));
}
#[test]
fn a_class_mixing_singles_and_ranges_accepts_all_of_them() {
for c in ["b", "d", "x", "y", "z"] {
assert!(accepts_whole("root ::= [bdx-z]", c), "{c} should match");
}
for c in ["a", "c", "w"] {
assert!(
!accepts_whole("root ::= [bdx-z]", c),
"{c} should not match"
);
}
}
#[test]
fn dot_matches_any_character_including_a_multibyte_one() {
assert!(accepts_whole("root ::= .", "a"));
assert!(accepts_whole("root ::= .", "\n"));
assert!(accepts_whole("root ::= .", "€"));
assert!(!accepts_whole("root ::= .", "ab"), "exactly one");
}
#[test]
fn multibyte_ranges_bound_correctly() {
assert!(accepts_whole(JAPANESE_GBNF, "ひらがな"));
assert!(accepts_whole(JAPANESE_GBNF, "カタカナ"));
assert!(accepts_whole(JAPANESE_GBNF, "漢字"));
assert!(!accepts_whole(JAPANESE_GBNF, "€"), "outside every class");
}
#[test]
fn a_token_that_ends_mid_codepoint_stays_viable() {
let hira = "ひ".as_bytes();
let mut g = grammar(JAPANESE_GBNF);
g.accept_bytes(&hira[..1])
.expect("a partial codepoint must survive");
assert_eq!(g.partial_utf8().n_remain, 2);
assert!(!g.is_dead());
g.accept_bytes(&hira[1..]).expect("and complete");
assert_eq!(g.partial_utf8().n_remain, 0);
assert!(g.allows_eog());
}
#[test]
fn a_partial_codepoint_that_cannot_complete_into_the_class_is_rejected() {
let g = grammar("root ::= [a-z]+");
let euro_lead = &"€".as_bytes()[..1];
assert!(
!accepts_token(&g, 1, euro_lead).unwrap(),
"no continuation of a 3-byte lead lands in [a-z]"
);
let g2 = grammar("root ::= [\\u2000-\\u2100]+");
assert!(accepts_token(&g2, 1, euro_lead).unwrap());
}
#[test]
fn an_overlong_two_byte_partial_is_refused() {
let g = grammar("root ::= .");
assert!(!accepts_token(&g, 1, &[0xC0u8]).unwrap());
}
#[test]
fn a_hand_built_partial_with_an_impossible_length_is_refused_not_a_panic() {
use super::element::RulePos;
use super::machine::match_partial_char;
let g = grammar("root ::= [a-z]");
let verdict = match_partial_char(g.rules(), RulePos::new(0, 0), PartialUtf8::new(1, 9));
assert_eq!(verdict, Ok(false));
}
#[test]
fn a_token_element_matches_on_id_not_on_text() {
let mut g = grammar("root ::= <[1000]> \"a\"");
g.accept_token(1000, b"anything at all")
.expect("id matches");
g.accept_str("a").expect("then the literal");
assert!(g.allows_eog());
let mut g2 = grammar("root ::= <[1000]> \"a\"");
let err = g2.accept_token(1001, b"anything at all").unwrap_err();
assert!(matches!(err, GrammarError::NoViableStack { .. }), "{err:?}");
}
#[test]
fn an_inverted_token_element_matches_everything_but_its_id() {
let mut g = grammar("root ::= !<[1001]>");
g.accept_token(7, b"x").expect("7 is not 1001");
assert!(g.allows_eog());
let mut g2 = grammar("root ::= !<[1001]>");
assert!(g2.accept_token(1001, b"x").is_err());
}
#[test]
fn accept_str_cannot_satisfy_a_token_element() {
let mut g = grammar("root ::= <[1000]>");
assert!(g.accept_str("a").is_err());
}
#[test]
fn candidate_rejection_over_a_token_element_uses_the_id() {
let g = grammar("root ::= <[1000]> \"a\"");
let cands = [
Candidate::new(0, 1000, b"whatever"),
Candidate::new(1, 1001, b"whatever"),
Candidate::new(2, 1000, b"a"),
];
let mut rejected = reject_candidates(&g, &cands).unwrap();
rejected.sort_unstable();
assert_eq!(rejected, vec![1]);
}
#[test]
fn direct_left_recursion_is_refused() {
let err = Grammar::from_str_with_root("root ::= root \"a\" | \"a\"", "root").unwrap_err();
assert!(matches!(err, GrammarError::LeftRecursion { .. }), "{err:?}");
}
#[test]
fn indirect_left_recursion_is_refused() {
let err = Grammar::from_str_with_root("root ::= a \"x\"\na ::= root \"y\" | \"y\"", "root")
.unwrap_err();
assert!(matches!(err, GrammarError::LeftRecursion { .. }), "{err:?}");
}
#[test]
fn left_recursion_through_a_nullable_prefix_is_refused() {
let err =
Grammar::from_str_with_root("root ::= opt root \"a\" | \"a\"\nopt ::= \"b\" |", "root")
.unwrap_err();
assert!(matches!(err, GrammarError::LeftRecursion { .. }), "{err:?}");
}
#[test]
fn right_recursion_is_fine() {
assert!(accepts_whole("root ::= \"a\" root | \"a\"", "aaaa"));
}
#[test]
fn a_missing_root_is_named() {
let err = Grammar::from_str_with_root("start ::= \"a\"", "root").unwrap_err();
match err {
GrammarError::MissingRoot { name } => assert_eq!(name, "root"),
other => panic!("expected MissingRoot, got {other:?}"),
}
}
#[test]
fn rejection_masks_exactly_the_impossible_tokens() {
let g = grammar(JSON_GBNF);
let pieces = ["{", "}", "[", "\"", "1", " ", "{\"", "null"];
let cands: Vec<Candidate> = pieces
.iter()
.enumerate()
.map(|(i, p)| Candidate::new(i, i as u32, p.as_bytes()))
.collect();
let mut rejected = reject_candidates(&g, &cands).unwrap();
rejected.sort_unstable();
assert_eq!(rejected, vec![1, 2, 3, 4, 5, 7]);
}
#[test]
fn rejection_is_empty_when_every_candidate_is_possible() {
let g = grammar("root ::= [a-c]+");
let cands = [
Candidate::new(0, 0, b"a"),
Candidate::new(1, 1, b"b"),
Candidate::new(2, 2, b"abc"),
];
assert!(reject_candidates(&g, &cands).unwrap().is_empty());
}
#[test]
fn a_multi_character_token_is_rejected_if_any_of_its_characters_is() {
let g = grammar("root ::= \"ab\" \"cd\"");
assert!(accepts_token(&g, 0, b"ab").unwrap());
assert!(accepts_token(&g, 0, b"abc").unwrap());
assert!(accepts_token(&g, 0, b"abcd").unwrap());
assert!(!accepts_token(&g, 0, b"ax").unwrap());
assert!(!accepts_token(&g, 0, b"abce").unwrap());
assert!(!accepts_token(&g, 0, b"abcde").unwrap(), "one past the end");
}
#[test]
fn a_satisfied_grammar_rejects_every_token_that_adds_text() {
let mut g = grammar("root ::= \"a\"");
g.accept_str("a").unwrap();
assert!(g.allows_eog());
let cands = [Candidate::new(0, 0, b"b"), Candidate::new(1, 1, b"")];
let mut rejected = reject_candidates(&g, &cands).unwrap();
rejected.sort_unstable();
assert_eq!(rejected, vec![0]);
}
#[test]
fn a_dead_grammar_rejects_everything() {
let mut g = grammar("root ::= \"a\"");
let _ = g.accept_str("z");
assert!(g.is_dead());
let cands = [Candidate::new(0, 0, b"a"), Candidate::new(1, 1, b"b")];
let mut rejected = reject_candidates(&g, &cands).unwrap();
rejected.sort_unstable();
assert_eq!(rejected, vec![0, 1]);
}
#[test]
fn nested_alternation_keeps_more_than_one_viable_stack() {
let g = grammar("root ::= choice \"!\"\nchoice ::= \"a\" | \"b\" | \"c\"");
assert!(g.stacks().len() >= 3, "got {:?}", g.stacks());
for c in ["a", "b", "c"] {
assert!(accepts_whole(
"root ::= choice \"!\"\nchoice ::= \"a\" | \"b\" | \"c\"",
&format!("{c}!")
));
}
assert!(!accepts_token(&g, 0, b"d").unwrap());
}
#[test]
fn the_empty_piece_is_the_one_case_the_two_paths_read_differently() {
let mut g = grammar("root ::= \"a\"");
g.accept_str("a").unwrap();
assert!(g.stacks().iter().all(|s| s.is_empty()), "fully satisfied");
assert!(
accepts_token(&g, 0, b"").unwrap(),
"the batch path keeps the empty piece"
);
assert!(
g.clone().accept_token(0, b"").is_err(),
"the replay path drops the finished stack and dies"
);
}
#[test]
fn the_two_acceptance_paths_agree() {
let pieces = [
"a", "b", "z", "{", "}", "[", "]", "\"", ":", ",", " ", "\n", "0", "1", "9", "-", "+", "e",
"true", "null", "{\"a", "\": ", "ab", "abc", "- ", "1. ", "€", "ひ", "\t",
];
let mut checked = 0usize;
for (name, src, _) in SHIPPED_GRAMMARS {
let mut g =
Grammar::from_str_with_root(src, "root").unwrap_or_else(|e| panic!("{name}: {e}"));
for step in 0..4 {
for piece in pieces {
let batch = accepts_token(&g, 0, piece.as_bytes())
.unwrap_or_else(|e| panic!("{name} step {step} piece {piece:?}: {e}"));
let mut replay = g.clone();
let replayed = replay.accept_token(0, piece.as_bytes()).is_ok();
assert_eq!(
batch, replayed,
"{name} step {step}: reject_candidates and accept_token disagree on {piece:?}"
);
checked += 1;
}
let Some(next) = pieces
.iter()
.find(|p| accepts_token(&g, 0, p.as_bytes()).unwrap_or(false))
else {
break;
};
g.accept_token(0, next.as_bytes())
.expect("just checked it is viable");
}
}
assert!(checked > 500, "only {checked} comparisons ran");
}