1
//! A small, self-contained s-expression value + reader/writer for the SLYNK
2
//! wire subset (see `doc/editor/slynk-protocol-transcript.org`). This is
3
//! deliberately NOT the nomiscript reader: SLYNK forms are Emacs-Lisp / CL
4
//! shaped (`'quote`, `pkg:sym`, `t`/`nil`, dotted-free lists, CL string
5
//! escapes) and the language reader neither parses nor preserves those. The
6
//! grammar here is exactly what SLY emits; anything outside it is a parse
7
//! error, surfaced to the caller (never a panic).
8

            
9
use std::fmt::Write as _;
10

            
11
/// Recursion ceiling for nested lists — SLYNK messages are shallow; a deeper
12
/// nesting is treated as malformed rather than risking a native stack overflow
13
/// on hostile input (mirrors the compiler's depth-guard discipline).
14
const MAX_DEPTH: usize = 64;
15

            
16
/// A parsed SLYNK s-expression. `Symbol` covers bare symbols, keywords
17
/// (`:foo`), `t`, and `nil` verbatim (callers compare against the literal text,
18
/// e.g. `"nil"`); `quote` is preserved as a `(quote X)` two-element list so
19
/// `'(a b)` and `(quote (a b))` are indistinguishable downstream (matching CL).
20
#[derive(Debug, Clone, PartialEq)]
21
pub enum Sexp {
22
    Symbol(String),
23
    Int(i64),
24
    Str(String),
25
    List(Vec<Sexp>),
26
}
27

            
28
impl Sexp {
29
    /// The symbol/keyword text, if this is a `Symbol`.
30
27
    pub fn as_symbol(&self) -> Option<&str> {
31
27
        match self {
32
27
            Sexp::Symbol(s) => Some(s),
33
            _ => None,
34
        }
35
27
    }
36

            
37
    /// The string contents, if this is a `Str`.
38
6
    pub fn as_str(&self) -> Option<&str> {
39
6
        match self {
40
6
            Sexp::Str(s) => Some(s),
41
            _ => None,
42
        }
43
6
    }
44

            
45
12
    pub fn as_int(&self) -> Option<i64> {
46
12
        match self {
47
12
            Sexp::Int(n) => Some(*n),
48
            _ => None,
49
        }
50
12
    }
51

            
52
    /// The elements, if this is a `List`.
53
32
    pub fn as_list(&self) -> Option<&[Sexp]> {
54
32
        match self {
55
32
            Sexp::List(items) => Some(items),
56
            _ => None,
57
        }
58
32
    }
59
}
60

            
61
#[derive(Debug, PartialEq, Eq)]
62
pub struct ParseError(pub String);
63

            
64
impl std::fmt::Display for ParseError {
65
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66
        write!(f, "slynk sexp parse error: {}", self.0)
67
    }
68
}
69

            
70
impl std::error::Error for ParseError {}
71

            
72
/// Parses exactly one s-expression from `input`, ignoring trailing whitespace.
73
23
pub fn parse(input: &str) -> Result<Sexp, ParseError> {
74
23
    let mut p = Parser {
75
23
        chars: input.chars().peekable(),
76
23
        depth: 0,
77
23
    };
78
23
    p.skip_ws();
79
23
    let v = p.parse_value()?;
80
16
    p.skip_ws();
81
16
    if p.chars.peek().is_some() {
82
        return Err(ParseError("trailing data after expression".into()));
83
16
    }
84
16
    Ok(v)
85
23
}
86

            
87
struct Parser<'a> {
88
    chars: std::iter::Peekable<std::str::Chars<'a>>,
89
    depth: usize,
90
}
91

            
92
impl Parser<'_> {
93
240
    fn skip_ws(&mut self) {
94
304
        while matches!(self.chars.peek(), Some(c) if c.is_whitespace()) {
95
64
            self.chars.next();
96
64
        }
97
240
    }
98

            
99
192
    fn parse_value(&mut self) -> Result<Sexp, ParseError> {
100
192
        match self.chars.peek().copied() {
101
1
            None => Err(ParseError("unexpected end of input".into())),
102
103
            Some('(') => self.parse_list(),
103
11
            Some('"') => self.parse_string(),
104
            Some('\'') => {
105
                // 'X → (quote X), matching CL/Elisp reader.
106
4
                self.chars.next();
107
4
                let inner = self.parse_value()?;
108
3
                Ok(Sexp::List(vec![Sexp::Symbol("quote".into()), inner]))
109
            }
110
1
            Some(')') => Err(ParseError("unexpected ')'".into())),
111
72
            Some(_) => self.parse_atom(),
112
        }
113
192
    }
114

            
115
103
    fn parse_list(&mut self) -> Result<Sexp, ParseError> {
116
103
        if self.depth >= MAX_DEPTH {
117
1
            return Err(ParseError("list nesting too deep".into()));
118
102
        }
119
102
        self.chars.next(); // consume '('
120
102
        self.depth += 1;
121
102
        let mut items = Vec::new();
122
        loop {
123
201
            self.skip_ws();
124
201
            match self.chars.peek().copied() {
125
3
                None => return Err(ParseError("unterminated list".into())),
126
                Some(')') => {
127
33
                    self.chars.next();
128
33
                    break;
129
                }
130
165
                Some(_) => items.push(self.parse_value()?),
131
            }
132
        }
133
33
        self.depth -= 1;
134
33
        Ok(Sexp::List(items))
135
103
    }
136

            
137
11
    fn parse_string(&mut self) -> Result<Sexp, ParseError> {
138
11
        self.chars.next(); // consume opening '"'
139
11
        let mut s = String::new();
140
        loop {
141
71
            match self.chars.next() {
142
1
                None => return Err(ParseError("unterminated string".into())),
143
10
                Some('"') => break,
144
                // CL/Elisp string escapes: only `\` and `"` are semantically
145
                // meaningful on the wire; any other `\x` keeps `x` verbatim.
146
2
                Some('\\') => match self.chars.next() {
147
                    None => return Err(ParseError("trailing backslash in string".into())),
148
                    Some('n') => s.push('\n'),
149
                    Some('t') => s.push('\t'),
150
2
                    Some(c) => s.push(c),
151
                },
152
58
                Some(c) => s.push(c),
153
            }
154
        }
155
10
        Ok(Sexp::Str(s))
156
11
    }
157

            
158
    /// A bare token (symbol/keyword/`t`/`nil`/integer), terminated by
159
    /// whitespace, `(`, `)`, or `"`.
160
72
    fn parse_atom(&mut self) -> Result<Sexp, ParseError> {
161
72
        let mut tok = String::new();
162
557
        while let Some(&c) = self.chars.peek() {
163
555
            if c.is_whitespace() || c == '(' || c == ')' || c == '"' {
164
70
                break;
165
485
            }
166
485
            tok.push(c);
167
485
            self.chars.next();
168
        }
169
72
        if tok.is_empty() {
170
            return Err(ParseError("empty atom".into()));
171
72
        }
172
72
        if let Ok(n) = tok.parse::<i64>() {
173
15
            return Ok(Sexp::Int(n));
174
57
        }
175
57
        Ok(Sexp::Symbol(tok))
176
72
    }
177
}
178

            
179
// --- writer ---
180

            
181
/// Renders an `Sexp` to its wire form with correct CL string escaping.
182
25
pub fn write(sexp: &Sexp) -> String {
183
25
    let mut out = String::new();
184
25
    write_into(&mut out, sexp);
185
25
    out
186
25
}
187

            
188
210
fn write_into(out: &mut String, sexp: &Sexp) {
189
210
    match sexp {
190
79
        Sexp::Symbol(s) => out.push_str(s),
191
29
        Sexp::Int(n) => {
192
29
            let _ = write!(out, "{n}");
193
29
        }
194
35
        Sexp::Str(s) => write_string(out, s),
195
67
        Sexp::List(items) => {
196
67
            out.push('(');
197
185
            for (i, item) in items.iter().enumerate() {
198
185
                if i > 0 {
199
121
                    out.push(' ');
200
121
                }
201
185
                write_into(out, item);
202
            }
203
67
            out.push(')');
204
        }
205
    }
206
210
}
207

            
208
/// Writes a CL string literal: wrap in `"` and escape `\` and `"`. Other bytes
209
/// (including newlines) pass through literally — the reader handles them.
210
35
fn write_string(out: &mut String, s: &str) {
211
35
    out.push('"');
212
265
    for c in s.chars() {
213
265
        if c == '"' || c == '\\' {
214
3
            out.push('\\');
215
262
        }
216
265
        out.push(c);
217
    }
218
35
    out.push('"');
219
35
}
220

            
221
#[cfg(test)]
222
mod tests {
223
    use super::*;
224

            
225
3
    fn sym(s: &str) -> Sexp {
226
3
        Sexp::Symbol(s.into())
227
3
    }
228

            
229
    #[test]
230
1
    fn parses_the_emacs_rex_connection_info() {
231
1
        let v = parse("(:emacs-rex (slynk:connection-info) nil t 1)").unwrap();
232
1
        let items = v.as_list().unwrap();
233
1
        assert_eq!(items[0].as_symbol(), Some(":emacs-rex"));
234
1
        assert_eq!(
235
1
            items[1].as_list().unwrap()[0].as_symbol(),
236
            Some("slynk:connection-info")
237
        );
238
1
        assert_eq!(items[2].as_symbol(), Some("nil"));
239
1
        assert_eq!(items[3].as_symbol(), Some("t"));
240
1
        assert_eq!(items[4].as_int(), Some(1));
241
1
    }
242

            
243
    #[test]
244
1
    fn parses_channel_send_process() {
245
1
        let v = parse("(:emacs-channel-send 1 (:process \"(+ 1 2)\"))").unwrap();
246
1
        let items = v.as_list().unwrap();
247
1
        assert_eq!(items[0].as_symbol(), Some(":emacs-channel-send"));
248
1
        assert_eq!(items[1].as_int(), Some(1));
249
1
        let inner = items[2].as_list().unwrap();
250
1
        assert_eq!(inner[0].as_symbol(), Some(":process"));
251
1
        assert_eq!(inner[1].as_str(), Some("(+ 1 2)"));
252
1
    }
253

            
254
    #[test]
255
1
    fn parses_quote_as_quote_list() {
256
1
        let v = parse("(slynk:slynk-require '(\"a\" \"b\"))").unwrap();
257
1
        let items = v.as_list().unwrap();
258
1
        let q = items[1].as_list().unwrap();
259
1
        assert_eq!(q[0].as_symbol(), Some("quote"));
260
1
        assert_eq!(q[1].as_list().unwrap().len(), 2);
261
1
    }
262

            
263
    #[test]
264
1
    fn parses_string_with_escapes() {
265
1
        let v = parse(r#"(:x "a\"b\\c")"#).unwrap();
266
1
        assert_eq!(v.as_list().unwrap()[1].as_str(), Some("a\"b\\c"));
267
1
    }
268

            
269
    #[test]
270
1
    fn writes_string_with_escaping() {
271
1
        let s = Sexp::Str("a\"b\\c\nd".into());
272
1
        assert_eq!(write(&s), "\"a\\\"b\\\\c\nd\"");
273
1
    }
274

            
275
    #[test]
276
1
    fn write_round_trips_a_return() {
277
1
        let v = Sexp::List(vec![
278
1
            sym(":return"),
279
1
            Sexp::List(vec![sym(":ok"), sym("nil")]),
280
1
            Sexp::Int(2),
281
1
        ]);
282
1
        assert_eq!(write(&v), "(:return (:ok nil) 2)");
283
1
        assert_eq!(parse(&write(&v)).unwrap(), v);
284
1
    }
285

            
286
    #[test]
287
1
    fn malformed_inputs_error_not_panic() {
288
6
        for bad in ["(", ")", "(:a", "\"unterminated", "(a (b (c", "'"] {
289
6
            assert!(parse(bad).is_err(), "{bad:?} should be a parse error");
290
        }
291
1
    }
292

            
293
    #[test]
294
1
    fn deep_nesting_is_rejected() {
295
1
        let deep = "(".repeat(200);
296
1
        assert!(parse(&deep).is_err());
297
1
    }
298
}