Skip to main content

scheme_edit/
cst.rs

1/// One element of a list body or the top level. Whitespace and comments
2/// are items like any other, so emitting a document is pure concatenation.
3#[derive(Debug, Clone, PartialEq, Eq)]
4pub enum Item {
5    /// Exact run of whitespace as it appeared, e.g. "  \n\t".
6    Ws(String),
7    /// ";..." up to but excluding the newline (the newline is Ws).
8    LineComment(String),
9    /// "#|...|#" or "#!...!#" including delimiters; nesting preserved verbatim.
10    BlockComment(String),
11    /// "#;" plus the complete commented-out datum, verbatim.
12    DatumComment(String),
13    Node(Node),
14}
15
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum ListKind {
18    Paren,   // ( )
19    Bracket, // [ ]
20    Vector,  // #( )
21    /// Tagged vector/array; String is the full opener incl. `#` and `(`,
22    /// e.g. `#u8(`, `#vu8(`, `#f32(`, `#2(`, `#1@0(`. Closes with `)`.
23    TaggedVector(String),
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum Node {
28    List {
29        kind: ListKind,
30        items: Vec<Item>,
31    },
32    /// Symbols, numbers, booleans (#t/#f), chars (#\x), keywords (#:k),
33    /// `#{...}#` extended symbols, and `.`
34    Atom(String),
35    /// Raw string literal INCLUDING surrounding quotes, escapes as written.
36    Str(String),
37    /// prefix is one of: ' ` , ,@ #' #` #, #,@ #~ #$ #$@ #+ (gexp syntax).
38    Prefixed {
39        prefix: String,
40        inner: Box<Node>,
41    },
42}
43
44impl Node {
45    /// Verbatim source text of this node.
46    pub fn to_source(&self) -> String {
47        self.to_string()
48    }
49
50    /// For a list, the text of its first data child if that child is an Atom.
51    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    /// Symbol text, drilling through `'sym` and `(quote sym)`.
69    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    /// Unescaped contents of a string literal.
92    pub fn as_string_lit(&self) -> Option<String> {
93        let Node::Str(raw) = self else { return None };
94        // Str is a public variant; guard against hand-built malformed nodes.
95        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                // Permissive: any escaped char yields itself, matching the
104                // subset of guile escapes that appear in guix files.
105                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    /// Data children of a list, trivia skipped. Empty for non-lists.
116    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}