Skip to main content

conformance/
conformance.rs

1//! Grammar conformance runner: feeds a corpus of known-valid SurrealQL
2//! (extracted from SurrealDB's own test suites) through the tree-sitter
3//! grammar and reports anything that fails to parse cleanly.
4//!
5//!     cargo run -p surrealguard-syntax --example conformance -- corpus.json
6//!
7//! The corpus file is a JSON array of query strings. Output is one line
8//! per failing entry with the first ERROR/MISSING region, plus a summary —
9//! the working list for the grammar fork.
10
11use std::fmt::Write as _;
12
13fn main() {
14    let path = std::env::args()
15        .nth(1)
16        .expect("usage: conformance <corpus.json>");
17    let raw = std::fs::read_to_string(&path).expect("corpus file readable");
18    let corpus: Vec<String> = parse_json_strings(&raw);
19
20    let mut failures = 0usize;
21    let mut report = String::new();
22    for (index, query) in corpus.iter().enumerate() {
23        let source = surrealguard_syntax::source::SourceId::new(format!("corpus:{index}"));
24        let Ok(parsed) = surrealguard_syntax::parse::parse_source(source, query.as_str()) else {
25            failures += 1;
26            let _ = writeln!(report, "#{index}: parser returned no tree");
27            continue;
28        };
29        if let Some(range) = first_broken_range(parsed.tree().root_node()) {
30            failures += 1;
31            let snippet: String = query[range.clone()].chars().take(60).collect();
32            let context: String = query.chars().take(80).collect();
33            let _ = writeln!(
34                report,
35                "#{index}: ERROR at {range:?}: `{}`\n    in: {}",
36                snippet.replace('\n', " "),
37                context.replace('\n', " ")
38            );
39        }
40    }
41
42    print!("{report}");
43    println!(
44        "---\n{failures}/{} corpus entries fail to parse",
45        corpus.len()
46    );
47    if failures > 0 {
48        std::process::exit(1);
49    }
50}
51
52fn first_broken_range(node: tree_sitter::Node<'_>) -> Option<std::ops::Range<usize>> {
53    if node.is_error() || node.is_missing() {
54        return Some(node.byte_range());
55    }
56    if !node.has_error() {
57        return None;
58    }
59    let mut cursor = node.walk();
60    let children: Vec<_> = node.children(&mut cursor).collect();
61    for child in children {
62        if let Some(range) = first_broken_range(child) {
63            return Some(range);
64        }
65    }
66    Some(node.byte_range())
67}
68
69/// Just enough JSON to read an array of strings without a dependency.
70fn parse_json_strings(raw: &str) -> Vec<String> {
71    let mut out = Vec::new();
72    let mut chars = raw.chars().peekable();
73    while let Some(c) = chars.next() {
74        if c != '"' {
75            continue;
76        }
77        let mut s = String::new();
78        while let Some(c) = chars.next() {
79            match c {
80                '"' => break,
81                '\\' => match chars.next() {
82                    Some('n') => s.push('\n'),
83                    Some('t') => s.push('\t'),
84                    Some('u') => {
85                        let hex: String = (0..4).filter_map(|_| chars.next()).collect();
86                        if let Ok(code) = u32::from_str_radix(&hex, 16) {
87                            if let Some(ch) = char::from_u32(code) {
88                                s.push(ch);
89                            }
90                        }
91                    }
92                    Some(other) => s.push(other),
93                    None => break,
94                },
95                other => s.push(other),
96            }
97        }
98        out.push(s);
99    }
100    out
101}