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