Skip to main content

sim_codec_lisp/implementation/
decode.rs

1//! Decoding entry points for the Lisp codec: lexes and reads s-expression
2//! source (or a `proc-macro2` token stream) into checked `Expr` forms and
3//! located expression trees, lowering the eval surface as it goes.
4
5use proc_macro2::{Delimiter, Group, Literal, TokenStream, TokenTree};
6use sim_codec::{DecodeBudget, Decoder, Input, LocatedDecoder, ReadCx, TreeDecoder};
7use sim_kernel::{
8    Error, Expr, LocatedExpr, LocatedExprTree, QuoteMode, Result, SourceId, Symbol, Value,
9    read_construct_capability, read_eval_capability,
10};
11
12use super::forms::{
13    decode_data_expr, lower_eval_surface, may_be_number_literal, parse_byte_string_literal,
14    parse_logic_var, parse_string_literal, parse_symbol, read_escape_form, read_explicit_quote,
15};
16use super::lex::{
17    lex_lisp_tokens, lex_lisp_tokens_without_trivia, origin_from_lisp_source,
18    strip_lisp_line_comments_preserve_layout,
19};
20use super::tree::LispTreeReader;
21
22/// Returns the type name of the `proc-macro2` token stream this codec accepts as
23/// pre-lexed input, used by the runtime to recognize token-stream decode inputs.
24pub fn token_stream_type_name() -> &'static str {
25    core::any::type_name::<proc_macro2::TokenStream>()
26}
27
28/// Lisp decoder built on `proc-macro2` tokenization.
29///
30/// Implements the codec's [`Decoder`], [`LocatedDecoder`], and [`TreeDecoder`]
31/// roles: it lexes s-expression source and reads it into checked [`Expr`] forms,
32/// located expressions, or located expression trees, lowering the eval surface
33/// as it goes.
34pub struct LispProcMacroDecoder;
35
36impl Decoder for LispProcMacroDecoder {
37    fn decode(&self, cx: &mut ReadCx<'_>, input: Input) -> Result<Expr> {
38        let source = input.into_string()?;
39        let mut budget = DecodeBudget::new(cx.limits);
40        budget.check_input_bytes(cx.codec, source.len())?;
41        let tokens = lex_lisp_tokens_without_trivia(cx.codec, &source, &mut budget)?;
42        budget.check_tokens(cx.codec, tokens.len())?;
43        let mut reader = LispTreeReader::new(
44            cx,
45            SourceId("<lisp-memory>".to_owned()),
46            &source,
47            tokens,
48            &mut budget,
49        );
50        let expr = reader.read_one(0)?.expr;
51        if !reader.is_empty() {
52            return Err(Error::CodecError {
53                codec: cx.codec,
54                message: "expected exactly one top-level expression".to_owned(),
55            });
56        }
57        Ok(expr)
58    }
59}
60
61impl LocatedDecoder for LispProcMacroDecoder {
62    fn decode_located(
63        &self,
64        cx: &mut ReadCx<'_>,
65        input: Input,
66        source_id: String,
67    ) -> Result<LocatedExpr> {
68        decode_lisp_located(cx, source_id, input)
69    }
70}
71
72impl TreeDecoder for LispProcMacroDecoder {
73    fn decode_tree(
74        &self,
75        cx: &mut ReadCx<'_>,
76        input: Input,
77        source_id: String,
78    ) -> Result<LocatedExprTree> {
79        decode_lisp_tree(cx, source_id, input)
80    }
81}
82
83/// Decodes Lisp source into a [`LocatedExpr`], interning the source text under
84/// `source_id` and attaching span origins so diagnostics can point back at it.
85pub fn decode_lisp_located(
86    cx: &mut ReadCx<'_>,
87    source_id: impl Into<String>,
88    input: Input,
89) -> Result<LocatedExpr> {
90    let source = input.into_string()?;
91    let mut budget = DecodeBudget::new(cx.limits);
92    budget.check_input_bytes(cx.codec, source.len())?;
93    let source_id = SourceId(source_id.into());
94    cx.cx.sources_mut().intern_text(source_id.clone(), &source);
95    let normalized = strip_lisp_line_comments_preserve_layout(&source);
96    let stream = normalized
97        .parse::<TokenStream>()
98        .map_err(|err| Error::CodecError {
99            codec: cx.codec,
100            message: err.to_string(),
101        })?;
102    let tokens = stream.into_iter().collect::<Vec<_>>();
103    budget.check_tokens(cx.codec, tokens.len())?;
104    let mut reader = LispReader::new(cx, tokens, &mut budget);
105    let expr = reader.read_one(0)?;
106    if !reader.is_empty() {
107        return Err(Error::CodecError {
108            codec: cx.codec,
109            message: "expected exactly one top-level expression".to_owned(),
110        });
111    }
112
113    Ok(LocatedExpr {
114        expr,
115        origin: Some(origin_from_lisp_source(cx.codec, source_id, &source)),
116    })
117}
118
119/// Decodes Lisp source into a [`LocatedExprTree`], preserving trivia and span
120/// information for every node so the tree can be re-encoded losslessly.
121pub fn decode_lisp_tree(
122    cx: &mut ReadCx<'_>,
123    source_id: impl Into<String>,
124    input: Input,
125) -> Result<LocatedExprTree> {
126    let source = input.into_string()?;
127    let mut budget = DecodeBudget::new(cx.limits);
128    budget.check_input_bytes(cx.codec, source.len())?;
129    let source_id = SourceId(source_id.into());
130    cx.cx.sources_mut().intern_text(source_id.clone(), &source);
131    let tokens = lex_lisp_tokens(cx.codec, &source, &mut budget)?;
132    budget.check_tokens(cx.codec, tokens.len())?;
133    let mut reader = LispTreeReader::new(cx, source_id.clone(), &source, tokens, &mut budget);
134    let mut tree = reader.read_one(0)?;
135    if !reader.is_empty() {
136        return Err(Error::CodecError {
137            codec: cx.codec,
138            message: "expected exactly one top-level expression".to_owned(),
139        });
140    }
141    tree.origin = Some(origin_from_lisp_source(cx.codec, source_id, &source));
142    Ok(tree)
143}
144
145struct LispReader<'a, 'cx, 'b> {
146    cx: &'a mut ReadCx<'cx>,
147    budget: &'b mut sim_codec::DecodeBudget,
148    tokens: Vec<TokenTree>,
149    index: usize,
150}
151
152impl<'a, 'cx, 'b> LispReader<'a, 'cx, 'b> {
153    fn new(
154        cx: &'a mut ReadCx<'cx>,
155        tokens: Vec<TokenTree>,
156        budget: &'b mut sim_codec::DecodeBudget,
157    ) -> Self {
158        Self {
159            cx,
160            budget,
161            tokens,
162            index: 0,
163        }
164    }
165
166    fn is_empty(&self) -> bool {
167        self.index >= self.tokens.len()
168    }
169
170    fn peek(&self) -> Option<&TokenTree> {
171        self.tokens.get(self.index)
172    }
173
174    fn next(&mut self) -> Result<TokenTree> {
175        let token = self
176            .tokens
177            .get(self.index)
178            .cloned()
179            .ok_or(Error::CodecError {
180                codec: self.cx.codec,
181                message: "unexpected end of input".to_owned(),
182            })?;
183        self.index += 1;
184        Ok(token)
185    }
186
187    fn read_one(&mut self, depth: usize) -> Result<Expr> {
188        let token = self.next()?;
189        self.read_token(token, depth)
190    }
191
192    fn read_token(&mut self, token: TokenTree, depth: usize) -> Result<Expr> {
193        match token {
194            TokenTree::Group(group) => self.read_group(group, depth),
195            TokenTree::Literal(literal) => self.read_literal(literal, depth),
196            TokenTree::Punct(punct) if punct.as_char() == '\'' => {
197                self.budget.enter_node(self.cx.codec, depth)?;
198                let expr = self.read_one(depth + 1)?;
199                Ok(Expr::Quote {
200                    mode: QuoteMode::Quote,
201                    expr: Box::new(expr),
202                })
203            }
204            TokenTree::Punct(punct) if punct.as_char() == '#' => self.read_dispatch(depth),
205            token => self.read_symbolish(token, depth),
206        }
207    }
208
209    fn read_group(&mut self, group: Group, depth: usize) -> Result<Expr> {
210        let inner = group.stream().into_iter().collect::<Vec<_>>();
211        let mut nested = LispReader::new(self.cx, inner, self.budget);
212        let mut items: Vec<Expr> = Vec::new();
213        while !nested.is_empty() {
214            nested
215                .budget
216                .check_collection_len(nested.cx.codec, items.len() + 1)?;
217            items.push(nested.read_one(depth + 1)?);
218        }
219        self.budget.enter_node(self.cx.codec, depth)?;
220
221        match group.delimiter() {
222            Delimiter::Parenthesis => {
223                if let Some(quoted) = read_explicit_quote(&items) {
224                    Ok(quoted)
225                } else if let Some(expr) = read_escape_form(&items)? {
226                    Ok(expr)
227                } else {
228                    Ok(Expr::List(items))
229                }
230            }
231            Delimiter::Bracket => Ok(Expr::Vector(items)),
232            Delimiter::Brace | Delimiter::None => Ok(Expr::Block(items)),
233        }
234    }
235
236    fn read_literal(&mut self, literal: Literal, depth: usize) -> Result<Expr> {
237        self.budget.enter_node(self.cx.codec, depth)?;
238        let raw = literal.to_string();
239        if raw.starts_with('"') {
240            let value = parse_string_literal(self.cx.codec, &raw)?;
241            self.budget.check_string_bytes(self.cx.codec, value.len())?;
242            return Ok(Expr::String(value));
243        }
244        if raw.starts_with("b\"") {
245            let value = parse_byte_string_literal(&raw)?;
246            self.budget.check_blob_bytes(self.cx.codec, value.len())?;
247            return Ok(Expr::Bytes(value));
248        }
249        if raw == "true" {
250            return Ok(Expr::Bool(true));
251        }
252        if raw == "false" {
253            return Ok(Expr::Bool(false));
254        }
255        let mut candidate = raw.clone();
256        if may_be_number_literal(&candidate) {
257            while let Some(next) = self.peek() {
258                if !continues_number_literal(&candidate, next) {
259                    break;
260                }
261                let fragment = token_to_symbol_fragment(next);
262                let joined = format!("{candidate}{fragment}");
263                self.next()?;
264                candidate = joined;
265            }
266        }
267        if may_be_number_literal(&candidate)
268            && let Some(number) = self.cx.cx.parse_number_literal(&candidate)?
269        {
270            return Ok(Expr::Number(number));
271        }
272        Ok(Expr::Symbol(Symbol::new(candidate)))
273    }
274
275    fn read_symbolish(&mut self, first: TokenTree, depth: usize) -> Result<Expr> {
276        self.budget.enter_node(self.cx.codec, depth)?;
277        let mut text = token_to_symbol_fragment(&first);
278        while let Some(next) = self.peek() {
279            if !continues_symbol(text.as_str(), next) {
280                break;
281            }
282            text.push_str(&token_to_symbol_fragment(&self.next()?));
283        }
284
285        match text.as_str() {
286            "nil" => Ok(Expr::Nil),
287            "true" => Ok(Expr::Bool(true)),
288            "false" => Ok(Expr::Bool(false)),
289            _ if parse_logic_var(&text).is_some() => Ok(parse_logic_var(&text).unwrap()),
290            _ => Ok(Expr::Symbol(parse_symbol(&text))),
291        }
292    }
293
294    fn read_dispatch(&mut self, depth: usize) -> Result<Expr> {
295        self.budget.enter_node(self.cx.codec, depth)?;
296        let token = self.next()?;
297        match token {
298            TokenTree::Group(group) if group.delimiter() == Delimiter::Parenthesis => {
299                self.read_construct(group, depth + 1)
300            }
301            TokenTree::Ident(ident) if ident == "eval" => {
302                let token = self.next()?;
303                let TokenTree::Group(group) = token else {
304                    return Err(self.error("expected #eval(...)"));
305                };
306                self.read_eval(group, depth + 1)
307            }
308            TokenTree::Punct(punct) if punct.as_char() == '.' => {
309                // `#.` is Common Lisp read-time eval; gate it on the same
310                // capability as `#eval`/`#(...)`, which is otherwise theater
311                // while `#.` evaluates untrusted input unguarded beside them.
312                self.cx.read_policy.require(&read_eval_capability())?;
313                let expr = self.read_one(depth + 1)?;
314                self.eval_read_expr(expr)
315            }
316            other => Err(self.error(format!("unknown dispatch token {other}"))),
317        }
318    }
319
320    fn read_construct(&mut self, group: Group, depth: usize) -> Result<Expr> {
321        self.cx.read_policy.require(&read_construct_capability())?;
322
323        let form = self.read_group(group, depth)?;
324        let Expr::List(items) = form else {
325            return Err(self.error("read constructor must be a list"));
326        };
327        let Some((head, tail)) = items.split_first() else {
328            return Err(self.error("empty read constructor"));
329        };
330        let Expr::Symbol(class_symbol) = head else {
331            return Err(self.error("read constructor head must be a class symbol"));
332        };
333
334        let args = tail
335            .iter()
336            .cloned()
337            .map(|expr| self.decode_read_construct_arg(expr))
338            .collect::<Result<Vec<_>>>()?;
339        let value = self.cx.cx.read_construct(class_symbol, args)?;
340        value.object().as_expr(self.cx.cx)
341    }
342
343    fn read_eval(&mut self, group: Group, depth: usize) -> Result<Expr> {
344        self.cx.read_policy.require(&read_eval_capability())?;
345        let inner = group.stream().into_iter().collect::<Vec<_>>();
346        let mut nested = LispReader::new(self.cx, inner, self.budget);
347        let mut items: Vec<Expr> = Vec::new();
348        while !nested.is_empty() {
349            nested
350                .budget
351                .check_collection_len(nested.cx.codec, items.len() + 1)?;
352            items.push(nested.read_one(depth)?);
353        }
354        let expr = match items.as_slice() {
355            [] => return Err(self.error("empty #eval group")),
356            [one] => one.clone(),
357            _ => lower_eval_surface(Expr::List(items)),
358        };
359        self.eval_read_expr(expr)
360    }
361
362    fn eval_read_expr(&mut self, expr: Expr) -> Result<Expr> {
363        let value = self.cx.cx.eval_expr(lower_eval_surface(expr))?;
364        value.object().as_expr(self.cx.cx)
365    }
366
367    fn decode_read_construct_arg(&mut self, expr: Expr) -> Result<Value> {
368        decode_data_expr(self.cx, expr)
369    }
370
371    fn error(&self, message: impl Into<String>) -> Error {
372        Error::CodecError {
373            codec: self.cx.codec,
374            message: message.into(),
375        }
376    }
377}
378
379fn continues_symbol(current: &str, next: &TokenTree) -> bool {
380    match next {
381        TokenTree::Ident(_) => current.ends_with(['/', ':', '?', '!', '-', '+', '.']),
382        TokenTree::Punct(punct) => {
383            matches!(punct.as_char(), '/' | ':' | '?' | '!' | '-' | '+' | '.')
384        }
385        _ => false,
386    }
387}
388
389fn continues_number_literal(current: &str, next: &TokenTree) -> bool {
390    match next {
391        TokenTree::Punct(punct) => {
392            let joined = format!("{}{}", current, punct.as_char());
393            matches!(punct.as_char(), '+' | '-' | '/' | '.') && may_be_number_literal(&joined)
394        }
395        TokenTree::Ident(ident) => {
396            let joined = format!("{current}{ident}");
397            current.chars().any(|ch| ch.is_ascii_digit()) && may_be_number_literal(&joined)
398        }
399        TokenTree::Literal(literal) => {
400            if !current.ends_with(['+', '-', '/', '.']) {
401                return false;
402            }
403            let joined = format!("{current}{literal}");
404            may_be_number_literal(&joined)
405        }
406        TokenTree::Group(_) => false,
407    }
408}
409
410fn token_to_symbol_fragment(token: &TokenTree) -> String {
411    match token {
412        TokenTree::Group(group) => group.to_string(),
413        TokenTree::Ident(ident) => ident.to_string(),
414        TokenTree::Literal(literal) => literal.to_string(),
415        TokenTree::Punct(punct) => punct.as_char().to_string(),
416    }
417}