Skip to main content

quarb_code/
lib.rs

1//! The code level for the Quarb query engine.
2//!
3//! Cross-language code navigation above the syntax level
4//! (`quarb-tree-sitter`): **function names are node names, not
5//! properties**. `/lexer/lex/is_name_char` descends module,
6//! function, nested function — a filepath into the program —
7//! where the syntax level spells the same question
8//! `//function_item[::name = "lex"]`.
9//!
10//! - **Names.** A declaration's edge name is its declared
11//!   identifier; every other construct in the vocabulary is named
12//!   by its normalized keyword (`if`, `switch`, `for`, `call`);
13//!   everything else dissolves — children hoist, as the text
14//!   level dissolves markup soup. A nameless function-valued
15//!   expression adopts the identifier of the binding receiving
16//!   it (`const lex = () => {}` is a function named `lex`).
17//! - **Traits** classify: `<function>`, `<type>`, `<module>`,
18//!   `<loop>`, `<conditional>`, `<call>`, `<import>`.
19//! - **Properties** are uniform: `::signature` (the declaration
20//!   head), `::doc` (attached documentation), `::callee` (on
21//!   calls); bare `::` is the node's source text.
22//! - **Annotations**: `::::kind` (the raw backend kind — the only
23//!   place tree-sitter vocabulary survives), `::::construct`,
24//!   `::::start-line` / `::::end-line`, `::::lang`,
25//!   `::::n-children`, `::::n-params` — every one aliased to
26//!   `::` (the surface is closed; ruling #29).
27//! - **Crosslinks**: every `call` carries `->definition` edges to
28//!   the same-file declarations matching its callee;
29//!   `//lex<-definition` is find-references.
30//!
31//! The vocabulary and the per-grammar lowering tables are ruled
32//! in the spec (The Code Level, ruling #31) and doubled as
33//! conformance fixtures in this crate's tests. Grammars: Rust,
34//! Python, JavaScript, C — the syntax level's set, each nailed.
35
36use quarb::{AstAdapter, NodeId, Value};
37
38mod lower;
39
40/// A grammar of the code level's set.
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum Lang {
43    Rust,
44    Python,
45    Javascript,
46    C,
47}
48
49impl Lang {
50    /// The `::::lang` spelling.
51    pub fn name(self) -> &'static str {
52        match self {
53            Lang::Rust => "rust",
54            Lang::Python => "python",
55            Lang::Javascript => "javascript",
56            Lang::C => "c",
57        }
58    }
59}
60
61/// The grammar for a file extension (lowercased).
62pub fn lang_for_ext(ext: &str) -> Option<Lang> {
63    match ext {
64        "rs" => Some(Lang::Rust),
65        "py" => Some(Lang::Python),
66        "js" | "mjs" | "cjs" | "jsx" => Some(Lang::Javascript),
67        "c" | "h" => Some(Lang::C),
68        _ => None,
69    }
70}
71
72/// Whether an extension has a code-level lowering (for dispatch
73/// and grafting). Agrees with `quarb_tree_sitter::supported`.
74pub fn supported(ext: &str) -> bool {
75    lang_for_ext(ext).is_some()
76}
77
78/// An error reading a source file at the code level.
79#[derive(Debug, thiserror::Error)]
80pub enum CodeError {
81    #[error("code: {0}")]
82    Io(#[from] std::io::Error),
83    #[error("code: no code-level support for extension {0:?} (rs, py, js, mjs, cjs, jsx, c, h)")]
84    Language(String),
85    #[error(transparent)]
86    Backend(#[from] quarb_tree_sitter::TreeSitterError),
87}
88
89/// One lowered construct — the code level's producer seam, the
90/// parallel of `quarb_text::Block`. A producer emits `Decl`s in
91/// pre-order (a parent precedes its children);
92/// [`CodeModel::build`] derives the arbor. The tree-sitter
93/// producer lives in this crate; another backend supplies the
94/// same stream and nothing above it moves.
95#[derive(Debug)]
96pub struct Decl {
97    /// Index of the parent `Decl`, or `None` for a top-level one.
98    pub parent: Option<usize>,
99    /// The vocabulary word: `function`, `type`, `if`, `call`, …
100    pub construct: &'static str,
101    /// The declared (or adopted) identifier, where one exists.
102    pub name: Option<String>,
103    /// Curated trait set — never backend kinds.
104    pub traits: &'static [&'static str],
105    /// The raw backend kind; surfaces only as `::::kind`.
106    pub kind: String,
107    /// Byte range into the source.
108    pub span: (usize, usize),
109    /// 1-based start/end lines.
110    pub lines: (usize, usize),
111    /// The declaration head, whitespace-collapsed (`::signature`).
112    pub signature: Option<String>,
113    /// Attached documentation, markers stripped (`::doc`).
114    pub doc: Option<String>,
115    /// A call's callee text (`::callee`).
116    pub callee: Option<String>,
117    /// Declared parameter count, functions only (`::::n-params`).
118    pub n_params: Option<i64>,
119}
120
121struct Node {
122    parent: Option<NodeId>,
123    children: Vec<NodeId>,
124    construct: &'static str,
125    name: Option<String>,
126    traits: &'static [&'static str],
127    kind: String,
128    span: (usize, usize),
129    lines: (usize, usize),
130    signature: Option<String>,
131    doc: Option<String>,
132    callee: Option<String>,
133    n_params: Option<i64>,
134    /// `->definition` targets (calls only).
135    links: Vec<NodeId>,
136    /// `<-definition` sources (declarations only).
137    backlinks: Vec<NodeId>,
138}
139
140/// A source file read at the code level.
141pub struct CodeModel {
142    source: String,
143    lang: Lang,
144    nodes: Vec<Node>,
145}
146
147/// Every annotation key answers at `::` too: the property
148/// surface is closed (a source file cannot mint a property —
149/// identifiers become names), so ruling #29 applies in full.
150/// Four colons stay the portable spelling.
151const ALIASED: &[&str] = &[
152    "kind",
153    "construct",
154    "start-line",
155    "end-line",
156    "lang",
157    "n-children",
158    "n-params",
159];
160
161impl CodeModel {
162    /// Derive the arbor from a producer's `Decl` stream — the
163    /// seam. `decls` must be pre-order: a parent precedes its
164    /// children.
165    pub fn build(source: String, lang: Lang, decls: Vec<Decl>) -> Self {
166        let mut nodes = Vec::with_capacity(decls.len() + 1);
167        // nodes[0]: the unnamed file root.
168        nodes.push(Node {
169            parent: None,
170            children: Vec::new(),
171            construct: "",
172            name: None,
173            traits: &[],
174            kind: String::new(),
175            span: (0, source.len()),
176            lines: (1, source.lines().count().max(1)),
177            signature: None,
178            doc: None,
179            callee: None,
180            n_params: None,
181            links: Vec::new(),
182            backlinks: Vec::new(),
183        });
184        for d in decls {
185            let id = NodeId(nodes.len() as u64);
186            let parent = NodeId(d.parent.map_or(0, |p| p as u64 + 1));
187            nodes.push(Node {
188                parent: Some(parent),
189                children: Vec::new(),
190                construct: d.construct,
191                name: d.name,
192                traits: d.traits,
193                kind: d.kind,
194                span: d.span,
195                lines: d.lines,
196                signature: d.signature,
197                doc: d.doc,
198                callee: d.callee,
199                n_params: d.n_params,
200                links: Vec::new(),
201                backlinks: Vec::new(),
202            });
203            nodes[parent.0 as usize].children.push(id);
204        }
205        let mut model = CodeModel {
206            source,
207            lang,
208            nodes,
209        };
210        model.link_definitions();
211        model
212    }
213
214    /// Resolve every call's callee against the file's named
215    /// function and type declarations — `->definition`, by
216    /// identifier. Unresolved callees carry no edge; an ambiguous
217    /// identifier fans out to every match.
218    fn link_definitions(&mut self) {
219        let mut by_name: std::collections::HashMap<&str, Vec<NodeId>> =
220            std::collections::HashMap::new();
221        for (i, n) in self.nodes.iter().enumerate() {
222            if matches!(n.construct, "function" | "type")
223                && let Some(name) = &n.name
224            {
225                by_name.entry(name.as_str()).or_default().push(NodeId(i as u64));
226            }
227        }
228        let mut links: Vec<(NodeId, Vec<NodeId>)> = Vec::new();
229        for (i, n) in self.nodes.iter().enumerate() {
230            if let Some(callee) = &n.callee
231                && let Some(ident) = trailing_ident(callee)
232                && let Some(targets) = by_name.get(ident)
233            {
234                links.push((NodeId(i as u64), targets.clone()));
235            }
236        }
237        for (call, targets) in links {
238            for t in &targets {
239                self.nodes[t.0 as usize].backlinks.push(call);
240            }
241            self.nodes[call.0 as usize].links = targets;
242        }
243    }
244
245    /// Read `text` as `ext`'s language at the code level: the
246    /// backend parse (cached when the thread's AST cache is
247    /// enabled — see `quarb_tree_sitter::set_cache`) lowers
248    /// through the grammar's table.
249    pub fn parse(text: &str, ext: &str) -> Result<Self, CodeError> {
250        let ext = ext.to_ascii_lowercase();
251        let lang = lang_for_ext(&ext).ok_or_else(|| CodeError::Language(ext.clone()))?;
252        let ts = quarb_tree_sitter::TreeSitterAdapter::parse(text, &ext)?;
253        let decls = lower::lower(&ts, lang);
254        Ok(Self::build(text.to_string(), lang, decls))
255    }
256
257    /// Read a file at the code level, language by extension.
258    pub fn open(path: &std::path::Path) -> Result<Self, CodeError> {
259        let ext = path
260            .extension()
261            .and_then(|e| e.to_str())
262            .unwrap_or("")
263            .to_ascii_lowercase();
264        let text = std::fs::read_to_string(path)?;
265        Self::parse(&text, &ext)
266    }
267
268    /// A human-readable locator: name-or-construct segments, a
269    /// `[n]` index only among same-label siblings —
270    /// `/lexer/lex/is_name_char`, `/main/for/call[3]`.
271    pub fn locator(&self, node: NodeId) -> String {
272        let mut parts = Vec::new();
273        let mut cur = node;
274        while let Some(parent) = self.nodes[cur.0 as usize].parent {
275            parts.push(self.segment(parent, cur));
276            cur = parent;
277        }
278        parts.reverse();
279        format!("/{}", parts.join("/"))
280    }
281
282    fn label(&self, node: NodeId) -> &str {
283        let n = &self.nodes[node.0 as usize];
284        n.name.as_deref().unwrap_or(n.construct)
285    }
286
287    fn segment(&self, parent: NodeId, child: NodeId) -> String {
288        let label = self.label(child);
289        let same: Vec<NodeId> = self.nodes[parent.0 as usize]
290            .children
291            .iter()
292            .copied()
293            .filter(|&c| self.label(c) == label)
294            .collect();
295        if same.len() > 1 {
296            let pos = same.iter().position(|&c| c == child).unwrap() + 1;
297            format!("{label}[{pos}]")
298        } else {
299            label.to_string()
300        }
301    }
302
303    fn text_of(&self, n: &Node) -> &str {
304        &self.source[n.span.0.min(self.source.len())..n.span.1.min(self.source.len())]
305    }
306}
307
308/// The trailing identifier of a callee text — `Type::method`,
309/// `obj.method`, and `path.to.f` all resolve by their last
310/// segment.
311fn trailing_ident(callee: &str) -> Option<&str> {
312    let end = callee.trim_end_matches(['!', '?']);
313    let start = end
314        .char_indices()
315        .rev()
316        .take_while(|(_, c)| c.is_alphanumeric() || *c == '_' || *c == '$')
317        .last()
318        .map(|(i, _)| i)?;
319    Some(&end[start..])
320}
321
322impl AstAdapter for CodeModel {
323    fn root(&self) -> NodeId {
324        NodeId(0)
325    }
326
327    fn children(&self, node: NodeId) -> Vec<NodeId> {
328        self.nodes[node.0 as usize].children.clone()
329    }
330
331    /// The declared identifier, else the construct word; the
332    /// file root stays unnamed.
333    fn name(&self, node: NodeId) -> Option<String> {
334        let n = &self.nodes[node.0 as usize];
335        n.parent?;
336        Some(n.name.clone().unwrap_or_else(|| n.construct.to_string()))
337    }
338
339    fn parent(&self, node: NodeId) -> Option<NodeId> {
340        self.nodes[node.0 as usize].parent
341    }
342
343    fn traits(&self, node: NodeId) -> Vec<String> {
344        self.nodes[node.0 as usize]
345            .traits
346            .iter()
347            .map(|t| t.to_string())
348            .collect()
349    }
350
351    /// The uniform property set: `::signature`, `::doc`,
352    /// `::callee` — the identifier is NOT a property (it is the
353    /// name; `:::name` answers it). Aliased annotation keys fall
354    /// through to `metadata` via [`AstAdapter::aliased_metadata`].
355    fn property(&self, node: NodeId, name: &str) -> Option<Value> {
356        let n = &self.nodes[node.0 as usize];
357        match name {
358            "signature" => n.signature.clone().map(Value::Str),
359            "doc" => n.doc.clone().map(Value::Str),
360            "callee" => n.callee.clone().map(Value::Str),
361            _ => None,
362        }
363    }
364
365    /// A node's source text.
366    fn default_value(&self, node: NodeId) -> Option<Value> {
367        Some(Value::Str(
368            self.text_of(&self.nodes[node.0 as usize]).to_string(),
369        ))
370    }
371
372    fn metadata(&self, node: NodeId, key: &str) -> Option<Value> {
373        let n = &self.nodes[node.0 as usize];
374        match key {
375            // The raw backend kind — the escape hatch, and the
376            // only place backend vocabulary survives.
377            "kind" => (!n.kind.is_empty()).then(|| Value::Str(n.kind.clone())),
378            "construct" => (!n.construct.is_empty()).then(|| Value::Str(n.construct.to_string())),
379            "start-line" => Some(Value::Int(n.lines.0 as i64)),
380            "end-line" => Some(Value::Int(n.lines.1 as i64)),
381            "lang" => Some(Value::Str(self.lang.name().to_string())),
382            "n-children" => Some(Value::Int(n.children.len() as i64)),
383            "n-params" => n.n_params.map(Value::Int),
384            _ => None,
385        }
386    }
387
388    fn aliased_metadata(&self, _node: NodeId) -> &'static [&'static str] {
389        ALIASED
390    }
391
392    /// `->definition`: a call's edges to the declarations its
393    /// callee resolves to (same file, by identifier).
394    fn links(&self, node: NodeId) -> Vec<(String, NodeId)> {
395        self.nodes[node.0 as usize]
396            .links
397            .iter()
398            .map(|&t| ("definition".to_string(), t))
399            .collect()
400    }
401
402    /// `<-definition`: find-references — every call site whose
403    /// callee resolves here.
404    fn backlinks(&self, node: NodeId) -> Vec<(String, NodeId)> {
405        self.nodes[node.0 as usize]
406            .backlinks
407            .iter()
408            .map(|&s| ("definition".to_string(), s))
409            .collect()
410    }
411}