Skip to main content

sipha_diff/
lib.rs

1//! Tree diff and comparison for sipha syntax trees.
2//!
3//! Compare two syntax trees by their emitted text (round-trip). Useful for
4//! tests (assert formatted output) or refactors (compare before/after).
5//!
6//! ## Grammar tests with S-expressions
7//!
8//! Use [`assert_parse_eq`] or the [`assert_parse!`] macro to compare a parsed
9//! tree against an expected S-expression string (e.g. `"(ROOT (EXPR (NUM \"1\")))"`).
10
11use sipha::emit::{syntax_root_to_string, EmitOptions};
12use sipha::red::SyntaxNode;
13pub use sipha::sexp::{syntax_node_to_sexp, SexpOptions};
14
15/// Returns true if both trees emit the same string (same tokens and trivia).
16#[inline]
17pub fn trees_equal(a: &SyntaxNode, b: &SyntaxNode) -> bool {
18    syntax_root_to_string(a, &EmitOptions::full()) == syntax_root_to_string(b, &EmitOptions::full())
19}
20
21/// Returns true if both trees have the same semantic content (ignoring trivia).
22#[inline]
23pub fn trees_equal_semantic(a: &SyntaxNode, b: &SyntaxNode) -> bool {
24    syntax_root_to_string(a, &EmitOptions::semantic_only())
25        == syntax_root_to_string(b, &EmitOptions::semantic_only())
26}
27
28/// Format a short diff message: "expected" vs "got" (full round-trip).
29pub fn format_diff(expected: &SyntaxNode, got: &SyntaxNode) -> String {
30    let expected_str = syntax_root_to_string(expected, &EmitOptions::full());
31    let got_str = syntax_root_to_string(got, &EmitOptions::full());
32    if expected_str == got_str {
33        return "trees are equal".to_string();
34    }
35    format!(
36        "expected ({} bytes):\n  {:?}\ngot ({} bytes):\n  {:?}",
37        expected_str.len(),
38        truncate_for_display(&expected_str, 200),
39        got_str.len(),
40        truncate_for_display(&got_str, 200)
41    )
42}
43
44fn truncate_for_display(s: &str, max: usize) -> String {
45    let s = s.replace('\n', "\\n");
46    if s.len() <= max {
47        s
48    } else {
49        format!("{}...", &s[..max])
50    }
51}
52
53// ─── Grammar test helpers (S-expression) ────────────────────────────────────
54
55/// Assert that parsing `input` yields a tree whose S-expression equals `expected_sexp`.
56///
57/// `parse_result` is typically `parse(input)` or `parse_expression(input)` returning
58/// `Result<Option<SyntaxNode>, E>`. On failure prints expected vs got S-expression.
59pub fn assert_parse_eq<E: std::fmt::Debug>(
60    parse_result: Result<Option<SyntaxNode>, E>,
61    _input: &str,
62    expected_sexp: &str,
63    options: &SexpOptions,
64) {
65    let root = parse_result
66        .unwrap_or_else(|e| panic!("parse failed: {:?}", e))
67        .expect("parse returned None (no root)");
68    let got = syntax_node_to_sexp(&root, options);
69    assert_eq!(
70        got, expected_sexp,
71        "S-expression mismatch:\nexpected:\n  {}\ngot:\n  {}",
72        expected_sexp, got
73    );
74}
75
76/// Assert that parsing `input` with `parse_fn` produces a tree matching `expected_sexp`.
77///
78/// Example:
79/// ```ignore
80/// assert_parse!(|s| leekscript::parse(s), "1 + 2", "(ROOT (EXPR ...))");
81/// ```
82#[macro_export]
83macro_rules! assert_parse {
84    ($parse_fn:expr, $input:expr, $expected_sexp:expr) => {
85        $crate::assert_parse_eq(
86            $parse_fn($input),
87            $input,
88            $expected_sexp,
89            &$crate::SexpOptions::semantic_only(),
90        )
91    };
92    ($parse_fn:expr, $input:expr, $expected_sexp:expr, $options:expr) => {
93        $crate::assert_parse_eq($parse_fn($input), $input, $expected_sexp, $options)
94    };
95}