Skip to main content

harn_hostlib/ast/
types.rs

1//! Shared data types for the `ast::*` builtins.
2//!
3//! These mirror the JSON schemas in `crates/harn-hostlib/schemas/ast/` and
4//! are the structural source of truth for the wire format. The
5//! [`to_vm_value`](Symbol::to_vm_value) helpers shape each value into the
6//! `VmValue::Dict` layout the schema declares so handlers in
7//! [`crate::ast`] don't have to rebuild the field set in three places.
8
9#![allow(missing_docs)]
10
11use std::collections::BTreeMap;
12use std::sync::Arc;
13
14use harn_vm::VmValue;
15
16/// Symbol kind. The wire form is the lowercase string returned by
17/// [`SymbolKind::as_str`].
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub enum SymbolKind {
20    Function,
21    Method,
22    Class,
23    Struct,
24    Enum,
25    Interface,
26    Protocol,
27    Type,
28    Variable,
29    Module,
30    Other,
31}
32
33impl SymbolKind {
34    /// Wire form ("function", "class", ...).
35    pub fn as_str(self) -> &'static str {
36        match self {
37            SymbolKind::Function => "function",
38            SymbolKind::Method => "method",
39            SymbolKind::Class => "class",
40            SymbolKind::Struct => "struct",
41            SymbolKind::Enum => "enum",
42            SymbolKind::Interface => "interface",
43            SymbolKind::Protocol => "protocol",
44            SymbolKind::Type => "type",
45            SymbolKind::Variable => "variable",
46            SymbolKind::Module => "module",
47            SymbolKind::Other => "other",
48        }
49    }
50
51    /// True for kinds that contain other symbols (classes, enums, …).
52    /// Drives the flat-symbols → nested-outline fold in
53    /// [`crate::ast::outline`].
54    pub fn is_container(self) -> bool {
55        matches!(
56            self,
57            SymbolKind::Class
58                | SymbolKind::Struct
59                | SymbolKind::Enum
60                | SymbolKind::Interface
61                | SymbolKind::Protocol
62                | SymbolKind::Module
63        )
64    }
65}
66
67/// A flat symbol record. All row/col coordinates are 0-based, matching
68/// tree-sitter native positions.
69#[derive(Debug, Clone)]
70pub struct Symbol {
71    pub name: String,
72    pub kind: SymbolKind,
73    pub container: Option<String>,
74    pub signature: String,
75    pub start_row: u32,
76    pub start_col: u32,
77    pub end_row: u32,
78    pub end_col: u32,
79}
80
81impl Symbol {
82    /// Render as a `VmValue::Dict` matching `schemas/ast/symbols.response.json`.
83    pub fn to_vm_value(&self) -> VmValue {
84        let mut dict: BTreeMap<String, VmValue> = BTreeMap::new();
85        dict.insert(
86            "name".into(),
87            VmValue::String(Arc::from(self.name.as_str())),
88        );
89        dict.insert(
90            "kind".into(),
91            VmValue::String(Arc::from(self.kind.as_str())),
92        );
93        dict.insert(
94            "container".into(),
95            match &self.container {
96                Some(s) => VmValue::String(Arc::from(s.as_str())),
97                None => VmValue::Nil,
98            },
99        );
100        dict.insert(
101            "signature".into(),
102            VmValue::String(Arc::from(self.signature.as_str())),
103        );
104        dict.insert("start_row".into(), VmValue::Int(self.start_row as i64));
105        dict.insert("start_col".into(), VmValue::Int(self.start_col as i64));
106        dict.insert("end_row".into(), VmValue::Int(self.end_row as i64));
107        dict.insert("end_col".into(), VmValue::Int(self.end_col as i64));
108        VmValue::Dict(Arc::new(dict))
109    }
110}
111
112/// One node in a hierarchical outline. The `children` list nests in
113/// document order; see [`crate::ast::outline`] for the fold algorithm.
114#[derive(Debug, Clone)]
115pub struct OutlineItem {
116    pub name: String,
117    pub kind: SymbolKind,
118    pub signature: String,
119    pub start_row: u32,
120    pub end_row: u32,
121    pub children: Vec<OutlineItem>,
122}
123
124impl OutlineItem {
125    pub fn to_vm_value(&self) -> VmValue {
126        let mut dict: BTreeMap<String, VmValue> = BTreeMap::new();
127        dict.insert(
128            "name".into(),
129            VmValue::String(Arc::from(self.name.as_str())),
130        );
131        dict.insert(
132            "kind".into(),
133            VmValue::String(Arc::from(self.kind.as_str())),
134        );
135        dict.insert(
136            "signature".into(),
137            VmValue::String(Arc::from(self.signature.as_str())),
138        );
139        dict.insert("start_row".into(), VmValue::Int(self.start_row as i64));
140        dict.insert("end_row".into(), VmValue::Int(self.end_row as i64));
141        let kids: Vec<VmValue> = self.children.iter().map(OutlineItem::to_vm_value).collect();
142        dict.insert("children".into(), VmValue::List(Arc::new(kids)));
143        VmValue::Dict(Arc::new(dict))
144    }
145}
146
147/// One tree-sitter node, flattened for `parse_file`'s wire format.
148/// Matches `schemas/ast/parse_file.response.json#/$defs/Node`.
149#[derive(Debug, Clone)]
150pub struct ParsedNode {
151    pub id: u32,
152    pub parent_id: Option<u32>,
153    pub kind: String,
154    pub is_named: bool,
155    pub start_byte: u32,
156    pub end_byte: u32,
157    pub start_row: u32,
158    pub start_col: u32,
159    pub end_row: u32,
160    pub end_col: u32,
161}
162
163impl ParsedNode {
164    pub fn to_vm_value(&self) -> VmValue {
165        let mut dict: BTreeMap<String, VmValue> = BTreeMap::new();
166        dict.insert("id".into(), VmValue::Int(self.id as i64));
167        dict.insert(
168            "parent_id".into(),
169            self.parent_id
170                .map_or(VmValue::Nil, |id| VmValue::Int(id as i64)),
171        );
172        dict.insert(
173            "kind".into(),
174            VmValue::String(Arc::from(self.kind.as_str())),
175        );
176        dict.insert("is_named".into(), VmValue::Bool(self.is_named));
177        dict.insert("start_byte".into(), VmValue::Int(self.start_byte as i64));
178        dict.insert("end_byte".into(), VmValue::Int(self.end_byte as i64));
179        dict.insert("start_row".into(), VmValue::Int(self.start_row as i64));
180        dict.insert("start_col".into(), VmValue::Int(self.start_col as i64));
181        dict.insert("end_row".into(), VmValue::Int(self.end_row as i64));
182        dict.insert("end_col".into(), VmValue::Int(self.end_col as i64));
183        VmValue::Dict(Arc::new(dict))
184    }
185}
186
187/// One ERROR / MISSING node from a tree-sitter parse. All row/column
188/// coordinates are 0-based, matching tree-sitter's native Point.
189///
190/// `message` is a short human-readable description (e.g. `"unexpected
191/// '+'"`, `"missing ')'"`). `snippet` is the raw source text covered by
192/// the node, truncated to 60 chars and with newlines escaped — kept
193/// separately so callers can render the message without re-parsing it.
194#[derive(Debug, Clone)]
195pub struct ParseError {
196    pub start_row: u32,
197    pub start_col: u32,
198    pub end_row: u32,
199    pub end_col: u32,
200    pub start_byte: u32,
201    pub end_byte: u32,
202    pub message: String,
203    pub snippet: String,
204    pub missing: bool,
205}
206
207impl ParseError {
208    pub fn to_vm_value(&self) -> VmValue {
209        let mut dict: BTreeMap<String, VmValue> = BTreeMap::new();
210        dict.insert("start_row".into(), VmValue::Int(self.start_row as i64));
211        dict.insert("start_col".into(), VmValue::Int(self.start_col as i64));
212        dict.insert("end_row".into(), VmValue::Int(self.end_row as i64));
213        dict.insert("end_col".into(), VmValue::Int(self.end_col as i64));
214        dict.insert("start_byte".into(), VmValue::Int(self.start_byte as i64));
215        dict.insert("end_byte".into(), VmValue::Int(self.end_byte as i64));
216        dict.insert(
217            "message".into(),
218            VmValue::String(Arc::from(self.message.as_str())),
219        );
220        dict.insert(
221            "snippet".into(),
222            VmValue::String(Arc::from(self.snippet.as_str())),
223        );
224        dict.insert("missing".into(), VmValue::Bool(self.missing));
225        VmValue::Dict(Arc::new(dict))
226    }
227}
228
229/// Reference to an identifier that wasn't defined within the current
230/// file. Coordinates are 0-based. `kind` is `"identifier"` for value-side
231/// references and `"type"` for type-only references (TypeScript only).
232#[derive(Debug, Clone)]
233pub struct UndefinedName {
234    pub name: String,
235    pub kind: &'static str,
236    pub row: u32,
237    pub column: u32,
238}
239
240impl UndefinedName {
241    pub fn to_vm_value(&self) -> VmValue {
242        let mut dict: BTreeMap<String, VmValue> = BTreeMap::new();
243        dict.insert(
244            "name".into(),
245            VmValue::String(Arc::from(self.name.as_str())),
246        );
247        dict.insert("kind".into(), VmValue::String(Arc::from(self.kind)));
248        dict.insert("row".into(), VmValue::Int(self.row as i64));
249        dict.insert("column".into(), VmValue::Int(self.column as i64));
250        let message = format!("undefined name '{}'", self.name);
251        dict.insert(
252            "message".into(),
253            VmValue::String(Arc::from(message.as_str())),
254        );
255        VmValue::Dict(Arc::new(dict))
256    }
257}