1#[derive(Debug, Clone, PartialEq, Eq)]
4pub enum Item {
5 Ws(String),
7 LineComment(String),
9 BlockComment(String),
11 DatumComment(String),
13 Node(Node),
14}
15
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum ListKind {
18 Paren, Bracket, Vector, TaggedVector(String),
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum Node {
28 List {
29 kind: ListKind,
30 items: Vec<Item>,
31 },
32 Atom(String),
35 Str(String),
37 Prefixed {
39 prefix: String,
40 inner: Box<Node>,
41 },
42}
43
44impl Node {
45 pub fn to_source(&self) -> String {
47 self.to_string()
48 }
49
50 pub fn head_symbol(&self) -> Option<&str> {
52 match self {
53 Node::List { items, .. } => {
54 for item in items {
55 if let Item::Node(n) = item {
56 return match n {
57 Node::Atom(a) => Some(a.as_str()),
58 _ => None,
59 };
60 }
61 }
62 None
63 }
64 _ => None,
65 }
66 }
67
68 pub fn as_symbol(&self) -> Option<&str> {
70 match self {
71 Node::Atom(a) => {
72 let first = a.chars().next()?;
73 if first.is_ascii_digit() || first == '#' {
74 None
75 } else {
76 Some(a.as_str())
77 }
78 }
79 Node::Prefixed { prefix, inner } if prefix.trim() == "'" => inner.as_symbol(),
80 Node::List { .. } if self.head_symbol() == Some("quote") => {
81 let args: Vec<&Node> = self.list_nodes().skip(1).collect();
82 match args.as_slice() {
83 [Node::Atom(a)] => Some(a.as_str()),
84 _ => None,
85 }
86 }
87 _ => None,
88 }
89 }
90
91 pub fn as_string_lit(&self) -> Option<String> {
93 let Node::Str(raw) = self else { return None };
94 if raw.len() < 2 || !raw.starts_with('"') || !raw.ends_with('"') {
96 return None;
97 }
98 let inner = &raw[1..raw.len() - 1];
99 let mut out = String::with_capacity(inner.len());
100 let mut chars = inner.chars();
101 while let Some(c) = chars.next() {
102 if c == '\\' {
103 if let Some(next) = chars.next() {
106 out.push(next);
107 }
108 } else {
109 out.push(c);
110 }
111 }
112 Some(out)
113 }
114
115 pub fn list_nodes(&self) -> impl Iterator<Item = &Node> {
117 let items: &[Item] = match self {
118 Node::List { items, .. } => items,
119 _ => &[],
120 };
121 items.iter().filter_map(|i| match i {
122 Item::Node(n) => Some(n),
123 _ => None,
124 })
125 }
126}
127
128impl std::fmt::Display for Item {
129 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130 match self {
131 Item::Ws(s) | Item::LineComment(s) | Item::BlockComment(s) | Item::DatumComment(s) => {
132 f.write_str(s)
133 }
134 Item::Node(n) => n.fmt(f),
135 }
136 }
137}
138
139impl std::fmt::Display for Node {
140 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141 match self {
142 Node::Atom(s) | Node::Str(s) => f.write_str(s),
143 Node::List { kind, items } => {
144 f.write_str(match kind {
145 ListKind::Paren => "(",
146 ListKind::Bracket => "[",
147 ListKind::Vector => "#(",
148 ListKind::TaggedVector(s) => s.as_str(),
149 })?;
150 for item in items {
151 item.fmt(f)?;
152 }
153 f.write_str(match kind {
154 ListKind::Bracket => "]",
155 _ => ")",
156 })
157 }
158 Node::Prefixed { prefix, inner } => {
159 f.write_str(prefix)?;
160 inner.fmt(f)
161 }
162 }
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use crate::Document;
169
170 #[test]
171 fn head_symbol_skips_trivia() {
172 let doc = Document::parse("( ;; c\n channel (name 'guix))").unwrap();
173 let form = doc.forms().next().unwrap();
174 assert_eq!(form.head_symbol(), Some("channel"));
175 }
176
177 #[test]
178 fn as_symbol_drills_quote_forms() {
179 let doc = Document::parse("'guix (quote nonguix) plain \"str\"").unwrap();
180 let f: Vec<_> = doc.forms().collect();
181 assert_eq!(f[0].as_symbol(), Some("guix"));
182 assert_eq!(f[1].as_symbol(), Some("nonguix"));
183 assert_eq!(f[2].as_symbol(), Some("plain"));
184 assert_eq!(f[3].as_symbol(), None);
185 }
186
187 #[test]
188 fn as_string_lit_rejects_malformed_str_nodes() {
189 use crate::Node;
190 assert_eq!(Node::Str(String::new()).as_string_lit(), None);
191 assert_eq!(Node::Str("x".into()).as_string_lit(), None);
192 assert_eq!(Node::Str("\"".into()).as_string_lit(), None);
193 assert_eq!(Node::Str("\"unterminated".into()).as_string_lit(), None);
194 }
195
196 #[test]
197 fn as_string_lit_unescapes() {
198 let doc = Document::parse(r#""a\"b\\c""#).unwrap();
199 assert_eq!(
200 doc.forms().next().unwrap().as_string_lit().unwrap(),
201 r#"a"b\c"#
202 );
203 }
204}