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