aboutsummaryrefslogtreecommitdiff
path: root/tests/oracle.rs
diff options
context:
space:
mode:
Diffstat (limited to 'tests/oracle.rs')
-rw-r--r--tests/oracle.rs579
1 files changed, 579 insertions, 0 deletions
diff --git a/tests/oracle.rs b/tests/oracle.rs
index cab790f..1390511 100644
--- a/tests/oracle.rs
+++ b/tests/oracle.rs
@@ -1800,4 +1800,583 @@ mod parser_side {
failures[0]
);
}
+
+ // ------------------------------------------------------------
+ // Recognition gate (plan step 1), written by Claude Code.
+ //
+ // The recognizer (run with `no_item = true`, so no forest is built)
+ // must accept an input if and only if the input is derivable.
+ // Derivability is computed exactly, without a height bound, by a
+ // fixpoint over the facts `(X, i, j)`.
+ // ------------------------------------------------------------
+
+ /// Exact derivability of `tokens` from the start symbol.
+ fn exact_derivable(grammar: &Grammar, tokens: &[String]) -> bool {
+ let n = tokens.len();
+ let mut facts: std::collections::BTreeSet<(String, usize, usize)> = Default::default();
+
+ loop {
+ let before = facts.len();
+
+ for (nt, alternatives) in &grammar.rules {
+ for alt in alternatives {
+ for i in 0..=n {
+ // Positions reachable after matching a prefix of `alt`.
+ let mut ends = std::collections::BTreeSet::from([i]);
+
+ for symbol in alt {
+ let mut next = std::collections::BTreeSet::new();
+
+ for &k in &ends {
+ if grammar.is_nonterminal(symbol) {
+ for l in k..=n {
+ if facts.contains(&(symbol.clone(), k, l)) {
+ next.insert(l);
+ }
+ }
+ } else if k < n && tokens[k] == *symbol {
+ next.insert(k + 1);
+ }
+ }
+
+ ends = next;
+ }
+
+ for j in ends {
+ facts.insert((nt.clone(), i, j));
+ }
+ }
+ }
+ }
+
+ if facts.len() == before {
+ break;
+ }
+ }
+
+ facts.contains(&(grammar.start.clone(), 0, n))
+ }
+
+ /// Run only the recognizer, with the same timeout and panic guard as
+ /// `run_parser`. An error from `chain` counts as a rejection.
+ fn recognizes(setup: &Setup, input: &[String]) -> Result<bool, String> {
+ init();
+
+ let (setup, input) = (setup.clone(), input.to_vec());
+ let (sender, receiver) = std::sync::mpsc::channel();
+
+ std::thread::spawn(move || {
+ let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
+ let atom = DefaultAtom::from_grammar(parser_grammar(&setup))
+ .map_err(|e| format!("from_grammar: {e:?}"))?;
+ let mut chain = DefaultChain::unit(atom).map_err(|e| format!("unit: {e:?}"))?;
+
+ for (pos, token) in input.iter().enumerate() {
+ let id = setup.names.ters.iter().position(|t| t == token).unwrap();
+
+ if chain.chain(id, pos, true).is_err() {
+ return Ok(false);
+ }
+ }
+
+ chain.epsilon().map_err(|e| format!("epsilon: {e:?}"))
+ }));
+
+ let _ = sender.send(match outcome {
+ Ok(result) => result,
+ Err(_) => Err("parser panicked".to_string()),
+ });
+ });
+
+ receiver
+ .recv_timeout(std::time::Duration::from_secs(20))
+ .unwrap_or_else(|_| Err("TIMEOUT".to_string()))
+ }
+
+ /// Compare the recognizer with exact derivability on one input.
+ fn check_recognition(grammar_text: &str, input: &str) -> Result<(), String> {
+ let input = tokens(input);
+ let setup = Setup::new(grammar_text, &input);
+ let expected = exact_derivable(&setup.grammar, &input);
+
+ match recognizes(&setup, &input) {
+ Ok(got) if got == expected => Ok(()),
+ Ok(got) => Err(format!(
+ "grammar:\n{}\ninput: {:?}\nrecognizer says {got}, exact derivability is {expected}",
+ grammar_text.trim(),
+ input.join(" ")
+ )),
+ Err(e) => Err(format!(
+ "grammar:\n{}\ninput: {:?}\n{e}",
+ grammar_text.trim(),
+ input.join(" ")
+ )),
+ }
+ }
+
+ /// Every input of length 1 to `max_len` over `alphabet`.
+ fn all_inputs(alphabet: &[&str], max_len: usize) -> Vec<String> {
+ let mut result = Vec::new();
+ let mut layer: Vec<Vec<&str>> = vec![Vec::new()];
+
+ for _ in 0..max_len {
+ layer = layer
+ .iter()
+ .flat_map(|prefix| {
+ alphabet.iter().map(move |t| {
+ let mut v = prefix.clone();
+ v.push(t);
+ v
+ })
+ })
+ .collect();
+
+ result.extend(layer.iter().map(|v| v.join(" ")));
+ }
+
+ result
+ }
+
+ #[test]
+ fn gate_recognition_hand_cases() {
+ let cases: &[(&str, &[&str], usize)] = &[
+ ("S -> a a", &["a"], 4),
+ ("S -> a S | a", &["a"], 5),
+ ("S -> S a | a", &["a"], 5),
+ ("S -> S S | a", &["a"], 5),
+ ("S -> S S | S | a", &["a"], 4),
+ ("S -> S S | a | eps", &["a"], 4),
+ ("S -> S | a", &["a"], 3),
+ ("S -> A | a\nA -> S", &["a"], 3),
+ ("S -> A A\nA -> a | eps", &["a"], 4),
+ ("S -> a S | eps", &["a"], 4),
+ ("S -> a A b\nA -> eps | A c", &["a", "b", "c"], 5),
+ ("S -> A S a | a\nA -> eps", &["a"], 5),
+ ("S -> B a\nB -> a B | eps", &["a"], 5),
+ ("S -> b | a\nA -> A S A | eps", &["a", "b"], 5),
+ ("S -> X d\nX -> Y c\nY -> a", &["a", "c", "d"], 4),
+ ("S -> Y\nY -> A | B\nA -> x\nB -> x", &["x"], 3),
+ ("S -> A\nA -> B C\nB -> x | eps\nC -> y | eps", &["x", "y"], 4),
+ ("S -> A b | d\nA -> S c", &["b", "c", "d"], 5),
+ ("S -> x S | S a | a", &["x", "a"], 5),
+ ("S -> a S b | eps", &["a", "b"], 6),
+ ("E -> E + T | T\nT -> T * n | n", &["n", "+", "*"], 5),
+ ("E -> E + E | n", &["n", "+"], 5),
+ ];
+
+ let mut failures = Vec::new();
+
+ for (grammar, alphabet, max_len) in cases {
+ for input in all_inputs(alphabet, *max_len) {
+ if let Err(report) = check_recognition(grammar, &input) {
+ failures.push(report);
+ }
+ }
+ }
+
+ assert!(
+ failures.is_empty(),
+ "\n{} mismatches; first:\n{}\n",
+ failures.len(),
+ failures.join("\n--\n")
+ );
+ }
+
+ #[test]
+ fn gate_recognition_random_grammars() {
+ let seed: u64 = std::env::var("REP_SEED")
+ .ok()
+ .and_then(|s| s.parse().ok())
+ .unwrap_or(0x5EED);
+ let cases: usize = std::env::var("REP_CASES")
+ .ok()
+ .and_then(|s| s.parse().ok())
+ .unwrap_or(200);
+
+ eprintln!("recognition gate: seed = {seed}, cases = {cases}");
+
+ let mut rng = Rng(seed | 1);
+ let mut failures = Vec::new();
+
+ for _ in 0..cases {
+ let grammar_text = random_grammar(&mut rng);
+ let len = 1 + rng.below(6);
+ let input: Vec<&str> = (0..len).map(|_| ["a", "b"][rng.below(2)]).collect();
+
+ if let Err(report) = check_recognition(&grammar_text, &input.join(" ")) {
+ failures.push(report);
+ }
+ }
+
+ assert!(
+ failures.is_empty(),
+ "\n{} of {cases} mismatches (seed {seed}); first:\n{}\n",
+ failures.len(),
+ failures[0]
+ );
+ }
+
+ /// Print the chain after every token (recognizer only), for
+ /// `DIAG_GRAMMAR` and `DIAG_INPUT`. Written by Claude Code.
+ #[test]
+ #[ignore]
+ fn probe_chain() {
+ init();
+ let g = std::env::var("DIAG_GRAMMAR").unwrap_or("S -> A A\nA -> a | eps".into());
+ let i = std::env::var("DIAG_INPUT").unwrap_or("a a a".into());
+ let toks = tokens(&i);
+ let setup = Setup::new(&g, &toks);
+ let atom = DefaultAtom::from_grammar(parser_grammar(&setup)).unwrap();
+ if std::env::var("DIAG_ATOM").is_ok() {
+ use chain::atom::Atom;
+ for node in 0..atom.nodes_len() {
+ let name = atom.rule_pos_string(node / 2).map(|n| format!("{n} ({})", node % 2)).unwrap_or_default();
+ for (label, targets) in atom.labels_of(node).unwrap() {
+ println!("ATOM {node} [{name}] --{label}--> {:?} (acc={})", targets.collect::<Vec<_>>(), atom.is_accepting(node).unwrap());
+ }
+ }
+ }
+ let mut chain = DefaultChain::unit(atom).unwrap();
+
+ for (pos, token) in toks.iter().enumerate() {
+ let id = setup.names.ters.iter().position(|t| t == token).unwrap();
+ let result = chain.chain(id, pos, true);
+ println!("after token {pos} ({token}): {result:?}; current = {}, accepting = {:?}",
+ chain.current(), chain.epsilon());
+
+ for (label, targets) in chain.labels_of(chain.current()).unwrap() {
+ let name = chain.atom().rule_pos_string(label.label()).unwrap_or_else(|_| format!("#{}", label.label()));
+ println!(" {name} [state {}] frame={} acc={} -> {:?}",
+ label.label(), label.frame_nt(), label.is_accepting(), targets.collect::<Vec<_>>());
+ }
+ }
+ }
+
+ // ------------------------------------------------------------
+ // Facts gate (plan step 10), written by Claude Code.
+ //
+ // While the parser reads the input it records facts `(X, i, j)`.
+ // The recording alone is checked here, without the forest: after
+ // an eager closure of the recorded facts,
+ //
+ // - no fact is missing: every nonterminal node of every oracle
+ // tree (of height at most `h`) with a non-empty span is a fact;
+ // - the input is derivable from the facts (plus the empty spans,
+ // which come from nullability) if and only if it is derivable.
+ // ------------------------------------------------------------
+
+ /// The facts `(X, i, j)` with `i < j` of a tree in the notation of
+ /// the oracle.
+ fn tree_facts(tree: &str, nons: &[String]) -> BTreeSet<(usize, usize, usize)> {
+ let padded = tree.replace('(', " ( ").replace(')', " ) ");
+ let words: Vec<&str> = padded.split_whitespace().collect();
+
+ let mut result = BTreeSet::new();
+ let mut stack: Vec<(usize, usize)> = Vec::new();
+ let mut at = 0usize;
+ let mut k = 0usize;
+
+ while k < words.len() {
+ match words[k] {
+ "(" => {
+ let name = words[k + 1];
+ stack.push((nons.iter().position(|n| n == name).unwrap(), at));
+ k += 1;
+ }
+ ")" => {
+ let (nt, start) = stack.pop().unwrap();
+
+ if at > start {
+ result.insert((nt, start, at));
+ }
+ }
+ "eps" => {}
+ _ => at += 1,
+ }
+
+ k += 1;
+ }
+
+ result
+ }
+
+ /// Derivability when a nonterminal may derive a non-empty span
+ /// only if that is a recorded fact.
+ fn derivable_from_facts(
+ grammar: &Grammar,
+ nons: &[String],
+ tokens: &[String],
+ facts: &BTreeSet<(usize, usize, usize)>,
+ ) -> bool {
+ let n = tokens.len();
+ let id = |name: &str| nons.iter().position(|x| x == name).unwrap();
+ let mut known: BTreeSet<(usize, usize, usize)> = BTreeSet::new();
+
+ loop {
+ let before = known.len();
+
+ for (nt, alternatives) in &grammar.rules {
+ for alt in alternatives {
+ for i in 0..=n {
+ let mut ends = BTreeSet::from([i]);
+
+ for symbol in alt {
+ let mut next = BTreeSet::new();
+
+ for &k in &ends {
+ if grammar.is_nonterminal(symbol) {
+ for l in k..=n {
+ if known.contains(&(id(symbol), k, l)) {
+ next.insert(l);
+ }
+ }
+ } else if k < n && tokens[k] == *symbol {
+ next.insert(k + 1);
+ }
+ }
+
+ ends = next;
+ }
+
+ for j in ends {
+ if i == j || facts.contains(&(id(nt), i, j)) {
+ known.insert((id(nt), i, j));
+ }
+ }
+ }
+ }
+ }
+
+ if known.len() == before {
+ break;
+ }
+ }
+
+ known.contains(&(0, 0, n))
+ }
+
+ /// Run the parser with the forest requested and return the chain.
+ fn run_chain(setup: &Setup, input: &[String]) -> Result<DefaultChain, String> {
+ init();
+
+ std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
+ let atom = DefaultAtom::from_grammar(parser_grammar(setup))
+ .map_err(|e| format!("from_grammar: {e:?}"))?;
+ let mut chain = DefaultChain::unit(atom).map_err(|e| format!("unit: {e:?}"))?;
+
+ for (pos, token) in input.iter().enumerate() {
+ let id = setup.names.ters.iter().position(|t| t == token).unwrap();
+
+ chain
+ .chain(id, pos, false)
+ .map_err(|e| format!("chain() failed at token {pos}: {e:?}"))?;
+ }
+
+ Ok(chain)
+ }))
+ .unwrap_or_else(|_| Err("parser panicked".to_string()))
+ }
+
+ fn check_facts(grammar_text: &str, input: &str, h: usize) -> Result<(), String> {
+ let input = tokens(input);
+ let setup = Setup::new(grammar_text, &input);
+ let header = format!("grammar:\n{}\ninput: {:?}", grammar_text.trim(), input.join(" "));
+
+ let chain = run_chain(&setup, &input).map_err(|e| format!("{header}\n{e}"))?;
+
+ let facts = chain
+ .facts()
+ .eager_closure(chain.tokens(), chain.atom())
+ .map_err(|e| format!("{header}\neager closure: {e:?}"))?;
+
+ let mut problems = Vec::new();
+
+ let mut wanted = BTreeSet::new();
+
+ for tree in oracle_trees(&setup.grammar, &input, h) {
+ wanted.extend(tree_facts(&tree, &setup.names.nons));
+ }
+
+ let name = |f: &(usize, usize, usize)| format!("{}[{},{}]", setup.names.nons[f.0], f.1, f.2);
+
+ let missing: Vec<String> = wanted.difference(&facts).map(name).collect();
+
+ if !missing.is_empty() {
+ problems.push(format!("missing facts: {missing:?}"));
+ }
+
+ let exact = exact_derivable(&setup.grammar, &input);
+ let from_facts = derivable_from_facts(&setup.grammar, &setup.names.nons, &input, &facts);
+
+ if exact != from_facts {
+ problems.push(format!("derivable from facts is {from_facts}, exact derivability is {exact}"));
+ }
+
+ if problems.is_empty() {
+ Ok(())
+ } else {
+ Err(format!("{header}\n{}", problems.join("\n")))
+ }
+ }
+
+ #[test]
+ fn gate_facts_hand_cases() {
+ let cases: &[(&str, &str, usize)] = &[
+ ("S -> a a", "a a", 3),
+ ("S -> a S | a", "a a a a", 6),
+ ("S -> A b\nA -> a", "a b", 3),
+ ("S -> A B\nA -> a\nB -> b", "a b", 3),
+ ("S -> S S | a", "a a a a a a", 6),
+ ("E -> E + E | n", "n + n + n", 5),
+ ("S -> A A\nA -> a | eps", "a", 4),
+ ("S -> a S | eps", "a a", 5),
+ ("S -> a A b\nA -> eps | A c", "a c c b", 5),
+ ("S -> S a | a", "a a a a", 5),
+ ("S -> A S a | a\nA -> eps", "a a a", 5),
+ ("S -> S | a", "a", 4),
+ ("S -> A | a\nA -> S", "a", 5),
+ ("S -> S S | S | a", "a a", 4),
+ ("S -> S S | a | eps", "a a", 4),
+ ("S -> S S S | a", "a a a a a a a a a", 5),
+ ("S -> S S | S a | a", "a a a a a a a", 5),
+ ("S -> A b | d\nA -> S c", "d c b c b", 6),
+ ("S -> x S | S a | a", "x x a a a", 6),
+ ("E -> E + T | T\nT -> T * n | n", "n + n * n + n", 6),
+ ("S -> Y c\nY -> A | B\nA -> x\nB -> x", "x c", 5),
+ ("S -> Y\nY -> A | B\nA -> x\nB -> x", "x", 5),
+ ("S -> Y c | Y\nY -> A d | B\nA -> B\nB -> x", "x d c", 6),
+ ("S -> A c | B c\nA -> x\nB -> x", "x c", 4),
+ ("S -> A\nA -> B C\nB -> x | eps\nC -> y | eps", "x", 5),
+ ("S -> A B c\nA -> x | eps\nB -> A | eps", "x c", 5),
+ ];
+
+ let mut failures = Vec::new();
+
+ for (grammar, input, h) in cases {
+ if let Err(report) = check_facts(grammar, input, *h) {
+ failures.push(report);
+ }
+ }
+
+ assert!(
+ failures.is_empty(),
+ "\n{} failing cases; first:\n{}\n",
+ failures.len(),
+ failures[0]
+ );
+ }
+
+ #[test]
+ fn gate_facts_random_grammars() {
+ let seed: u64 = std::env::var("REP_SEED")
+ .ok()
+ .and_then(|s| s.parse().ok())
+ .unwrap_or(0x5EED);
+ let cases: usize = std::env::var("REP_CASES")
+ .ok()
+ .and_then(|s| s.parse().ok())
+ .unwrap_or(150);
+
+ eprintln!("facts gate: seed = {seed}, cases = {cases}");
+
+ let mut rng = Rng(seed | 1);
+ let mut failures = Vec::new();
+
+ for _ in 0..cases {
+ let grammar_text = random_grammar(&mut rng);
+ let len = 1 + rng.below(6);
+ let input: Vec<&str> = (0..len).map(|_| ["a", "b"][rng.below(2)]).collect();
+
+ if let Err(report) = check_facts(&grammar_text, &input.join(" "), 4) {
+ failures.push(report);
+ }
+ }
+
+ assert!(
+ failures.is_empty(),
+ "\n{} of {cases} failing (seed {seed}); first:\n{}\n",
+ failures.len(),
+ failures[0]
+ );
+ }
+
+ // ------------------------------------------------------------
+ // Sizes (plan section 4.6): the forest must be polynomial, and
+ // for the linear grammars below, linear in the input.
+ // ------------------------------------------------------------
+
+ fn forest_size(grammar_text: &str, input: &str) -> (usize, usize) {
+ let input = tokens(input);
+ let setup = Setup::new(grammar_text, &input);
+ let start = std::time::Instant::now();
+ let forest = run_parser(&setup, &input).expect("the parser fails");
+ let _ = start.elapsed();
+
+ (forest.nodes_len(), forest.edges().count())
+ }
+
+ #[test]
+ fn size_linear_grammars_have_linear_forests() {
+ for (grammar, token) in [
+ ("S -> a S | eps", "a"),
+ ("S -> S a | a", "a"),
+ ("S -> a S b | eps", "a b"),
+ ] {
+ let make = |n: usize| {
+ if grammar.contains('b') {
+ format!("{} {}", vec!["a"; n].join(" "), vec!["b"; n].join(" "))
+ } else {
+ vec![token; n].join(" ")
+ }
+ };
+
+ let (small, _) = forest_size(grammar, &make(20));
+ let (large, _) = forest_size(grammar, &make(40));
+
+ assert!(
+ large <= 2 * small + 8,
+ "{grammar}: {small} nodes at 20 and {large} nodes at 40 are not linear"
+ );
+ }
+ }
+
+ #[test]
+ fn size_ambiguous_grammar_is_cubic_at_most() {
+ let (small, _) = forest_size("S -> S S | a", &vec!["a"; 10].join(" "));
+ let (large, _) = forest_size("S -> S S | a", &vec!["a"; 20].join(" "));
+
+ // O(n^2) nodes with O(n) alternatives each; doubling the input
+ // multiplies the nodes by about 4 to 8.
+ assert!(large <= 10 * small, "{small} nodes at 10 and {large} nodes at 20");
+ }
+
+ /// Recognition of long inputs (plan step 6): no quadratic behaviour
+ /// for left and right recursion and for the expression grammar.
+ #[test]
+ fn recognition_time_on_long_inputs() {
+ init();
+
+ for (grammar, input) in [
+ ("S -> a S | a", vec!["a"; 400].join(" ")),
+ ("S -> S a | a", vec!["a"; 400].join(" ")),
+ ("E -> E + T | T\nT -> T * n | n", vec!["n"; 200].join(" + ")),
+ ] {
+ let toks = tokens(&input);
+ let setup = Setup::new(grammar, &toks);
+ let atom = DefaultAtom::from_grammar(parser_grammar(&setup)).unwrap();
+ let mut chain = DefaultChain::unit(atom).unwrap();
+ let start = std::time::Instant::now();
+
+ for (pos, token) in toks.iter().enumerate() {
+ let id = setup.names.ters.iter().position(|t| t == token).unwrap();
+ chain.chain(id, pos, true).unwrap();
+ }
+
+ let elapsed = start.elapsed();
+
+ assert!(chain.epsilon().unwrap());
+ eprintln!("{} tokens, {grammar:?}: {elapsed:?}", toks.len());
+ assert!(elapsed < std::time::Duration::from_secs(10), "{grammar}: {elapsed:?}");
+ }
+ }
}