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::rc::Rc;
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("name".into(), VmValue::String(Rc::from(self.name.as_str())));
86        dict.insert("kind".into(), VmValue::String(Rc::from(self.kind.as_str())));
87        dict.insert(
88            "container".into(),
89            match &self.container {
90                Some(s) => VmValue::String(Rc::from(s.as_str())),
91                None => VmValue::Nil,
92            },
93        );
94        dict.insert(
95            "signature".into(),
96            VmValue::String(Rc::from(self.signature.as_str())),
97        );
98        dict.insert("start_row".into(), VmValue::Int(self.start_row as i64));
99        dict.insert("start_col".into(), VmValue::Int(self.start_col as i64));
100        dict.insert("end_row".into(), VmValue::Int(self.end_row as i64));
101        dict.insert("end_col".into(), VmValue::Int(self.end_col as i64));
102        VmValue::Dict(Rc::new(dict))
103    }
104}
105
106/// One node in a hierarchical outline. The `children` list nests in
107/// document order; see [`crate::ast::outline`] for the fold algorithm.
108#[derive(Debug, Clone)]
109pub struct OutlineItem {
110    pub name: String,
111    pub kind: SymbolKind,
112    pub signature: String,
113    pub start_row: u32,
114    pub end_row: u32,
115    pub children: Vec<OutlineItem>,
116}
117
118impl OutlineItem {
119    pub fn to_vm_value(&self) -> VmValue {
120        let mut dict: BTreeMap<String, VmValue> = BTreeMap::new();
121        dict.insert("name".into(), VmValue::String(Rc::from(self.name.as_str())));
122        dict.insert("kind".into(), VmValue::String(Rc::from(self.kind.as_str())));
123        dict.insert(
124            "signature".into(),
125            VmValue::String(Rc::from(self.signature.as_str())),
126        );
127        dict.insert("start_row".into(), VmValue::Int(self.start_row as i64));
128        dict.insert("end_row".into(), VmValue::Int(self.end_row as i64));
129        let kids: Vec<VmValue> = self.children.iter().map(OutlineItem::to_vm_value).collect();
130        dict.insert("children".into(), VmValue::List(Rc::new(kids)));
131        VmValue::Dict(Rc::new(dict))
132    }
133}
134
135/// One tree-sitter node, flattened for `parse_file`'s wire format.
136/// Matches `schemas/ast/parse_file.response.json#/$defs/Node`.
137#[derive(Debug, Clone)]
138pub struct ParsedNode {
139    pub id: u32,
140    pub parent_id: Option<u32>,
141    pub kind: String,
142    pub is_named: bool,
143    pub start_byte: u32,
144    pub end_byte: u32,
145    pub start_row: u32,
146    pub start_col: u32,
147    pub end_row: u32,
148    pub end_col: u32,
149}
150
151impl ParsedNode {
152    pub fn to_vm_value(&self) -> VmValue {
153        let mut dict: BTreeMap<String, VmValue> = BTreeMap::new();
154        dict.insert("id".into(), VmValue::Int(self.id as i64));
155        dict.insert(
156            "parent_id".into(),
157            self.parent_id
158                .map_or(VmValue::Nil, |id| VmValue::Int(id as i64)),
159        );
160        dict.insert("kind".into(), VmValue::String(Rc::from(self.kind.as_str())));
161        dict.insert("is_named".into(), VmValue::Bool(self.is_named));
162        dict.insert("start_byte".into(), VmValue::Int(self.start_byte as i64));
163        dict.insert("end_byte".into(), VmValue::Int(self.end_byte as i64));
164        dict.insert("start_row".into(), VmValue::Int(self.start_row as i64));
165        dict.insert("start_col".into(), VmValue::Int(self.start_col as i64));
166        dict.insert("end_row".into(), VmValue::Int(self.end_row as i64));
167        dict.insert("end_col".into(), VmValue::Int(self.end_col as i64));
168        VmValue::Dict(Rc::new(dict))
169    }
170}
171
172/// One ERROR / MISSING node from a tree-sitter parse. All row/column
173/// coordinates are 0-based, matching tree-sitter's native Point.
174///
175/// `message` is a short human-readable description (e.g. `"unexpected
176/// '+'"`, `"missing ')'"`). `snippet` is the raw source text covered by
177/// the node, truncated to 60 chars and with newlines escaped — kept
178/// separately so callers can render the message without re-parsing it.
179#[derive(Debug, Clone)]
180pub struct ParseError {
181    pub start_row: u32,
182    pub start_col: u32,
183    pub end_row: u32,
184    pub end_col: u32,
185    pub start_byte: u32,
186    pub end_byte: u32,
187    pub message: String,
188    pub snippet: String,
189    pub missing: bool,
190}
191
192impl ParseError {
193    pub fn to_vm_value(&self) -> VmValue {
194        let mut dict: BTreeMap<String, VmValue> = BTreeMap::new();
195        dict.insert("start_row".into(), VmValue::Int(self.start_row as i64));
196        dict.insert("start_col".into(), VmValue::Int(self.start_col as i64));
197        dict.insert("end_row".into(), VmValue::Int(self.end_row as i64));
198        dict.insert("end_col".into(), VmValue::Int(self.end_col as i64));
199        dict.insert("start_byte".into(), VmValue::Int(self.start_byte as i64));
200        dict.insert("end_byte".into(), VmValue::Int(self.end_byte as i64));
201        dict.insert(
202            "message".into(),
203            VmValue::String(Rc::from(self.message.as_str())),
204        );
205        dict.insert(
206            "snippet".into(),
207            VmValue::String(Rc::from(self.snippet.as_str())),
208        );
209        dict.insert("missing".into(), VmValue::Bool(self.missing));
210        VmValue::Dict(Rc::new(dict))
211    }
212}
213
214/// Reference to an identifier that wasn't defined within the current
215/// file. Coordinates are 0-based. `kind` is `"identifier"` for value-side
216/// references and `"type"` for type-only references (TypeScript only).
217#[derive(Debug, Clone)]
218pub struct UndefinedName {
219    pub name: String,
220    pub kind: &'static str,
221    pub row: u32,
222    pub column: u32,
223}
224
225impl UndefinedName {
226    pub fn to_vm_value(&self) -> VmValue {
227        let mut dict: BTreeMap<String, VmValue> = BTreeMap::new();
228        dict.insert("name".into(), VmValue::String(Rc::from(self.name.as_str())));
229        dict.insert("kind".into(), VmValue::String(Rc::from(self.kind)));
230        dict.insert("row".into(), VmValue::Int(self.row as i64));
231        dict.insert("column".into(), VmValue::Int(self.column as i64));
232        let message = format!("undefined name '{}'", self.name);
233        dict.insert(
234            "message".into(),
235            VmValue::String(Rc::from(message.as_str())),
236        );
237        VmValue::Dict(Rc::new(dict))
238    }
239}