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
use scripting::nomiscript::{Value, format_value};
12

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

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

            
30
impl Sexp {
31
    /// Emacs's `nil`, spelled by the nomiscript printer so both sides agree.
32
16
    pub fn nil() -> Self {
33
16
        Sexp::Symbol(format_value(&Value::Nil))
34
16
    }
35

            
36
    /// The symbol/keyword text, if this is a `Symbol`.
37
27
    pub fn as_symbol(&self) -> Option<&str> {
38
27
        match self {
39
27
            Sexp::Symbol(s) => Some(s),
40
            _ => None,
41
        }
42
27
    }
43

            
44
    /// The string contents, if this is a `Str`.
45
6
    pub fn as_str(&self) -> Option<&str> {
46
6
        match self {
47
6
            Sexp::Str(s) => Some(s),
48
            _ => None,
49
        }
50
6
    }
51

            
52
12
    pub fn as_int(&self) -> Option<i64> {
53
12
        match self {
54
12
            Sexp::Int(n) => Some(*n),
55
            _ => None,
56
        }
57
12
    }
58

            
59
    /// The elements, if this is a `List`.
60
32
    pub fn as_list(&self) -> Option<&[Sexp]> {
61
32
        match self {
62
32
            Sexp::List(items) => Some(items),
63
            _ => None,
64
        }
65
32
    }
66
}
67

            
68
#[derive(Debug, PartialEq, Eq)]
69
pub struct ParseError(pub String);
70

            
71
impl std::fmt::Display for ParseError {
72
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73
        write!(f, "slynk sexp parse error: {}", self.0)
74
    }
75
}
76

            
77
impl std::error::Error for ParseError {}
78

            
79
/// Parses exactly one s-expression from `input`, ignoring trailing whitespace.
80
26
pub fn parse(input: &str) -> Result<Sexp, ParseError> {
81
26
    let mut p = Parser {
82
26
        chars: input.chars().peekable(),
83
26
        depth: 0,
84
26
    };
85
26
    p.skip_ws();
86
26
    let v = p.parse_value()?;
87
19
    p.skip_ws();
88
19
    if p.chars.peek().is_some() {
89
        return Err(ParseError("trailing data after expression".into()));
90
19
    }
91
19
    Ok(v)
92
26
}
93

            
94
struct Parser<'a> {
95
    chars: std::iter::Peekable<std::str::Chars<'a>>,
96
    depth: usize,
97
}
98

            
99
impl Parser<'_> {
100
246
    fn skip_ws(&mut self) {
101
310
        while matches!(self.chars.peek(), Some(c) if c.is_whitespace()) {
102
64
            self.chars.next();
103
64
        }
104
246
    }
105

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

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

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

            
165
    /// A bare token (symbol/keyword/`t`/`nil`/integer), terminated by
166
    /// whitespace, `(`, `)`, or `"`.
167
75
    fn parse_atom(&mut self) -> Result<Sexp, ParseError> {
168
75
        let mut tok = String::new();
169
567
        while let Some(&c) = self.chars.peek() {
170
562
            if c.is_whitespace() || c == '(' || c == ')' || c == '"' {
171
70
                break;
172
492
            }
173
492
            tok.push(c);
174
492
            self.chars.next();
175
        }
176
75
        if tok.is_empty() {
177
            return Err(ParseError("empty atom".into()));
178
75
        }
179
75
        if let Ok(n) = tok.parse::<i64>() {
180
15
            return Ok(Sexp::Int(n));
181
60
        }
182
60
        Ok(Sexp::Symbol(tok))
183
75
    }
184
}
185

            
186
// --- writer ---
187

            
188
/// Renders an `Sexp` to its wire form with correct CL string escaping.
189
26
pub fn write(sexp: &Sexp) -> String {
190
26
    let mut out = String::new();
191
26
    write_into(&mut out, sexp);
192
26
    out
193
26
}
194

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

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

            
228
#[cfg(test)]
229
mod tests {
230
    use super::*;
231

            
232
5
    fn sym(s: &str) -> Sexp {
233
5
        Sexp::Symbol(s.into())
234
5
    }
235

            
236
    #[test]
237
1
    fn parses_the_emacs_rex_connection_info() {
238
1
        let v = parse("(:emacs-rex (slynk:connection-info) nil t 1)").unwrap();
239
1
        let items = v.as_list().unwrap();
240
1
        assert_eq!(items[0].as_symbol(), Some(":emacs-rex"));
241
1
        assert_eq!(
242
1
            items[1].as_list().unwrap()[0].as_symbol(),
243
            Some("slynk:connection-info")
244
        );
245
1
        assert_eq!(items[2].as_symbol(), Some("nil"));
246
1
        assert_eq!(items[3].as_symbol(), Some("t"));
247
1
        assert_eq!(items[4].as_int(), Some(1));
248
1
    }
249

            
250
    #[test]
251
1
    fn parses_channel_send_process() {
252
1
        let v = parse("(:emacs-channel-send 1 (:process \"(+ 1 2)\"))").unwrap();
253
1
        let items = v.as_list().unwrap();
254
1
        assert_eq!(items[0].as_symbol(), Some(":emacs-channel-send"));
255
1
        assert_eq!(items[1].as_int(), Some(1));
256
1
        let inner = items[2].as_list().unwrap();
257
1
        assert_eq!(inner[0].as_symbol(), Some(":process"));
258
1
        assert_eq!(inner[1].as_str(), Some("(+ 1 2)"));
259
1
    }
260

            
261
    #[test]
262
1
    fn parses_quote_as_quote_list() {
263
1
        let v = parse("(slynk:slynk-require '(\"a\" \"b\"))").unwrap();
264
1
        let items = v.as_list().unwrap();
265
1
        let q = items[1].as_list().unwrap();
266
1
        assert_eq!(q[0].as_symbol(), Some("quote"));
267
1
        assert_eq!(q[1].as_list().unwrap().len(), 2);
268
1
    }
269

            
270
    #[test]
271
1
    fn parses_string_with_escapes() {
272
1
        let v = parse(r#"(:x "a\"b\\c")"#).unwrap();
273
1
        assert_eq!(v.as_list().unwrap()[1].as_str(), Some("a\"b\\c"));
274
1
    }
275

            
276
    #[test]
277
1
    fn writes_string_with_escaping() {
278
1
        let s = Sexp::Str("a\"b\\c\nd".into());
279
1
        assert_eq!(write(&s), "\"a\\\"b\\\\c\nd\"");
280
1
    }
281

            
282
    #[test]
283
1
    fn write_round_trips_a_return() {
284
1
        let v = Sexp::List(vec![
285
1
            sym(":return"),
286
1
            Sexp::List(vec![sym(":ok"), sym("nil")]),
287
1
            Sexp::Int(2),
288
1
        ]);
289
1
        assert_eq!(write(&v), "(:return (:ok nil) 2)");
290
1
        assert_eq!(parse(&write(&v)).unwrap(), v);
291
1
    }
292

            
293
    #[test]
294
1
    fn printed_t_and_nil_read_back_as_emacs_t_and_nil() {
295
1
        assert_eq!(parse(&format_value(&Value::T)).unwrap(), sym("t"));
296
1
        assert_eq!(parse(&format_value(&Value::Nil)).unwrap(), sym("nil"));
297
1
    }
298

            
299
    #[test]
300
1
    fn nil_writes_as_emacs_nil() {
301
1
        assert_eq!(write(&Sexp::nil()), "nil");
302
1
        assert_eq!(parse("nil").unwrap(), Sexp::nil());
303
1
    }
304

            
305
    #[test]
306
1
    fn malformed_inputs_error_not_panic() {
307
6
        for bad in ["(", ")", "(:a", "\"unterminated", "(a (b (c", "'"] {
308
6
            assert!(parse(bad).is_err(), "{bad:?} should be a parse error");
309
        }
310
1
    }
311

            
312
    #[test]
313
1
    fn deep_nesting_is_rejected() {
314
1
        let deep = "(".repeat(200);
315
1
        assert!(parse(&deep).is_err());
316
1
    }
317
}