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    // ---- inspection API (the door quarb-code-lsp reads through) ----
308
309    /// The parsed source text.
310    pub fn source(&self) -> &str {
311        &self.source
312    }
313
314    /// The grammar this model was lowered from.
315    pub fn lang(&self) -> Lang {
316        self.lang
317    }
318
319    /// The declared (or adopted) identifier — `None` on anonymous
320    /// constructs and the file root. Distinguishes a function
321    /// named `switch` from the construct: the identifier is a
322    /// stored fact, not a name-string comparison.
323    pub fn ident(&self, node: NodeId) -> Option<&str> {
324        self.nodes[node.0 as usize].name.as_deref()
325    }
326
327    /// The vocabulary word (`""` on the file root).
328    pub fn construct(&self, node: NodeId) -> &str {
329        self.nodes[node.0 as usize].construct
330    }
331
332    /// Byte range into the source.
333    pub fn span(&self, node: NodeId) -> (usize, usize) {
334        self.nodes[node.0 as usize].span
335    }
336
337    /// 1-based start/end lines.
338    pub fn line_span(&self, node: NodeId) -> (usize, usize) {
339        self.nodes[node.0 as usize].lines
340    }
341}
342
343/// The trailing identifier of a callee text — `Type::method`,
344/// `obj.method`, and `path.to.f` all resolve by their last
345/// segment.
346fn trailing_ident(callee: &str) -> Option<&str> {
347    let end = callee.trim_end_matches(['!', '?']);
348    let start = end
349        .char_indices()
350        .rev()
351        .take_while(|(_, c)| c.is_alphanumeric() || *c == '_' || *c == '$')
352        .last()
353        .map(|(i, _)| i)?;
354    Some(&end[start..])
355}
356
357impl AstAdapter for CodeModel {
358    fn root(&self) -> NodeId {
359        NodeId(0)
360    }
361
362    fn children(&self, node: NodeId) -> Vec<NodeId> {
363        self.nodes[node.0 as usize].children.clone()
364    }
365
366    /// The declared identifier, else the construct word; the
367    /// file root stays unnamed.
368    fn name(&self, node: NodeId) -> Option<String> {
369        let n = &self.nodes[node.0 as usize];
370        n.parent?;
371        Some(n.name.clone().unwrap_or_else(|| n.construct.to_string()))
372    }
373
374    fn parent(&self, node: NodeId) -> Option<NodeId> {
375        self.nodes[node.0 as usize].parent
376    }
377
378    fn traits(&self, node: NodeId) -> Vec<String> {
379        self.nodes[node.0 as usize]
380            .traits
381            .iter()
382            .map(|t| t.to_string())
383            .collect()
384    }
385
386    /// The uniform property set: `::signature`, `::doc`,
387    /// `::callee` — the identifier is NOT a property (it is the
388    /// name; `:::name` answers it). Aliased annotation keys fall
389    /// through to `metadata` via [`AstAdapter::aliased_metadata`].
390    fn property(&self, node: NodeId, name: &str) -> Option<Value> {
391        let n = &self.nodes[node.0 as usize];
392        match name {
393            "signature" => n.signature.clone().map(Value::Str),
394            "doc" => n.doc.clone().map(Value::Str),
395            "callee" => n.callee.clone().map(Value::Str),
396            _ => None,
397        }
398    }
399
400    /// A node's source text.
401    fn default_value(&self, node: NodeId) -> Option<Value> {
402        Some(Value::Str(
403            self.text_of(&self.nodes[node.0 as usize]).to_string(),
404        ))
405    }
406
407    fn metadata(&self, node: NodeId, key: &str) -> Option<Value> {
408        let n = &self.nodes[node.0 as usize];
409        match key {
410            // The raw backend kind — the escape hatch, and the
411            // only place backend vocabulary survives.
412            "kind" => (!n.kind.is_empty()).then(|| Value::Str(n.kind.clone())),
413            "construct" => (!n.construct.is_empty()).then(|| Value::Str(n.construct.to_string())),
414            "start-line" => Some(Value::Int(n.lines.0 as i64)),
415            "end-line" => Some(Value::Int(n.lines.1 as i64)),
416            "lang" => Some(Value::Str(self.lang.name().to_string())),
417            "n-children" => Some(Value::Int(n.children.len() as i64)),
418            "n-params" => n.n_params.map(Value::Int),
419            _ => None,
420        }
421    }
422
423    fn aliased_metadata(&self, _node: NodeId) -> &'static [&'static str] {
424        ALIASED
425    }
426
427    /// `->definition`: a call's edges to the declarations its
428    /// callee resolves to (same file, by identifier).
429    fn links(&self, node: NodeId) -> Vec<(String, NodeId)> {
430        self.nodes[node.0 as usize]
431            .links
432            .iter()
433            .map(|&t| ("definition".to_string(), t))
434            .collect()
435    }
436
437    /// `<-definition`: find-references — every call site whose
438    /// callee resolves here.
439    fn backlinks(&self, node: NodeId) -> Vec<(String, NodeId)> {
440        self.nodes[node.0 as usize]
441            .backlinks
442            .iter()
443            .map(|&s| ("definition".to_string(), s))
444            .collect()
445    }
446}