caixa_ast/node.rs
1use crate::span::Span;
2use crate::trivia::Trivia;
3
4/// A parsed Lisp node with span + attached trivia.
5#[derive(Debug, Clone, PartialEq)]
6pub struct Node {
7 pub kind: NodeKind,
8 pub span: Span,
9 /// Comments / blank lines immediately before this node.
10 pub leading: Vec<Trivia>,
11 /// For a compound node: trivia sitting between its last child and its
12 /// closing delimiter, with no child to attach to. Emitted INSIDE the
13 /// form, before the `)`.
14 ///
15 /// Note this slot is overloaded relative to its original meaning
16 /// ("trailing on the same line"); `sequence()` claimed it for the
17 /// dangling case. That is why [`Self::after`] exists rather than this
18 /// being reused again.
19 pub trailing: Vec<Trivia>,
20 /// Trivia that follows this node at its own level — OUTSIDE any
21 /// delimiter it owns.
22 ///
23 /// The distinction from [`Self::trailing`] is load-bearing, not
24 /// pedantry: `(define x 1) ; why` and `(define x 1 ; why\n)` are
25 /// different documents, and a single slot cannot represent both. With
26 /// only the two original slots the top-level case had nowhere to go
27 /// and was DISCARDED at EOF — measurably: one mass-format destroyed 44
28 /// trailing comments in `pleme-io/actions` alone.
29 pub after: Vec<Trivia>,
30}
31
32#[derive(Debug, Clone, PartialEq)]
33pub enum NodeKind {
34 Nil,
35 Symbol(String),
36 Keyword(String),
37 Str(String),
38 Int(i64),
39 Float(f64),
40 Bool(bool),
41 List(Vec<Node>),
42 /// `{ :k v … }` — the brace dialect. REAL SYNTAX per
43 /// theory/TATARA-LISP-CONSOLIDATION.md D4; 62 live caixa.lisp
44 /// manifests author nested maps and are consumed today.
45 Map(Vec<Node>),
46 /// `[ a b … ]` — the vector dialect, D4's sibling.
47 Vector(Vec<Node>),
48 Quote(Box<Node>),
49 Quasiquote(Box<Node>),
50 Unquote(Box<Node>),
51 UnquoteSplice(Box<Node>),
52}
53
54impl Node {
55 #[must_use]
56 pub fn new(kind: NodeKind, span: Span) -> Self {
57 Self {
58 kind,
59 span,
60 leading: Vec::new(),
61 trailing: Vec::new(),
62 after: Vec::new(),
63 }
64 }
65
66 /// Drop all spans + trivia, lowering into the plain `tatara_lisp::Sexp`
67 /// used by the compile pipeline.
68 #[must_use]
69 pub fn to_tatara_sexp(&self) -> tatara_lisp::Sexp {
70 use tatara_lisp::{Atom, Sexp};
71 match &self.kind {
72 NodeKind::Nil => Sexp::Nil,
73 NodeKind::Symbol(s) => Sexp::Atom(Atom::Symbol(s.clone())),
74 NodeKind::Keyword(s) => Sexp::Atom(Atom::Keyword(s.clone())),
75 NodeKind::Str(s) => Sexp::Atom(Atom::Str(s.clone())),
76 NodeKind::Int(i) => Sexp::Atom(Atom::Int(*i)),
77 NodeKind::Float(f) => Sexp::Atom(Atom::Float(*f)),
78 NodeKind::Bool(b) => Sexp::Atom(Atom::Bool(*b)),
79 NodeKind::List(items) => Sexp::List(items.iter().map(Node::to_tatara_sexp).collect()),
80 // `tatara_lisp::Sexp` has no Map/Vector variant yet — adding
81 // them is a LANGUAGE change, sequenced as Phase 2 of
82 // theory/TATARA-LISP-CONSOLIDATION.md D4 and gated on its own
83 // differential run over the 1,123-file corpus (correction C4).
84 // Until that lands, both lower to a plain list: the elements
85 // survive in order, only the brace-ness is dropped. That is
86 // strictly closer to intent than today's behaviour, where the
87 // delimiters lowered as literal `{` / `}` SYMBOLS inside the
88 // list. This projection is used only by the round-trip
89 // equivalence tests, which stay honest because formatting
90 // re-emits the delimiters and re-parsing recovers the node.
91 NodeKind::Map(items) | NodeKind::Vector(items) => {
92 Sexp::List(items.iter().map(Node::to_tatara_sexp).collect())
93 }
94 NodeKind::Quote(inner) => Sexp::Quote(Box::new(inner.to_tatara_sexp())),
95 NodeKind::Quasiquote(inner) => Sexp::Quasiquote(Box::new(inner.to_tatara_sexp())),
96 NodeKind::Unquote(inner) => Sexp::Unquote(Box::new(inner.to_tatara_sexp())),
97 NodeKind::UnquoteSplice(inner) => Sexp::UnquoteSplice(Box::new(inner.to_tatara_sexp())),
98 }
99 }
100
101 /// Head symbol for a list node like `(defX ...)`. Returns None unless this
102 /// is a `List` whose first element is a `Symbol`.
103 #[must_use]
104 pub fn head_symbol(&self) -> Option<&str> {
105 let NodeKind::List(items) = &self.kind else {
106 return None;
107 };
108 let NodeKind::Symbol(s) = &items.first()?.kind else {
109 return None;
110 };
111 Some(s)
112 }
113
114 /// For a list formatted as alternating `:key value :key value`, returns
115 /// the matching value node for `key` (without the leading colon).
116 #[must_use]
117 pub fn kwarg(&self, key: &str) -> Option<&Node> {
118 let NodeKind::List(items) = &self.kind else {
119 return None;
120 };
121 let start = if items
122 .first()
123 .is_some_and(|n| matches!(n.kind, NodeKind::Symbol(_)))
124 {
125 1
126 } else {
127 0
128 };
129 let mut i = start;
130 while i + 1 < items.len() {
131 if let NodeKind::Keyword(k) = &items[i].kind {
132 if k == key {
133 return Some(&items[i + 1]);
134 }
135 }
136 i += 2;
137 }
138 None
139 }
140}