#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Item {
Ws(String),
LineComment(String),
BlockComment(String),
DatumComment(String),
Node(Node),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ListKind {
Paren, Bracket, Vector, }
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Node {
List {
kind: ListKind,
items: Vec<Item>,
},
Atom(String),
Str(String),
Prefixed {
prefix: String,
inner: Box<Node>,
},
}
impl Node {
pub fn to_source(&self) -> String {
self.to_string()
}
pub fn head_symbol(&self) -> Option<&str> {
match self {
Node::List { items, .. } => {
for item in items {
if let Item::Node(n) = item {
return match n {
Node::Atom(a) => Some(a.as_str()),
_ => None,
};
}
}
None
}
_ => None,
}
}
pub fn as_symbol(&self) -> Option<&str> {
match self {
Node::Atom(a) => {
let first = a.chars().next()?;
if first.is_ascii_digit() || first == '#' {
None
} else {
Some(a.as_str())
}
}
Node::Prefixed { prefix, inner } if prefix.trim() == "'" => inner.as_symbol(),
Node::List { .. } if self.head_symbol() == Some("quote") => {
let args: Vec<&Node> = self.list_nodes().skip(1).collect();
match args.as_slice() {
[Node::Atom(a)] => Some(a.as_str()),
_ => None,
}
}
_ => None,
}
}
pub fn as_string_lit(&self) -> Option<String> {
let Node::Str(raw) = self else { return None };
if raw.len() < 2 || !raw.starts_with('"') || !raw.ends_with('"') {
return None;
}
let inner = &raw[1..raw.len() - 1];
let mut out = String::with_capacity(inner.len());
let mut chars = inner.chars();
while let Some(c) = chars.next() {
if c == '\\' {
if let Some(next) = chars.next() {
out.push(next);
}
} else {
out.push(c);
}
}
Some(out)
}
pub fn list_nodes(&self) -> impl Iterator<Item = &Node> {
let items: &[Item] = match self {
Node::List { items, .. } => items,
_ => &[],
};
items.iter().filter_map(|i| match i {
Item::Node(n) => Some(n),
_ => None,
})
}
}
impl std::fmt::Display for Item {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Item::Ws(s) | Item::LineComment(s) | Item::BlockComment(s) | Item::DatumComment(s) => {
f.write_str(s)
}
Item::Node(n) => n.fmt(f),
}
}
}
impl std::fmt::Display for Node {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Node::Atom(s) | Node::Str(s) => f.write_str(s),
Node::List { kind, items } => {
f.write_str(match kind {
ListKind::Paren => "(",
ListKind::Bracket => "[",
ListKind::Vector => "#(",
})?;
for item in items {
item.fmt(f)?;
}
f.write_str(match kind {
ListKind::Bracket => "]",
_ => ")",
})
}
Node::Prefixed { prefix, inner } => {
f.write_str(prefix)?;
inner.fmt(f)
}
}
}
}
#[cfg(test)]
mod tests {
use crate::Document;
#[test]
fn head_symbol_skips_trivia() {
let doc = Document::parse("( ;; c\n channel (name 'guix))").unwrap();
let form = doc.forms().next().unwrap();
assert_eq!(form.head_symbol(), Some("channel"));
}
#[test]
fn as_symbol_drills_quote_forms() {
let doc = Document::parse("'guix (quote nonguix) plain \"str\"").unwrap();
let f: Vec<_> = doc.forms().collect();
assert_eq!(f[0].as_symbol(), Some("guix"));
assert_eq!(f[1].as_symbol(), Some("nonguix"));
assert_eq!(f[2].as_symbol(), Some("plain"));
assert_eq!(f[3].as_symbol(), None);
}
#[test]
fn as_string_lit_rejects_malformed_str_nodes() {
use crate::Node;
assert_eq!(Node::Str(String::new()).as_string_lit(), None);
assert_eq!(Node::Str("x".into()).as_string_lit(), None);
assert_eq!(Node::Str("\"".into()).as_string_lit(), None);
assert_eq!(Node::Str("\"unterminated".into()).as_string_lit(), None);
}
#[test]
fn as_string_lit_unescapes() {
let doc = Document::parse(r#""a\"b\\c""#).unwrap();
assert_eq!(
doc.forms().next().unwrap().as_string_lit().unwrap(),
r#"a"b\c"#
);
}
}