Skip to main content

quarb_code/
lib.rs

1//! Source-code AST adapter for the Quarb query engine.
2//!
3//! The trait is named `AstAdapter`; this adapter takes the name
4//! literally: a source file parses (tree-sitter) into its syntax
5//! tree, and the tree is the arbor. Node kinds are the edge names
6//! (`//function_item`, `//call_expression`), a node's value
7//! (`::`) is its source text, and tree-sitter's *fields* become
8//! properties — `::name` on a `function_item` is the function's
9//! name, `::body` its block — so
10//! `//function_item[::name = "main"]` reads as it should.
11//!
12//! Only *named* nodes appear (punctuation and keywords are
13//! syntax, not structure). Metadata: `;;;kind`, `;;;field` (this
14//! node's field name in its parent), `;;;start-line` /
15//! `;;;end-line` (1-based), `;;;n-children`. //! Python, JavaScript, by extension (`rs`, `py`, `js`); the
16//! grammar set is compile-time and easily grown.
17//!
18//! Composed (`qua --descend`), source files graft like JSON does
19//! — `//function_item::name` over a whole directory tree is one
20//! query across every parsed file.
21
22use quarb::{AstAdapter, NodeId, Value};
23
24mod ast_cache;
25pub use ast_cache::Cache;
26
27thread_local! {
28    /// The AST cache for this thread, or `None` (uncached). Set once
29    /// by the CLI from `--cache`; consulted by every `parse` call, so
30    /// single-file and `--descend` (via quarb-compose) both benefit.
31    static CACHE: std::cell::RefCell<Option<Cache>> = const { std::cell::RefCell::new(None) };
32}
33
34/// Enable or disable the persistent AST cache for this thread.
35pub fn set_cache(cache: Option<Cache>) {
36    CACHE.with(|c| *c.borrow_mut() = cache);
37}
38
39/// An error parsing a source file.
40#[derive(Debug, thiserror::Error)]
41pub enum CodeError {
42    #[error("code: {0}")]
43    Io(#[from] std::io::Error),
44    #[error("code: no grammar for extension {0:?}")]
45    Language(String),
46    #[error("code: parse produced no tree")]
47    Parse,
48}
49
50#[derive(Debug, PartialEq)]
51pub(crate) struct Node {
52    pub(crate) kind: &'static str,
53    pub(crate) field: Option<&'static str>,
54    pub(crate) parent: Option<NodeId>,
55    pub(crate) children: Vec<NodeId>,
56    /// Byte range into the source.
57    pub(crate) start: usize,
58    pub(crate) end: usize,
59    pub(crate) start_line: usize,
60    pub(crate) end_line: usize,
61    /// Field name → child index, for `::field` properties.
62    pub(crate) fields: Vec<(&'static str, usize)>,
63}
64
65/// A parsed source file, exposed as its syntax tree.
66pub struct CodeAdapter {
67    source: String,
68    nodes: Vec<Node>,
69}
70
71/// The grammar for a file extension.
72fn language(ext: &str) -> Option<tree_sitter::Language> {
73    match ext {
74        "rs" => Some(tree_sitter_rust::LANGUAGE.into()),
75        "py" => Some(tree_sitter_python::LANGUAGE.into()),
76        "js" | "mjs" | "cjs" | "jsx" => Some(tree_sitter_javascript::LANGUAGE.into()),
77        "c" | "h" => Some(tree_sitter_c::LANGUAGE.into()),
78        _ => None,
79    }
80}
81
82/// Whether an extension has a grammar (for dispatch and grafting).
83pub fn supported(ext: &str) -> bool {
84    language(ext).is_some()
85}
86
87impl CodeAdapter {
88    /// Parse source text as `ext`'s language. When the thread's AST
89    /// cache is enabled (see [`set_cache`]), a cache hit for this
90    /// exact content skips tree-sitter entirely; a miss parses and
91    /// stores. Caching is transparent — the returned adapter is
92    /// identical either way.
93    pub fn parse(text: &str, ext: &str) -> Result<Self, CodeError> {
94        if let Some(cache) = CACHE.with(|c| c.borrow().clone())
95            && let Some(lang) = language(ext)
96        {
97            let tag = ast_cache::lang_tag(ext);
98            if tag != 0 {
99                let hash = quarb::sha256(text.as_bytes());
100                if let Some(nodes) = ast_cache::load(&cache, tag, &lang, &hash, text) {
101                    return Ok(CodeAdapter {
102                        source: text.to_string(),
103                        nodes,
104                    });
105                }
106                let adapter = Self::parse_raw(text, ext)?;
107                ast_cache::store(&cache, &adapter.nodes, tag, &lang, &hash, text.len() as u64);
108                return Ok(adapter);
109            }
110        }
111        Self::parse_raw(text, ext)
112    }
113
114    /// Parse without consulting the cache — the direct tree-sitter
115    /// path, and the miss path of [`parse`].
116    fn parse_raw(text: &str, ext: &str) -> Result<Self, CodeError> {
117        let lang = language(ext).ok_or_else(|| CodeError::Language(ext.to_string()))?;
118        let mut parser = tree_sitter::Parser::new();
119        parser
120            .set_language(&lang)
121            .map_err(|_| CodeError::Language(ext.to_string()))?;
122        let tree = parser.parse(text, None).ok_or(CodeError::Parse)?;
123        let mut nodes = Vec::new();
124        build(&mut nodes, tree.root_node());
125        Ok(CodeAdapter {
126            source: text.to_string(),
127            nodes,
128        })
129    }
130
131    /// Parse a file, language by extension.
132    pub fn open(path: &std::path::Path) -> Result<Self, CodeError> {
133        let ext = path
134            .extension()
135            .and_then(|e| e.to_str())
136            .unwrap_or("")
137            .to_ascii_lowercase();
138        let text = std::fs::read_to_string(path)?;
139        Self::parse(&text, &ext)
140    }
141
142    /// A human-readable locator: `/kind[start-line]` chain.
143    pub fn locator(&self, node: NodeId) -> String {
144        let mut parts = Vec::new();
145        let mut cur = Some(node);
146        while let Some(n) = cur {
147            let nd = &self.nodes[n.0 as usize];
148            if nd.parent.is_some() {
149                parts.push(format!("{}:{}", nd.kind, nd.start_line));
150            }
151            cur = nd.parent;
152        }
153        parts.reverse();
154        format!("/{}", parts.join("/"))
155    }
156
157    fn text_of(&self, n: &Node) -> &str {
158        &self.source[n.start.min(self.source.len())..n.end.min(self.source.len())]
159    }
160}
161
162/// Intern the named nodes. Uses an explicit stack rather than
163/// recursion so a deeply nested source file (thousands of levels)
164/// can't overflow the call stack. Nodes are interned in the same
165/// pre-order the recursive walk produced.
166fn build(nodes: &mut Vec<Node>, root: tree_sitter::Node<'_>) {
167    // (tree-sitter node, parent id, this node's field in its parent)
168    let mut stack: Vec<(tree_sitter::Node<'_>, Option<NodeId>, Option<&'static str>)> =
169        vec![(root, None, None)];
170    while let Some((ts, parent, field)) = stack.pop() {
171        let id = NodeId(nodes.len() as u64);
172        nodes.push(Node {
173            kind: ts.kind(),
174            field,
175            parent,
176            children: Vec::new(),
177            start: ts.start_byte(),
178            end: ts.end_byte(),
179            start_line: ts.start_position().row + 1,
180            end_line: ts.end_position().row + 1,
181            fields: Vec::new(),
182        });
183        // Record this node with its parent, preserving child order
184        // and the field-name → child-index map.
185        if let Some(p) = parent {
186            let pnode = &mut nodes[p.0 as usize];
187            if let Some(f) = field {
188                pnode.fields.push((f, pnode.children.len()));
189            }
190            pnode.children.push(id);
191        }
192        // Collect named children, then push them reversed so they
193        // pop — and get interned — in source order (a pre-order DFS,
194        // matching the previous recursive numbering).
195        let mut cursor = ts.walk();
196        let mut kids = Vec::new();
197        if cursor.goto_first_child() {
198            loop {
199                let child = cursor.node();
200                if child.is_named() {
201                    kids.push((child, cursor.field_name()));
202                }
203                if !cursor.goto_next_sibling() {
204                    break;
205                }
206            }
207        }
208        for (child, f) in kids.into_iter().rev() {
209            stack.push((child, Some(id), f));
210        }
211    }
212}
213
214impl AstAdapter for CodeAdapter {
215    fn root(&self) -> NodeId {
216        NodeId(0)
217    }
218
219    fn children(&self, node: NodeId) -> Vec<NodeId> {
220        self.nodes[node.0 as usize].children.clone()
221    }
222
223    /// The node kind (`function_item`, `identifier`, ...); the
224    /// root (source_file/module/program) stays unnamed so `/`
225    /// starts at its children.
226    fn name(&self, node: NodeId) -> Option<String> {
227        let n = &self.nodes[node.0 as usize];
228        n.parent.map(|_| n.kind.to_string())
229    }
230
231    fn parent(&self, node: NodeId) -> Option<NodeId> {
232        self.nodes[node.0 as usize].parent
233    }
234
235    /// The kind, as a trait too (`<function_item>` where a trait
236    /// filter reads better than a name step).
237    fn traits(&self, node: NodeId) -> Vec<String> {
238        vec![self.nodes[node.0 as usize].kind.to_string()]
239    }
240
241    /// Tree-sitter fields: `::name` on a function is the name
242    /// child's source text.
243    fn property(&self, node: NodeId, name: &str) -> Option<Value> {
244        let n = &self.nodes[node.0 as usize];
245        let (_, idx) = n.fields.iter().find(|(f, _)| *f == name)?;
246        let child = &self.nodes[n.children[*idx].0 as usize];
247        Some(Value::Str(self.text_of(child).to_string()))
248    }
249
250    /// A node's source text.
251    fn default_value(&self, node: NodeId) -> Option<Value> {
252        Some(Value::Str(
253            self.text_of(&self.nodes[node.0 as usize]).to_string(),
254        ))
255    }
256
257    fn metadata(&self, node: NodeId, key: &str) -> Option<Value> {
258        let n = &self.nodes[node.0 as usize];
259        match key {
260            "kind" => Some(Value::Str(n.kind.to_string())),
261            "field" => n.field.map(|f| Value::Str(f.to_string())),
262            "start-line" => Some(Value::Int(n.start_line as i64)),
263            "end-line" => Some(Value::Int(n.end_line as i64)),
264            "n-children" => Some(Value::Int(n.children.len() as i64)),
265            _ => None,
266        }
267    }
268}