Skip to main content

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}