use crate::span::Span;
use crate::trivia::Trivia;
#[derive(Debug, Clone, PartialEq)]
pub struct Node {
pub kind: NodeKind,
pub span: Span,
pub leading: Vec<Trivia>,
pub trailing: Vec<Trivia>,
pub after: Vec<Trivia>,
}
#[derive(Debug, Clone, PartialEq, gen_platform::IsVariant)]
pub enum NodeKind {
Nil,
Symbol(String),
Keyword(String),
Str(String),
Int(i64),
Float(f64),
Bool(bool),
List(Vec<Node>),
Map(Vec<Node>),
Vector(Vec<Node>),
Quote(Box<Node>),
Quasiquote(Box<Node>),
Unquote(Box<Node>),
UnquoteSplice(Box<Node>),
}
impl Node {
#[must_use]
pub fn new(kind: NodeKind, span: Span) -> Self {
Self {
kind,
span,
leading: Vec::new(),
trailing: Vec::new(),
after: Vec::new(),
}
}
#[must_use]
pub fn to_tatara_sexp(&self) -> tatara_lisp::Sexp {
use tatara_lisp::{Atom, Sexp};
match &self.kind {
NodeKind::Nil => Sexp::Nil,
NodeKind::Symbol(s) => Sexp::Atom(Atom::Symbol(s.clone())),
NodeKind::Keyword(s) => Sexp::Atom(Atom::Keyword(s.clone())),
NodeKind::Str(s) => Sexp::Atom(Atom::Str(s.clone())),
NodeKind::Int(i) => Sexp::Atom(Atom::Int(*i)),
NodeKind::Float(f) => Sexp::Atom(Atom::Float(*f)),
NodeKind::Bool(b) => Sexp::Atom(Atom::Bool(*b)),
NodeKind::List(items) => Sexp::List(items.iter().map(Node::to_tatara_sexp).collect()),
NodeKind::Map(items) | NodeKind::Vector(items) => {
Sexp::List(items.iter().map(Node::to_tatara_sexp).collect())
}
NodeKind::Quote(inner) => Sexp::Quote(Box::new(inner.to_tatara_sexp())),
NodeKind::Quasiquote(inner) => Sexp::Quasiquote(Box::new(inner.to_tatara_sexp())),
NodeKind::Unquote(inner) => Sexp::Unquote(Box::new(inner.to_tatara_sexp())),
NodeKind::UnquoteSplice(inner) => Sexp::UnquoteSplice(Box::new(inner.to_tatara_sexp())),
}
}
#[must_use]
pub fn head_symbol(&self) -> Option<&str> {
let NodeKind::List(items) = &self.kind else {
return None;
};
let NodeKind::Symbol(s) = &items.first()?.kind else {
return None;
};
Some(s)
}
#[must_use]
pub fn kwarg(&self, key: &str) -> Option<&Node> {
let NodeKind::List(items) = &self.kind else {
return None;
};
let start = if items.first().is_some_and(|n| n.kind.is_symbol()) {
1
} else {
0
};
let mut i = start;
while i + 1 < items.len() {
if let NodeKind::Keyword(k) = &items[i].kind {
if k == key {
return Some(&items[i + 1]);
}
}
i += 2;
}
None
}
}
#[cfg(test)]
mod is_variant_tests {
use super::*;
fn all_variants() -> Vec<(NodeKind, &'static str)> {
vec![
(NodeKind::Nil, "Nil"),
(NodeKind::Symbol("x".into()), "Symbol"),
(NodeKind::Keyword("k".into()), "Keyword"),
(NodeKind::Str("s".into()), "Str"),
(NodeKind::Int(0), "Int"),
(NodeKind::Float(0.0), "Float"),
(NodeKind::Bool(false), "Bool"),
(NodeKind::List(Vec::new()), "List"),
(NodeKind::Map(Vec::new()), "Map"),
(NodeKind::Vector(Vec::new()), "Vector"),
(
NodeKind::Quote(Box::new(Node::new(NodeKind::Nil, Span::new(0, 0)))),
"Quote",
),
(
NodeKind::Quasiquote(Box::new(Node::new(NodeKind::Nil, Span::new(0, 0)))),
"Quasiquote",
),
(
NodeKind::Unquote(Box::new(Node::new(NodeKind::Nil, Span::new(0, 0)))),
"Unquote",
),
(
NodeKind::UnquoteSplice(Box::new(Node::new(NodeKind::Nil, Span::new(0, 0)))),
"UnquoteSplice",
),
]
}
fn predicate_row(k: &NodeKind) -> [bool; 14] {
[
k.is_nil(),
k.is_symbol(),
k.is_keyword(),
k.is_str(),
k.is_int(),
k.is_float(),
k.is_bool(),
k.is_list(),
k.is_map(),
k.is_vector(),
k.is_quote(),
k.is_quasiquote(),
k.is_unquote(),
k.is_unquote_splice(),
]
}
#[test]
fn node_kind_is_variant_predicates_partition_the_arm_set() {
let variants = all_variants();
for (idx, (variant, name)) in variants.iter().enumerate() {
let observed = predicate_row(variant);
let mut expected = [false; 14];
expected[idx] = true;
assert_eq!(
observed, expected,
"NodeKind::{name} at declaration-order slot {idx} must \
satisfy exactly one is_* predicate (its own); observed \
row must equal the one-hot expected row"
);
}
}
#[test]
fn node_kind_is_symbol_and_is_keyword_byte_equal_pre_lift_matches_shape() {
for (variant, name) in all_variants() {
let via_matches_symbol = matches!(variant, NodeKind::Symbol(_));
let via_predicate_symbol = variant.is_symbol();
assert_eq!(
via_predicate_symbol, via_matches_symbol,
"NodeKind::{name}.is_symbol() must byte-equal \
matches!(_, NodeKind::Symbol(_)) — otherwise the \
converged call sites in caixa-ast/caixa-fmt would \
silently disagree with their pre-lift shape"
);
let via_matches_keyword = matches!(variant, NodeKind::Keyword(_));
let via_predicate_keyword = variant.is_keyword();
assert_eq!(
via_predicate_keyword, via_matches_keyword,
"NodeKind::{name}.is_keyword() must byte-equal \
matches!(_, NodeKind::Keyword(_)) — otherwise the \
converged call sites in caixa-fmt/caixa-lint would \
silently disagree with their pre-lift shape"
);
}
}
#[test]
fn node_kind_is_int_or_is_float_byte_equal_pre_lift_numeric_matches_shape() {
for (variant, name) in all_variants() {
let via_matches = matches!(variant, NodeKind::Int(_) | NodeKind::Float(_));
let via_predicate = variant.is_int() || variant.is_float();
assert_eq!(
via_predicate, via_matches,
"NodeKind::{name}.is_int() || .is_float() must \
byte-equal matches!(_, NodeKind::Int(_) | \
NodeKind::Float(_)) — otherwise caixa-fmt's grid-column \
numeric-right-align gate would silently disagree with \
its pre-lift shape"
);
}
}
}