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