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::sync::Arc;
12
13use harn_vm::VmValue;
14
15/// Symbol kind. The wire form is the lowercase string returned by
16/// [`SymbolKind::as_str`].
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18pub enum SymbolKind {
19    Function,
20    Method,
21    Class,
22    Struct,
23    Enum,
24    EnumCase,
25    Field,
26    Interface,
27    Protocol,
28    Type,
29    Variable,
30    Module,
31    Other,
32}
33
34impl SymbolKind {
35    /// Wire form ("function", "class", ...).
36    pub fn as_str(self) -> &'static str {
37        match self {
38            SymbolKind::Function => "function",
39            SymbolKind::Method => "method",
40            SymbolKind::Class => "class",
41            SymbolKind::Struct => "struct",
42            SymbolKind::Enum => "enum",
43            SymbolKind::EnumCase => "enum_case",
44            SymbolKind::Field => "field",
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 access_level: Option<String>,
78    pub signature: String,
79    pub start_row: u32,
80    pub start_col: u32,
81    pub end_row: u32,
82    pub end_col: u32,
83}
84
85impl Symbol {
86    /// Render as a `VmValue::Dict` matching `schemas/ast/symbols.response.json`.
87    pub fn to_vm_value(&self) -> VmValue {
88        let mut dict: harn_vm::value::DictMap = harn_vm::value::DictMap::new();
89        dict.insert(
90            "name".into(),
91            VmValue::String(arcstr::ArcStr::from(self.name.as_str())),
92        );
93        dict.insert(
94            "kind".into(),
95            VmValue::String(arcstr::ArcStr::from(self.kind.as_str())),
96        );
97        dict.insert(
98            "container".into(),
99            match &self.container {
100                Some(s) => VmValue::String(arcstr::ArcStr::from(s.as_str())),
101                None => VmValue::Nil,
102            },
103        );
104        dict.insert(
105            "access_level".into(),
106            match &self.access_level {
107                Some(s) => VmValue::String(arcstr::ArcStr::from(s.as_str())),
108                None => VmValue::Nil,
109            },
110        );
111        dict.insert(
112            "signature".into(),
113            VmValue::String(arcstr::ArcStr::from(self.signature.as_str())),
114        );
115        dict.insert("start_row".into(), VmValue::Int(self.start_row as i64));
116        dict.insert("start_col".into(), VmValue::Int(self.start_col as i64));
117        dict.insert("end_row".into(), VmValue::Int(self.end_row as i64));
118        dict.insert("end_col".into(), VmValue::Int(self.end_col as i64));
119        VmValue::dict(dict)
120    }
121}
122
123/// One node in a hierarchical outline. The `children` list nests in
124/// document order; see [`crate::ast::outline`] for the fold algorithm.
125#[derive(Debug, Clone)]
126pub struct OutlineItem {
127    pub name: String,
128    pub kind: SymbolKind,
129    pub access_level: Option<String>,
130    pub signature: String,
131    pub start_row: u32,
132    pub end_row: u32,
133    pub children: Vec<OutlineItem>,
134}
135
136impl OutlineItem {
137    pub fn to_vm_value(&self) -> VmValue {
138        let mut dict: harn_vm::value::DictMap = harn_vm::value::DictMap::new();
139        dict.insert(
140            "name".into(),
141            VmValue::String(arcstr::ArcStr::from(self.name.as_str())),
142        );
143        dict.insert(
144            "kind".into(),
145            VmValue::String(arcstr::ArcStr::from(self.kind.as_str())),
146        );
147        dict.insert(
148            "access_level".into(),
149            match &self.access_level {
150                Some(s) => VmValue::String(arcstr::ArcStr::from(s.as_str())),
151                None => VmValue::Nil,
152            },
153        );
154        dict.insert(
155            "signature".into(),
156            VmValue::String(arcstr::ArcStr::from(self.signature.as_str())),
157        );
158        dict.insert("start_row".into(), VmValue::Int(self.start_row as i64));
159        dict.insert("end_row".into(), VmValue::Int(self.end_row as i64));
160        let kids: Vec<VmValue> = self.children.iter().map(OutlineItem::to_vm_value).collect();
161        dict.insert("children".into(), VmValue::List(Arc::new(kids)));
162        VmValue::dict(dict)
163    }
164}
165
166/// One tree-sitter node, flattened for `parse_file`'s wire format.
167/// Matches `schemas/ast/parse_file.response.json#/$defs/Node`.
168#[derive(Debug, Clone)]
169pub struct ParsedNode {
170    pub id: u32,
171    pub parent_id: Option<u32>,
172    pub kind: String,
173    pub is_named: bool,
174    pub start_byte: u32,
175    pub end_byte: u32,
176    pub start_row: u32,
177    pub start_col: u32,
178    pub end_row: u32,
179    pub end_col: u32,
180}
181
182impl ParsedNode {
183    pub fn to_vm_value(&self) -> VmValue {
184        let mut dict: harn_vm::value::DictMap = harn_vm::value::DictMap::new();
185        dict.insert("id".into(), VmValue::Int(self.id as i64));
186        dict.insert(
187            "parent_id".into(),
188            self.parent_id
189                .map_or(VmValue::Nil, |id| VmValue::Int(id as i64)),
190        );
191        dict.insert(
192            "kind".into(),
193            VmValue::String(arcstr::ArcStr::from(self.kind.as_str())),
194        );
195        dict.insert("is_named".into(), VmValue::Bool(self.is_named));
196        dict.insert("start_byte".into(), VmValue::Int(self.start_byte as i64));
197        dict.insert("end_byte".into(), VmValue::Int(self.end_byte as i64));
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        VmValue::dict(dict)
203    }
204}
205
206/// One ERROR / MISSING node from a tree-sitter parse. All row/column
207/// coordinates are 0-based, matching tree-sitter's native Point.
208///
209/// `message` is a short human-readable description (e.g. `"unexpected
210/// '+'"`, `"missing ')'"`). `snippet` is the raw source text covered by
211/// the node, truncated to 60 chars and with newlines escaped — kept
212/// separately so callers can render the message without re-parsing it.
213#[derive(Debug, Clone)]
214pub struct ParseError {
215    pub start_row: u32,
216    pub start_col: u32,
217    pub end_row: u32,
218    pub end_col: u32,
219    pub start_byte: u32,
220    pub end_byte: u32,
221    pub message: String,
222    pub snippet: String,
223    pub missing: bool,
224}
225
226impl ParseError {
227    pub fn to_vm_value(&self) -> VmValue {
228        self.to_vm_value_with_span(false)
229    }
230
231    /// Same as [`to_vm_value`], plus a `spans_full_source` flag the caller
232    /// computes from the total line count. When true, this ERROR node covers
233    /// essentially the entire file — the fingerprint of a tree-sitter
234    /// grammar-limitation cascade (e.g. Scala 3 indented `match`) rather than a
235    /// localized, model-authored syntax mistake. Edit-validation gates use this
236    /// to avoid hard-rejecting a correct edit on a grammar blind spot.
237    pub fn to_vm_value_with_span(&self, spans_full_source: bool) -> VmValue {
238        let mut dict: harn_vm::value::DictMap = harn_vm::value::DictMap::new();
239        dict.insert("start_row".into(), VmValue::Int(self.start_row as i64));
240        dict.insert("start_col".into(), VmValue::Int(self.start_col as i64));
241        dict.insert("end_row".into(), VmValue::Int(self.end_row as i64));
242        dict.insert("end_col".into(), VmValue::Int(self.end_col as i64));
243        dict.insert("start_byte".into(), VmValue::Int(self.start_byte as i64));
244        dict.insert("end_byte".into(), VmValue::Int(self.end_byte as i64));
245        dict.insert(
246            "message".into(),
247            VmValue::String(arcstr::ArcStr::from(self.message.as_str())),
248        );
249        dict.insert(
250            "snippet".into(),
251            VmValue::String(arcstr::ArcStr::from(self.snippet.as_str())),
252        );
253        dict.insert("missing".into(), VmValue::Bool(self.missing));
254        dict.insert("spans_full_source".into(), VmValue::Bool(spans_full_source));
255        VmValue::dict(dict)
256    }
257}
258
259/// Reference to an identifier that wasn't defined within the current
260/// file. Coordinates are 0-based. `kind` is `"identifier"` for value-side
261/// references and `"type"` for type-only references (TypeScript only).
262#[derive(Debug, Clone)]
263pub struct UndefinedName {
264    pub name: String,
265    pub kind: &'static str,
266    pub row: u32,
267    pub column: u32,
268}
269
270impl UndefinedName {
271    pub fn to_vm_value(&self) -> VmValue {
272        let mut dict: harn_vm::value::DictMap = harn_vm::value::DictMap::new();
273        dict.insert(
274            "name".into(),
275            VmValue::String(arcstr::ArcStr::from(self.name.as_str())),
276        );
277        dict.insert(
278            "kind".into(),
279            VmValue::String(arcstr::ArcStr::from(self.kind)),
280        );
281        dict.insert("row".into(), VmValue::Int(self.row as i64));
282        dict.insert("column".into(), VmValue::Int(self.column as i64));
283        let message = format!("undefined name '{}'", self.name);
284        dict.insert(
285            "message".into(),
286            VmValue::String(arcstr::ArcStr::from(message.as_str())),
287        );
288        VmValue::dict(dict)
289    }
290}