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
226                    .entry(name.as_str())
227                    .or_default()
228                    .push(NodeId(i as u64));
229            }
230        }
231        let mut links: Vec<(NodeId, Vec<NodeId>)> = Vec::new();
232        for (i, n) in self.nodes.iter().enumerate() {
233            if let Some(callee) = &n.callee
234                && let Some(ident) = trailing_ident(callee)
235                && let Some(targets) = by_name.get(ident)
236            {
237                links.push((NodeId(i as u64), targets.clone()));
238            }
239        }
240        for (call, targets) in links {
241            for t in &targets {
242                self.nodes[t.0 as usize].backlinks.push(call);
243            }
244            self.nodes[call.0 as usize].links = targets;
245        }
246    }
247
248    /// Read `text` as `ext`'s language at the code level: the
249    /// backend parse (cached when the thread's AST cache is
250    /// enabled — see `quarb_tree_sitter::set_cache`) lowers
251    /// through the grammar's table.
252    pub fn parse(text: &str, ext: &str) -> Result<Self, CodeError> {
253        let ext = ext.to_ascii_lowercase();
254        let lang = lang_for_ext(&ext).ok_or_else(|| CodeError::Language(ext.clone()))?;
255        let ts = quarb_tree_sitter::TreeSitterAdapter::parse(text, &ext)?;
256        let decls = lower::lower(&ts, lang);
257        Ok(Self::build(text.to_string(), lang, decls))
258    }
259
260    /// Read a file at the code level, language by extension.
261    pub fn open(path: &std::path::Path) -> Result<Self, CodeError> {
262        let ext = path
263            .extension()
264            .and_then(|e| e.to_str())
265            .unwrap_or("")
266            .to_ascii_lowercase();
267        let text = std::fs::read_to_string(path)?;
268        Self::parse(&text, &ext)
269    }
270
271    /// A human-readable locator: name-or-construct segments, a
272    /// `[n]` index only among same-label siblings —
273    /// `/lexer/lex/is_name_char`, `/main/for/call[3]`.
274    pub fn locator(&self, node: NodeId) -> String {
275        let mut parts = Vec::new();
276        let mut cur = node;
277        while let Some(parent) = self.nodes[cur.0 as usize].parent {
278            parts.push(self.segment(parent, cur));
279            cur = parent;
280        }
281        parts.reverse();
282        format!("/{}", parts.join("/"))
283    }
284
285    fn label(&self, node: NodeId) -> &str {
286        let n = &self.nodes[node.0 as usize];
287        n.name.as_deref().unwrap_or(n.construct)
288    }
289
290    fn segment(&self, parent: NodeId, child: NodeId) -> String {
291        let label = self.label(child);
292        let same: Vec<NodeId> = self.nodes[parent.0 as usize]
293            .children
294            .iter()
295            .copied()
296            .filter(|&c| self.label(c) == label)
297            .collect();
298        if same.len() > 1 {
299            let pos = same.iter().position(|&c| c == child).unwrap() + 1;
300            format!("{label}[{pos}]")
301        } else {
302            label.to_string()
303        }
304    }
305
306    fn text_of(&self, n: &Node) -> &str {
307        &self.source[n.span.0.min(self.source.len())..n.span.1.min(self.source.len())]
308    }
309
310    // ---- inspection API (the door quarb-code-lsp reads through) ----
311
312    /// The parsed source text.
313    pub fn source(&self) -> &str {
314        &self.source
315    }
316
317    /// The grammar this model was lowered from.
318    pub fn lang(&self) -> Lang {
319        self.lang
320    }
321
322    /// The declared (or adopted) identifier — `None` on anonymous
323    /// constructs and the file root. Distinguishes a function
324    /// named `switch` from the construct: the identifier is a
325    /// stored fact, not a name-string comparison.
326    pub fn ident(&self, node: NodeId) -> Option<&str> {
327        self.nodes[node.0 as usize].name.as_deref()
328    }
329
330    /// The vocabulary word (`""` on the file root).
331    pub fn construct(&self, node: NodeId) -> &str {
332        self.nodes[node.0 as usize].construct
333    }
334
335    /// Byte range into the source.
336    pub fn span(&self, node: NodeId) -> (usize, usize) {
337        self.nodes[node.0 as usize].span
338    }
339
340    /// 1-based start/end lines.
341    pub fn line_span(&self, node: NodeId) -> (usize, usize) {
342        self.nodes[node.0 as usize].lines
343    }
344}
345
346/// The trailing identifier of a callee text — `Type::method`,
347/// `obj.method`, and `path.to.f` all resolve by their last
348/// segment.
349fn trailing_ident(callee: &str) -> Option<&str> {
350    let end = callee.trim_end_matches(['!', '?']);
351    let start = end
352        .char_indices()
353        .rev()
354        .take_while(|(_, c)| c.is_alphanumeric() || *c == '_' || *c == '$')
355        .last()
356        .map(|(i, _)| i)?;
357    Some(&end[start..])
358}
359
360impl AstAdapter for CodeModel {
361    fn root(&self) -> NodeId {
362        NodeId(0)
363    }
364
365    fn children(&self, node: NodeId) -> Vec<NodeId> {
366        self.nodes[node.0 as usize].children.clone()
367    }
368
369    /// The declared identifier, else the construct word; the
370    /// file root stays unnamed.
371    fn name(&self, node: NodeId) -> Option<String> {
372        let n = &self.nodes[node.0 as usize];
373        n.parent?;
374        Some(n.name.clone().unwrap_or_else(|| n.construct.to_string()))
375    }
376
377    fn parent(&self, node: NodeId) -> Option<NodeId> {
378        self.nodes[node.0 as usize].parent
379    }
380
381    fn traits(&self, node: NodeId) -> Vec<String> {
382        self.nodes[node.0 as usize]
383            .traits
384            .iter()
385            .map(|t| t.to_string())
386            .collect()
387    }
388
389    /// The uniform property set: `::signature`, `::doc`,
390    /// `::callee` — the identifier is NOT a property (it is the
391    /// name; `:::name` answers it). Aliased annotation keys fall
392    /// through to `metadata` via [`AstAdapter::aliased_metadata`].
393    fn property(&self, node: NodeId, name: &str) -> Option<Value> {
394        let n = &self.nodes[node.0 as usize];
395        match name {
396            "signature" => n.signature.clone().map(Value::Str),
397            "doc" => n.doc.clone().map(Value::Str),
398            "callee" => n.callee.clone().map(Value::Str),
399            _ => None,
400        }
401    }
402
403    /// A node's source text.
404    fn default_value(&self, node: NodeId) -> Option<Value> {
405        Some(Value::Str(
406            self.text_of(&self.nodes[node.0 as usize]).to_string(),
407        ))
408    }
409
410    fn metadata(&self, node: NodeId, key: &str) -> Option<Value> {
411        let n = &self.nodes[node.0 as usize];
412        match key {
413            // The raw backend kind — the escape hatch, and the
414            // only place backend vocabulary survives.
415            "kind" => (!n.kind.is_empty()).then(|| Value::Str(n.kind.clone())),
416            "construct" => (!n.construct.is_empty()).then(|| Value::Str(n.construct.to_string())),
417            "start-line" => Some(Value::Int(n.lines.0 as i64)),
418            "end-line" => Some(Value::Int(n.lines.1 as i64)),
419            "lang" => Some(Value::Str(self.lang.name().to_string())),
420            "n-children" => Some(Value::Int(n.children.len() as i64)),
421            "n-params" => n.n_params.map(Value::Int),
422            _ => None,
423        }
424    }
425
426    fn aliased_metadata(&self, _node: NodeId) -> &'static [&'static str] {
427        ALIASED
428    }
429
430    /// `->definition`: a call's edges to the declarations its
431    /// callee resolves to (same file, by identifier).
432    fn links(&self, node: NodeId) -> Vec<(String, NodeId)> {
433        self.nodes[node.0 as usize]
434            .links
435            .iter()
436            .map(|&t| ("definition".to_string(), t))
437            .collect()
438    }
439
440    /// `<-definition`: find-references — every call site whose
441    /// callee resolves here.
442    fn backlinks(&self, node: NodeId) -> Vec<(String, NodeId)> {
443        self.nodes[node.0 as usize]
444            .backlinks
445            .iter()
446            .map(|&s| ("definition".to_string(), s))
447            .collect()
448    }
449}