Skip to main content

surrealguard_syntax/lower/
mod.rs

1//! CST → AST lowering.
2//!
3//! The single place that reads tree-sitter nodes and source text. Everything
4//! downstream (analyzers) consumes [`crate::ast`] values.
5//!
6//! Constructs without a modeled lowering — and statements whose direct
7//! syntax is broken — lower to explicit `Partial` values rather than being
8//! silently dropped.
9
10mod expr;
11mod statement;
12
13pub use expr::{lower_expr, lower_type_expr};
14pub(crate) use statement::lower_statement;
15pub use statement::lower_statements;
16
17use crate::ast::{Expr, PartialNode, Script, Spanned, Statement};
18use crate::parse::ParsedSource;
19use crate::span::ByteRange;
20use tree_sitter::Node;
21
22/// Lowers the first CST node of `cst_kind` (depth-first) as an expression.
23///
24/// The node-walking entry point for tests and tools that target a specific
25/// sub-expression (`FunctionCall`, `Block`, `BinaryExpression`, ...) without
26/// reaching for tree-sitter themselves.
27pub fn lower_first_expr(parsed: &ParsedSource, cst_kind: &str) -> Option<Spanned<Expr>> {
28    let node = find_first(parsed.tree().root_node(), cst_kind)?;
29    Some(lower_expr(node, parsed.text()))
30}
31
32/// Lowers the first CST node of `cst_kind` (depth-first) as a statement.
33pub fn lower_first_statement(parsed: &ParsedSource, cst_kind: &str) -> Option<Spanned<Statement>> {
34    let node = find_first(parsed.tree().root_node(), cst_kind)?;
35    Some(lower_statement(node, parsed.text()))
36}
37
38fn find_first<'tree>(node: Node<'tree>, cst_kind: &str) -> Option<Node<'tree>> {
39    if node.kind() == cst_kind {
40        return Some(node);
41    }
42    let mut cursor = node.walk();
43    let children: Vec<Node<'tree>> = node.children(&mut cursor).collect();
44    children
45        .into_iter()
46        .find_map(|child| find_first(child, cst_kind))
47}
48
49/// Lowers a parsed source to a [`Script`], statements in source order.
50pub fn lower(parsed: &ParsedSource) -> Script {
51    let root = parsed.tree().root_node();
52    let mut statements = Vec::new();
53
54    let mut cursor = root.walk();
55    for child in root.children(&mut cursor) {
56        if !child.is_named() || matches!(child.kind(), "Comment" | "BlockComment") {
57            continue;
58        }
59        statements.push(statement::lower_statement(child, parsed.text()));
60    }
61
62    Script { statements }
63}
64
65pub(crate) fn node_range(node: Node<'_>) -> ByteRange {
66    let start = u32::try_from(node.start_byte()).unwrap_or(u32::MAX);
67    let end = u32::try_from(node.end_byte()).unwrap_or(u32::MAX);
68    ByteRange::new(start, end).expect("tree-sitter nodes have ordered byte ranges")
69}
70
71pub(crate) fn partial(node: Node<'_>) -> PartialNode {
72    let cst_kind = if node.is_missing() {
73        format!("MISSING {}", node.kind())
74    } else {
75        node.kind().to_string()
76    };
77    PartialNode {
78        span: node_range(node),
79        cst_kind,
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use crate::ast::Statement;
87    use crate::parse::parse_source;
88    use crate::source::SourceId;
89
90    #[test]
91    fn script_lowering_preserves_statement_order() {
92        let parsed = parse_source(
93            SourceId::new("lower:script"),
94            "DEFINE TABLE person;\nSELECT * FROM person;\nRETURN 1;",
95        )
96        .expect("parses");
97
98        let script = lower(&parsed);
99
100        assert!(matches!(&script.statements[0].node, Statement::Define(_)));
101        assert!(matches!(&script.statements[1].node, Statement::Select(_)));
102        assert!(matches!(&script.statements[2].node, Statement::Return(_)));
103        // Source order is positional: each statement's span starts after the
104        // previous one ends.
105        let spans: Vec<_> = script.statements.iter().map(|s| s.span).collect();
106        assert!(spans.windows(2).all(|w| w[0].end() <= w[1].start()));
107    }
108}