1#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum Sexp {
16 Symbol(String),
18 Str(String),
20 Int(i64),
22 List(Vec<Sexp>),
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum SexpError {
29 Parse(String),
32 Malformed(String),
34}
35
36impl std::fmt::Display for SexpError {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 match self {
39 Self::Parse(message) => write!(f, "parse error: {message}"),
40 Self::Malformed(message) => write!(f, "malformed: {message}"),
41 }
42 }
43}
44
45impl std::error::Error for SexpError {}
46
47impl Sexp {
48 #[must_use]
50 pub fn symbol(name: impl Into<String>) -> Self {
51 Self::Symbol(name.into())
52 }
53
54 #[must_use]
56 pub fn string(value: impl Into<String>) -> Self {
57 Self::Str(value.into())
58 }
59
60 #[must_use]
62 pub const fn int(value: i64) -> Self {
63 Self::Int(value)
64 }
65
66 #[must_use]
68 pub const fn list(items: Vec<Self>) -> Self {
69 Self::List(items)
70 }
71
72 #[must_use]
74 pub fn as_symbol(&self) -> Option<&str> {
75 match self {
76 Self::Symbol(name) => Some(name),
77 _ => None,
78 }
79 }
80
81 #[must_use]
83 pub fn as_str(&self) -> Option<&str> {
84 match self {
85 Self::Str(value) => Some(value),
86 _ => None,
87 }
88 }
89
90 #[must_use]
92 pub const fn as_int(&self) -> Option<i64> {
93 match self {
94 Self::Int(value) => Some(*value),
95 _ => None,
96 }
97 }
98
99 #[must_use]
101 pub fn as_list(&self) -> Option<&[Self]> {
102 match self {
103 Self::List(items) => Some(items),
104 _ => None,
105 }
106 }
107
108 #[must_use]
110 pub fn render(&self) -> String {
111 let mut out = String::new();
112 self.write(&mut out);
113 out
114 }
115
116 fn write(&self, out: &mut String) {
117 match self {
118 Self::Symbol(name) => out.push_str(name),
119 Self::Str(value) => write_string(out, value),
120 Self::Int(value) => out.push_str(&value.to_string()),
121 Self::List(items) => {
122 out.push('(');
123 for (position, item) in items.iter().enumerate() {
124 if position > 0 {
125 out.push(' ');
126 }
127 item.write(out);
128 }
129 out.push(')');
130 }
131 }
132 }
133}
134
135fn write_string(out: &mut String, value: &str) {
137 out.push('"');
138 for ch in value.chars() {
139 match ch {
140 '\\' => out.push_str("\\\\"),
141 '"' => out.push_str("\\\""),
142 '\n' => out.push_str("\\n"),
143 '\r' => out.push_str("\\r"),
144 '\t' => out.push_str("\\t"),
145 other => out.push(other),
146 }
147 }
148 out.push('"');
149}
150
151pub fn parse(input: &str) -> Result<Sexp, SexpError> {
158 let chars: Vec<char> = input.chars().collect();
159 let mut pos = 0;
160 let value = parse_one(&chars, &mut pos)?;
161 skip_whitespace(&chars, &mut pos);
162 if pos < chars.len() {
163 return Err(SexpError::Parse(format!(
164 "trailing input at position {pos}"
165 )));
166 }
167 Ok(value)
168}
169
170fn parse_one(chars: &[char], pos: &mut usize) -> Result<Sexp, SexpError> {
171 skip_whitespace(chars, pos);
172 let Some(&first) = chars.get(*pos) else {
173 return Err(SexpError::Parse("unexpected end of input".to_owned()));
174 };
175 match first {
176 '(' => parse_list(chars, pos),
177 ')' => Err(SexpError::Parse("unexpected closing paren".to_owned())),
178 '"' => parse_string(chars, pos),
179 '-' | '0'..='9' => parse_int(chars, pos),
180 _ => Ok(parse_symbol(chars, pos)),
181 }
182}
183
184fn parse_list(chars: &[char], pos: &mut usize) -> Result<Sexp, SexpError> {
185 *pos += 1; let mut items = Vec::new();
187 loop {
188 skip_whitespace(chars, pos);
189 match chars.get(*pos) {
190 None => return Err(SexpError::Parse("unterminated list".to_owned())),
191 Some(')') => {
192 *pos += 1;
193 return Ok(Sexp::List(items));
194 }
195 Some(_) => items.push(parse_one(chars, pos)?),
196 }
197 }
198}
199
200fn parse_string(chars: &[char], pos: &mut usize) -> Result<Sexp, SexpError> {
201 *pos += 1; let mut value = String::new();
203 loop {
204 let Some(&ch) = chars.get(*pos) else {
205 return Err(SexpError::Parse("unterminated string".to_owned()));
206 };
207 match ch {
208 '"' => {
209 *pos += 1;
210 return Ok(Sexp::Str(value));
211 }
212 '\\' => {
213 *pos += 1;
214 let Some(&escaped) = chars.get(*pos) else {
215 return Err(SexpError::Parse("unterminated escape".to_owned()));
216 };
217 value.push(unescape(escaped)?);
218 *pos += 1;
219 }
220 other => {
221 value.push(other);
222 *pos += 1;
223 }
224 }
225 }
226}
227
228fn unescape(escaped: char) -> Result<char, SexpError> {
229 match escaped {
230 '"' => Ok('"'),
231 '\\' => Ok('\\'),
232 'n' => Ok('\n'),
233 'r' => Ok('\r'),
234 't' => Ok('\t'),
235 other => Err(SexpError::Parse(format!("unknown escape: \\{other}"))),
236 }
237}
238
239fn parse_int(chars: &[char], pos: &mut usize) -> Result<Sexp, SexpError> {
240 let start = *pos;
241 if chars.get(*pos) == Some(&'-') {
242 *pos += 1;
243 }
244 while chars.get(*pos).is_some_and(char::is_ascii_digit) {
245 *pos += 1;
246 }
247 let text: String = chars.get(start..*pos).unwrap_or_default().iter().collect();
248 text.parse()
249 .map(Sexp::Int)
250 .map_err(|error: std::num::ParseIntError| SexpError::Parse(error.to_string()))
251}
252
253fn parse_symbol(chars: &[char], pos: &mut usize) -> Sexp {
254 let start = *pos;
255 while chars
256 .get(*pos)
257 .is_some_and(|ch| !ch.is_whitespace() && *ch != '(' && *ch != ')')
258 {
259 *pos += 1;
260 }
261 let name: String = chars.get(start..*pos).unwrap_or_default().iter().collect();
262 Sexp::Symbol(name)
263}
264
265fn skip_whitespace(chars: &[char], pos: &mut usize) {
266 while chars.get(*pos).is_some_and(|ch| ch.is_whitespace()) {
267 *pos += 1;
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::{Sexp, SexpError, parse};
274
275 #[test]
276 fn renders_each_value_kind() {
277 let value = Sexp::list(vec![
278 Sexp::symbol("tag"),
279 Sexp::string("a \"quoted\"\nline"),
280 Sexp::int(-7),
281 ]);
282 assert_eq!(value.render(), "(tag \"a \\\"quoted\\\"\\nline\" -7)");
283 }
284
285 #[test]
286 fn round_trips_through_parse() {
287 let text = "(gdoc-sync-state 1 (pos \"sec-intro/p-1\" 5 paragraph))";
288 let parsed = parse(text).expect("parses");
289 assert_eq!(parsed.render(), text);
290 }
291
292 #[test]
293 fn parses_nested_and_escaped() {
294 let parsed = parse("(a (b \"x\\ty\") -1)").expect("parses");
295 let items = parsed.as_list().expect("list");
296 assert_eq!(items.first().and_then(Sexp::as_symbol), Some("a"));
297 let inner = items.get(1).and_then(Sexp::as_list).expect("inner list");
298 assert_eq!(inner.get(1).and_then(Sexp::as_str), Some("x\ty"));
299 assert_eq!(items.get(2).and_then(Sexp::as_int), Some(-1));
300 }
301
302 #[test]
303 fn rejects_unterminated_list() {
304 assert!(matches!(parse("(a b"), Err(SexpError::Parse(_))));
305 }
306
307 #[test]
308 fn rejects_trailing_input() {
309 assert!(matches!(parse("(a) (b)"), Err(SexpError::Parse(_))));
310 }
311
312 #[test]
313 fn rejects_unterminated_string() {
314 assert!(matches!(parse("\"abc"), Err(SexpError::Parse(_))));
315 }
316}