Skip to main content

sema_reader/
reader.rs

1use std::collections::BTreeMap;
2use std::rc::Rc;
3
4use sema_core::{resolve, SemaError, Span, SpanMap, Value, ValueView};
5
6use crate::lexer::{tokenize, FStringPart, SpannedToken, Token};
7
8/// Maximum nesting depth for parsing. Untrusted input (files, the WASM
9/// playground, f-string interpolations) must not be able to overflow the thread
10/// stack via thousands of nested forms. 1024 is far beyond any real program.
11const MAX_PARSE_DEPTH: usize = 1024;
12
13/// Highest positional placeholder (`%N`) a short lambda may use. The desugar
14/// materializes a parameter symbol for every slot up to the highest index, so
15/// this bound is what keeps a tiny malicious input from forcing a huge
16/// allocation at read time.
17const MAX_SHORT_LAMBDA_ARG: usize = 255;
18
19struct Parser {
20    tokens: Vec<SpannedToken>,
21    pos: usize,
22    span_map: SpanMap,
23    symbol_spans: Vec<(String, Span)>,
24    depth: usize,
25    short_lambda_depth: usize,
26}
27
28impl Parser {
29    fn new(tokens: Vec<SpannedToken>) -> Self {
30        Parser {
31            tokens,
32            pos: 0,
33            span_map: SpanMap::new(),
34            symbol_spans: Vec::new(),
35            depth: 0,
36            short_lambda_depth: 0,
37        }
38    }
39
40    fn peek(&self) -> Option<&Token> {
41        let mut pos = self.pos;
42        while let Some(t) = self.tokens.get(pos) {
43            match &t.token {
44                Token::Comment(_) | Token::Newline => pos += 1,
45                _ => return Some(&t.token),
46            }
47        }
48        None
49    }
50
51    fn span(&self) -> Span {
52        let mut pos = self.pos;
53        while let Some(t) = self.tokens.get(pos) {
54            match &t.token {
55                Token::Comment(_) | Token::Newline => pos += 1,
56                _ => return t.span,
57            }
58        }
59        Span::point(0, 0)
60    }
61
62    fn skip_trivia(&mut self) {
63        while let Some(t) = self.tokens.get(self.pos) {
64            match &t.token {
65                Token::Comment(_) | Token::Newline => self.pos += 1,
66                _ => break,
67            }
68        }
69    }
70
71    fn advance(&mut self) -> Option<&SpannedToken> {
72        self.skip_trivia();
73        let tok = self.tokens.get(self.pos);
74        if tok.is_some() {
75            self.pos += 1;
76        }
77        tok
78    }
79
80    fn expect(&mut self, expected: &Token) -> Result<(), SemaError> {
81        let span = self.span();
82        match self.advance() {
83            Some(t) if &t.token == expected => Ok(()),
84            Some(t) => Err(SemaError::Reader {
85                message: format!(
86                    "expected `{}`, got `{}`",
87                    token_display(expected),
88                    token_display(&t.token)
89                ),
90                span,
91            }),
92            None => Err(SemaError::Reader {
93                message: format!("expected `{}`, got end of input", token_display(expected)),
94                span,
95            }),
96        }
97    }
98
99    fn parse_expr(&mut self) -> Result<Value, SemaError> {
100        // Bound recursion depth on the single common entry point: every nested
101        // form (list/vector/map/short-lambda elements) recurses through here.
102        self.depth += 1;
103        if self.depth > MAX_PARSE_DEPTH {
104            self.depth -= 1;
105            return Err(SemaError::Reader {
106                message: format!("input nested too deeply (limit {MAX_PARSE_DEPTH})"),
107                span: self.span(),
108            }
109            .with_hint("reduce nesting depth"));
110        }
111        let result = self.parse_expr_inner();
112        self.depth -= 1;
113        result
114    }
115
116    fn parse_expr_inner(&mut self) -> Result<Value, SemaError> {
117        let span = self.span();
118        match self.peek() {
119            None => Err(SemaError::Reader {
120                message: "unexpected end of input".to_string(),
121                span,
122            }),
123            Some(Token::LParen) => self.parse_list(),
124            Some(Token::LBracket) => self.parse_vector(),
125            Some(Token::LBrace) => self.parse_map(),
126            Some(Token::Quote) => {
127                self.advance();
128                let inner = self.parse_expr().map_err(|_| {
129                    SemaError::Reader {
130                        message: "quote (') requires an expression after it".to_string(),
131                        span,
132                    }
133                    .with_hint("e.g. '(1 2 3) or 'foo")
134                })?;
135                self.make_list_with_span(vec![Value::symbol("quote"), inner], span)
136            }
137            Some(Token::Quasiquote) => {
138                self.advance();
139                let inner = self.parse_expr().map_err(|_| {
140                    SemaError::Reader {
141                        message: "quasiquote (`) requires an expression after it".to_string(),
142                        span,
143                    }
144                    .with_hint("e.g. `(list ,x)")
145                })?;
146                self.make_list_with_span(vec![Value::symbol("quasiquote"), inner], span)
147            }
148            Some(Token::Unquote) => {
149                self.advance();
150                let inner = self.parse_expr().map_err(|_| {
151                    SemaError::Reader {
152                        message: "unquote (,) requires an expression after it".to_string(),
153                        span,
154                    }
155                    .with_hint("use inside quasiquote, e.g. `(list ,x)")
156                })?;
157                self.make_list_with_span(vec![Value::symbol("unquote"), inner], span)
158            }
159            Some(Token::UnquoteSplice) => {
160                self.advance();
161                let inner = self.parse_expr().map_err(|_| {
162                    SemaError::Reader {
163                        message: "unquote-splicing (,@) requires an expression after it"
164                            .to_string(),
165                        span,
166                    }
167                    .with_hint("use inside quasiquote, e.g. `(list ,@xs)")
168                })?;
169                self.make_list_with_span(vec![Value::symbol("unquote-splicing"), inner], span)
170            }
171            Some(Token::Deref) => {
172                self.advance();
173                let inner = self.parse_expr().map_err(|_| {
174                    SemaError::Reader {
175                        message: "deref (@) requires an expression after it".to_string(),
176                        span,
177                    }
178                    .with_hint("e.g. @x or @(atom)")
179                })?;
180                self.make_list_with_span(vec![Value::symbol("deref"), inner], span)
181            }
182            Some(Token::BytevectorStart) => self.parse_bytevector(),
183            Some(Token::ShortLambdaStart) => self.parse_short_lambda(),
184            Some(_) => {
185                let val = self.parse_atom()?;
186                if let Some(name) = val.as_symbol() {
187                    self.symbol_spans.push((name, span));
188                }
189                Ok(val)
190            }
191        }
192    }
193
194    fn make_list_with_span(&mut self, items: Vec<Value>, span: Span) -> Result<Value, SemaError> {
195        let rc = Rc::new(items);
196        let ptr = Rc::as_ptr(&rc) as usize;
197        self.span_map.insert(ptr, span);
198        Ok(Value::list_from_rc(rc))
199    }
200
201    /// Get the span of the previously consumed token (the one at pos-1).
202    fn prev_span(&self) -> Span {
203        if self.pos > 0 {
204            self.tokens[self.pos - 1].span
205        } else {
206            Span::point(0, 0)
207        }
208    }
209
210    /// Reject a foreign closing bracket inside `parse_list`/`parse_vector`/
211    /// `parse_map` with the standard mismatched-bracket message and the
212    /// per-opener hint. `foreign` is the closers that do not match this
213    /// opener (e.g. `]` and `}` inside a `(` list).
214    fn check_no_foreign_closer(
215        &self,
216        foreign: &[Token],
217        open_char: &str,
218        close_char: &str,
219        hint: &str,
220    ) -> Result<(), SemaError> {
221        match self.peek() {
222            Some(tok) if foreign.contains(tok) => {
223                let found = match tok {
224                    Token::RParen => "`)`",
225                    Token::RBracket => "`]`",
226                    Token::RBrace => "`}`",
227                    _ => unreachable!("foreign closers are only ) ] }}"),
228                };
229                Err(SemaError::Reader {
230                    message: format!(
231                        "mismatched bracket: expected `{close_char}` to close `{open_char}`, found {found}"
232                    ),
233                    span: self.span(),
234                }
235                .with_hint(hint))
236            }
237            _ => Ok(()),
238        }
239    }
240
241    fn parse_list(&mut self) -> Result<Value, SemaError> {
242        let open_span = self.span();
243        self.expect(&Token::LParen)?;
244        let mut items = Vec::new();
245        while self.peek() != Some(&Token::RParen) {
246            if self.peek().is_none() {
247                return Err(SemaError::Reader {
248                    message: "unterminated list".to_string(),
249                    span: open_span,
250                }
251                .with_hint("add a closing `)`"));
252            }
253            self.check_no_foreign_closer(
254                &[Token::RBracket, Token::RBrace],
255                "(",
256                ")",
257                "this list was opened with `(` — close it with `)`",
258            )?;
259            // Handle dotted pairs: (a . b)
260            if self.peek() == Some(&Token::Dot) {
261                self.advance(); // skip dot
262                let cdr = self.parse_expr()?;
263                self.expect(&Token::RParen)?;
264                let close = self.prev_span();
265                items.push(Value::symbol("."));
266                items.push(cdr);
267                return self.make_list_with_span(items, open_span.to(&close));
268            }
269            items.push(self.parse_expr()?);
270        }
271        self.expect(&Token::RParen)?;
272        let close = self.prev_span();
273        self.make_list_with_span(items, open_span.to(&close))
274    }
275
276    fn parse_vector(&mut self) -> Result<Value, SemaError> {
277        let open_span = self.span();
278        self.expect(&Token::LBracket)?;
279        let mut items = Vec::new();
280        while self.peek() != Some(&Token::RBracket) {
281            if self.peek().is_none() {
282                return Err(SemaError::Reader {
283                    message: "unterminated vector".to_string(),
284                    span: open_span,
285                }
286                .with_hint("add a closing `]`"));
287            }
288            self.check_no_foreign_closer(
289                &[Token::RParen, Token::RBrace],
290                "[",
291                "]",
292                "this vector was opened with `[` — close it with `]`",
293            )?;
294            items.push(self.parse_expr()?);
295        }
296        self.expect(&Token::RBracket)?;
297        let close = self.prev_span();
298        let rc = Rc::new(items);
299        let ptr = Rc::as_ptr(&rc) as usize;
300        self.span_map.insert(ptr, open_span.to(&close));
301        Ok(Value::vector_from_rc(rc))
302    }
303
304    fn parse_map(&mut self) -> Result<Value, SemaError> {
305        let open_span = self.span();
306        self.expect(&Token::LBrace)?;
307        let mut map = BTreeMap::new();
308        while self.peek() != Some(&Token::RBrace) {
309            if self.peek().is_none() {
310                return Err(SemaError::Reader {
311                    message: "unterminated map".to_string(),
312                    span: open_span,
313                }
314                .with_hint("add a closing `}`"));
315            }
316            self.check_no_foreign_closer(
317                &[Token::RParen, Token::RBracket],
318                "{",
319                "}",
320                "this map was opened with `{` — close it with `}`",
321            )?;
322            let key = self.parse_expr()?;
323            if self.peek() == Some(&Token::RBrace) || self.peek().is_none() {
324                return Err(SemaError::Reader {
325                    message: "map literal must have even number of forms".to_string(),
326                    span: self.span(),
327                });
328            }
329            let val = self.parse_expr()?;
330            map.insert(key, val);
331        }
332        self.expect(&Token::RBrace)?;
333        Ok(Value::map(map))
334    }
335
336    fn parse_bytevector(&mut self) -> Result<Value, SemaError> {
337        let open_span = self.span();
338        self.advance(); // consume BytevectorStart token
339        let mut bytes = Vec::new();
340        while self.peek() != Some(&Token::RParen) {
341            if self.peek().is_none() {
342                return Err(SemaError::Reader {
343                    message: "unterminated bytevector".to_string(),
344                    span: open_span,
345                }
346                .with_hint("add a closing `)`"));
347            }
348            let span = self.span();
349            match self.peek() {
350                Some(Token::Int(n)) => {
351                    let n = *n;
352                    self.advance();
353                    if !(0..=255).contains(&n) {
354                        return Err(SemaError::Reader {
355                            message: format!("#u8(...): byte value {n} out of range 0..255"),
356                            span,
357                        });
358                    }
359                    bytes.push(n as u8);
360                }
361                _ => {
362                    return Err(SemaError::Reader {
363                        message: "#u8(...): expected integer byte value".to_string(),
364                        span,
365                    });
366                }
367            }
368        }
369        self.expect(&Token::RParen)?;
370        Ok(Value::bytevector(bytes))
371    }
372
373    fn parse_short_lambda(&mut self) -> Result<Value, SemaError> {
374        let open_span = self.span();
375        if self.short_lambda_depth > 0 {
376            return Err(SemaError::Reader {
377                message: "nested short lambdas are not allowed".to_string(),
378                span: open_span,
379            }
380            .with_hint("use a regular `lambda` or `fn` for the inner function"));
381        }
382        self.advance(); // consume ShortLambdaStart
383        self.short_lambda_depth += 1;
384        let mut body_items = Vec::new();
385        while self.peek() != Some(&Token::RParen) {
386            if self.peek().is_none() {
387                self.short_lambda_depth -= 1;
388                return Err(SemaError::Reader {
389                    message: "unterminated short lambda #(...)".to_string(),
390                    span: open_span,
391                }
392                .with_hint("add a closing `)`"));
393            }
394            match self.parse_expr() {
395                Ok(expr) => body_items.push(expr),
396                Err(e) => {
397                    self.short_lambda_depth -= 1;
398                    return Err(e);
399                }
400            }
401        }
402        self.short_lambda_depth -= 1;
403        self.expect(&Token::RParen)?;
404
405        // Build the body as a single list form: (fn-name arg1 arg2 ...)
406        let body = Value::list(body_items);
407
408        // Scan body for % / %1 / %2 etc., rewrite % → %1
409        let mut max_arg: usize = 0;
410        let mut has_rest = false;
411        let body = rewrite_percent_args(&body, &mut max_arg, &mut has_rest);
412
413        // The parameter list below materializes one interned symbol per slot up
414        // to the highest placeholder, so an unbounded `%N` lets a 12-byte input
415        // (`#(%9999999)`) allocate millions of symbols — reachable from any
416        // untrusted read (a .sema file, `(read s)`, the LSP, the playground).
417        // 255 is far beyond any legitimate short lambda and keeps the
418        // allocation trivial.
419        if max_arg > MAX_SHORT_LAMBDA_ARG {
420            return Err(SemaError::Reader {
421                message: format!(
422                    "short lambda placeholder %{max_arg} exceeds the maximum of {MAX_SHORT_LAMBDA_ARG}"
423                ),
424                span: open_span,
425            }
426            .with_hint("use a regular `lambda` with named parameters instead"));
427        }
428
429        // Build parameter list
430        let mut params: Vec<Value> = if max_arg == 0 {
431            vec![]
432        } else {
433            (1..=max_arg)
434                .map(|n| Value::symbol(&format!("%{}", n)))
435                .collect()
436        };
437
438        if has_rest {
439            params.push(Value::symbol("."));
440            params.push(Value::symbol("%&"));
441        }
442
443        Ok(Value::list(vec![
444            Value::symbol("lambda"),
445            Value::list(params),
446            body,
447        ]))
448    }
449
450    /// After a parse error, skip tokens until we reach a position that
451    /// could plausibly start a new top-level expression (depth-0 open bracket,
452    /// quote, or atom). This enables error recovery in `read_many_recover`.
453    fn recover_to_next_expr(&mut self) {
454        let mut depth: usize = 0;
455        while let Some(tok) = self.peek() {
456            match tok {
457                // Opening brackets increase depth
458                Token::LParen
459                | Token::LBracket
460                | Token::LBrace
461                | Token::ShortLambdaStart
462                | Token::BytevectorStart => {
463                    if depth == 0 {
464                        // This could start a new top-level form — stop here
465                        return;
466                    }
467                    self.advance();
468                    depth += 1;
469                }
470                // Closing brackets decrease depth
471                Token::RParen | Token::RBracket | Token::RBrace => {
472                    if depth == 0 {
473                        // Stray closer at top level — stop and let parse_expr report it
474                        return;
475                    }
476                    self.advance();
477                    depth -= 1;
478                }
479                // Quote-like prefixes at depth 0 could start a new form
480                Token::Quote
481                | Token::Quasiquote
482                | Token::Unquote
483                | Token::UnquoteSplice
484                | Token::Deref => {
485                    if depth == 0 {
486                        return;
487                    }
488                    self.advance();
489                }
490                // Atoms at depth 0 could be a top-level expression
491                _ => {
492                    if depth == 0 {
493                        return;
494                    }
495                    self.advance();
496                }
497            }
498        }
499    }
500
501    fn parse_atom(&mut self) -> Result<Value, SemaError> {
502        let span = self.span();
503        match self.advance() {
504            Some(SpannedToken {
505                token: Token::Int(n),
506                ..
507            }) => Ok(Value::int(*n)),
508            Some(SpannedToken {
509                token: Token::BigInt(n),
510                ..
511            }) => Ok(Value::from_bigint(n.clone())),
512            Some(SpannedToken {
513                token: Token::Rational(r),
514                ..
515            }) => Ok(Value::rational(r.clone())),
516            Some(SpannedToken {
517                token: Token::Complex(re, im),
518                ..
519            }) => Ok(Value::complex(re.clone(), im.clone())),
520            Some(SpannedToken {
521                token: Token::Float(f),
522                ..
523            }) => Ok(Value::float(*f)),
524            Some(SpannedToken {
525                token: Token::String(s),
526                ..
527            }) => Ok(Value::string(s)),
528            Some(SpannedToken {
529                token: Token::Regex(s),
530                ..
531            }) => Ok(Value::string(s)),
532            Some(SpannedToken {
533                token: Token::Symbol(s),
534                ..
535            }) => {
536                if s == "nil" {
537                    Ok(Value::nil())
538                } else {
539                    Ok(Value::symbol(s))
540                }
541            }
542            Some(SpannedToken {
543                token: Token::Keyword(s),
544                ..
545            }) => Ok(Value::keyword(s)),
546            Some(SpannedToken {
547                token: Token::Bool(b),
548                ..
549            }) => Ok(Value::bool(*b)),
550            Some(SpannedToken {
551                token: Token::Char(c),
552                ..
553            }) => Ok(Value::char(*c)),
554            Some(SpannedToken {
555                token: Token::FString(parts),
556                ..
557            }) => {
558                let parts = parts.clone();
559                let mut items = vec![Value::symbol("__vm-str")];
560                for part in &parts {
561                    match part {
562                        FStringPart::Literal(s) => {
563                            if !s.is_empty() {
564                                items.push(Value::string(s));
565                            }
566                        }
567                        FStringPart::Expr(src) => {
568                            // Parse the interpolation as a sub-expression. Thread
569                            // the current depth through so nested f-strings can't
570                            // bypass MAX_PARSE_DEPTH by starting a fresh parser at
571                            // depth 0 (READ-1).
572                            let sub_tokens = tokenize(src)?;
573                            let mut sub = Parser::new(sub_tokens);
574                            sub.depth = self.depth;
575                            if sub.peek().is_none() {
576                                return Err(SemaError::Reader {
577                                    message: "f-string interpolation is empty".to_string(),
578                                    span,
579                                }
580                                .with_hint("put an expression inside ${...}"));
581                            }
582                            let val = sub.parse_expr()?;
583                            // An interpolation must hold exactly one expression;
584                            // silently dropping extra forms hides bugs (READ-2).
585                            if sub.peek().is_some() {
586                                return Err(SemaError::Reader {
587                                    message:
588                                        "f-string interpolation must contain exactly one expression"
589                                            .to_string(),
590                                    span,
591                                }
592                                .with_hint("wrap multiple forms, e.g. ${(do a b)}"));
593                            }
594                            items.push(val);
595                        }
596                    }
597                }
598                Ok(Value::list(items))
599            }
600            Some(t) => {
601                let (name, hint) = match &t.token {
602                    Token::RParen => (
603                        "unexpected closing `)`",
604                        Some("no matching opening parenthesis"),
605                    ),
606                    Token::RBracket => (
607                        "unexpected closing `]`",
608                        Some("no matching opening bracket"),
609                    ),
610                    Token::RBrace => ("unexpected closing `}`", Some("no matching opening brace")),
611                    Token::Dot => (
612                        "unexpected `.`",
613                        Some("dots are used in pair notation, e.g. (a . b)"),
614                    ),
615                    _ => ("unexpected token", None),
616                };
617                let err = SemaError::Reader {
618                    message: name.to_string(),
619                    span,
620                };
621                Err(if let Some(h) = hint {
622                    err.with_hint(h)
623                } else {
624                    err
625                })
626            }
627            None => Err(SemaError::Reader {
628                message: "unexpected end of input".to_string(),
629                span,
630            }),
631        }
632    }
633}
634
635fn token_display(tok: &Token) -> &'static str {
636    match tok {
637        Token::LParen => "(",
638        Token::RParen => ")",
639        Token::LBracket => "[",
640        Token::RBracket => "]",
641        Token::LBrace => "{",
642        Token::RBrace => "}",
643        Token::Quote => "'",
644        Token::Quasiquote => "`",
645        Token::Unquote => ",",
646        Token::UnquoteSplice => ",@",
647        Token::Deref => "@",
648        Token::Dot => ".",
649        Token::BytevectorStart => "#u8(",
650        Token::Int(_) => "integer",
651        Token::BigInt(_) => "integer",
652        Token::Rational(_) => "rational",
653        Token::Complex(_, _) => "complex",
654        Token::Float(_) => "float",
655        Token::String(_) => "string",
656        Token::Symbol(_) => "symbol",
657        Token::Keyword(_) => "keyword",
658        Token::Bool(_) => "boolean",
659        Token::Char(_) => "character",
660        Token::FString(_) => "f-string",
661        Token::ShortLambdaStart => "#(",
662        Token::Comment(_) => "comment",
663        Token::Newline => "newline",
664        Token::Regex(_) => "regex",
665    }
666}
667
668/// Recursively scan a Value AST for `%`, `%1`, `%2`, etc. symbols.
669/// Rewrites bare `%` to `%1`. Tracks the highest numbered arg in `max_arg`.
670/// Recurses into nested `(lambda ...)` / `(fn ...)` bodies so their placeholders bind to the enclosing `#()`. Sets `has_rest` when `%&` appears.
671fn rewrite_percent_args(expr: &Value, max_arg: &mut usize, has_rest: &mut bool) -> Value {
672    match expr.view() {
673        ValueView::Symbol(spur) => {
674            let name = resolve(spur);
675            if name == "%" {
676                *max_arg = (*max_arg).max(1);
677                Value::symbol("%1")
678            } else if name == "%&" {
679                *has_rest = true;
680                expr.clone()
681            } else if let Some(rest) = name.strip_prefix('%') {
682                if let Ok(n) = rest.parse::<usize>() {
683                    if n > 0 {
684                        *max_arg = (*max_arg).max(n);
685                    }
686                }
687                expr.clone()
688            } else {
689                expr.clone()
690            }
691        }
692        ValueView::List(items) => {
693            let new_items: Vec<Value> = items
694                .iter()
695                .map(|item| rewrite_percent_args(item, max_arg, has_rest))
696                .collect();
697            Value::list(new_items)
698        }
699        ValueView::Vector(items) => {
700            let new_items: Vec<Value> = items
701                .iter()
702                .map(|item| rewrite_percent_args(item, max_arg, has_rest))
703                .collect();
704            Value::vector(new_items)
705        }
706        // Map literals are scanned too. Without this arm `%` inside `{...}` was
707        // neither rewritten to `%1` nor counted, so `#({:n %})` built a
708        // zero-argument lambda and calling it failed with a misleading arity
709        // error that never mentioned the `%`. Keys as well as values: `{% 1}`
710        // is as legitimate as `{:k %}`.
711        ValueView::Map(entries) => {
712            let rewritten: std::collections::BTreeMap<Value, Value> = entries
713                .iter()
714                .map(|(k, v)| {
715                    (
716                        rewrite_percent_args(k, max_arg, has_rest),
717                        rewrite_percent_args(v, max_arg, has_rest),
718                    )
719                })
720                .collect();
721            Value::map(rewritten)
722        }
723        _ => expr.clone(),
724    }
725}
726
727/// Read a single s-expression from a string.
728pub fn read(input: &str) -> Result<Value, SemaError> {
729    let tokens = tokenize(input)?;
730    let mut parser = Parser::new(tokens);
731    if parser.peek().is_none() {
732        return Ok(Value::nil());
733    }
734    parser.parse_expr()
735}
736
737/// Read all s-expressions from a string.
738pub fn read_many(input: &str) -> Result<Vec<Value>, SemaError> {
739    let tokens = tokenize(input)?;
740    let mut parser = Parser::new(tokens);
741    let mut exprs = Vec::new();
742    while parser.peek().is_some() {
743        exprs.push(parser.parse_expr()?);
744    }
745    Ok(exprs)
746}
747
748/// Read all s-expressions and return the accumulated span map.
749pub fn read_many_with_spans(input: &str) -> Result<(Vec<Value>, SpanMap), SemaError> {
750    let tokens = tokenize(input)?;
751    let mut parser = Parser::new(tokens);
752    let mut exprs = Vec::new();
753    while parser.peek().is_some() {
754        exprs.push(parser.parse_expr()?);
755    }
756    Ok((exprs, parser.span_map))
757}
758
759/// Read all s-expressions and return spans for both compound expressions and individual symbols.
760/// Symbol spans enable precise go-to-definition (jumping to the name, not the whole form).
761#[allow(clippy::type_complexity)]
762pub fn read_many_with_symbol_spans(
763    input: &str,
764) -> Result<(Vec<Value>, SpanMap, Vec<(String, Span)>), SemaError> {
765    let tokens = tokenize(input)?;
766    let mut parser = Parser::new(tokens);
767    let mut exprs = Vec::new();
768    while parser.peek().is_some() {
769        exprs.push(parser.parse_expr()?);
770    }
771    Ok((exprs, parser.span_map, parser.symbol_spans))
772}
773
774/// Read all s-expressions with error recovery.
775/// On parse errors, skips to the next top-level form and continues.
776/// Returns (successfully parsed forms, span map, collected errors).
777/// Tokenizer errors are returned as a single error with no parsed forms.
778#[allow(clippy::type_complexity)]
779pub fn read_many_with_spans_recover(
780    input: &str,
781) -> (Vec<Value>, SpanMap, Vec<(String, Span)>, Vec<SemaError>) {
782    let tokens = match tokenize(input) {
783        Ok(t) => t,
784        Err(e) => return (vec![], SpanMap::new(), vec![], vec![e]),
785    };
786    let mut parser = Parser::new(tokens);
787    let mut exprs = Vec::new();
788    let mut errors = Vec::new();
789    while parser.peek().is_some() {
790        match parser.parse_expr() {
791            Ok(expr) => exprs.push(expr),
792            Err(err) => {
793                errors.push(err);
794                parser.recover_to_next_expr();
795            }
796        }
797    }
798    (exprs, parser.span_map, parser.symbol_spans, errors)
799}
800
801#[cfg(test)]
802#[allow(clippy::approx_constant)]
803mod tests {
804    use super::*;
805
806    #[test]
807    fn test_read_int() {
808        assert_eq!(read("42").unwrap(), Value::int(42));
809    }
810
811    #[test]
812    fn deeply_nested_input_errors_instead_of_overflowing() {
813        // Untrusted input with thousands of levels of nesting must return a
814        // reader error rather than recurse to a stack overflow. Run on a large
815        // stack so the result reflects the depth-limit check, not the small
816        // default test-thread stack (which would SIGSEGV either way).
817        let result = std::thread::Builder::new()
818            .stack_size(16 * 1024 * 1024)
819            .spawn(|| {
820                let depth = 3000;
821                let src = format!("{}{}", "[".repeat(depth), "]".repeat(depth));
822                read(&src).is_err()
823            })
824            .unwrap()
825            .join()
826            .expect("parser must not overflow the stack on deeply nested input");
827        assert!(
828            result,
829            "expected a depth-limit error for deeply nested input"
830        );
831    }
832
833    #[test]
834    fn test_read_negative_int() {
835        assert_eq!(read("-7").unwrap(), Value::int(-7));
836    }
837
838    #[test]
839    fn test_read_float() {
840        assert_eq!(read("3.14").unwrap(), Value::float(3.14));
841    }
842
843    #[test]
844    fn test_read_string() {
845        assert_eq!(read("\"hello\"").unwrap(), Value::string("hello"));
846    }
847
848    #[test]
849    fn test_read_symbol() {
850        assert_eq!(read("foo").unwrap(), Value::symbol("foo"));
851    }
852
853    #[test]
854    fn test_read_keyword() {
855        assert_eq!(read(":bar").unwrap(), Value::keyword("bar"));
856    }
857
858    #[test]
859    fn test_read_bool() {
860        assert_eq!(read("#t").unwrap(), Value::bool(true));
861        assert_eq!(read("#f").unwrap(), Value::bool(false));
862    }
863
864    #[test]
865    fn test_read_list() {
866        let result = read("(+ 1 2)").unwrap();
867        assert_eq!(
868            result,
869            Value::list(vec![Value::symbol("+"), Value::int(1), Value::int(2)])
870        );
871    }
872
873    #[test]
874    fn test_read_nested_list() {
875        let result = read("(* (+ 1 2) 3)").unwrap();
876        assert_eq!(
877            result,
878            Value::list(vec![
879                Value::symbol("*"),
880                Value::list(vec![Value::symbol("+"), Value::int(1), Value::int(2)]),
881                Value::int(3)
882            ])
883        );
884    }
885
886    #[test]
887    fn test_read_vector() {
888        let result = read("[1 2 3]").unwrap();
889        assert_eq!(
890            result,
891            Value::vector(vec![Value::int(1), Value::int(2), Value::int(3)])
892        );
893    }
894
895    #[test]
896    fn test_read_map() {
897        let result = read("{:a 1 :b 2}").unwrap();
898        let mut expected = BTreeMap::new();
899        expected.insert(Value::keyword("a"), Value::int(1));
900        expected.insert(Value::keyword("b"), Value::int(2));
901        assert_eq!(result, Value::map(expected));
902    }
903
904    #[test]
905    fn test_read_quote() {
906        let result = read("'foo").unwrap();
907        assert_eq!(
908            result,
909            Value::list(vec![Value::symbol("quote"), Value::symbol("foo")])
910        );
911    }
912
913    #[test]
914    fn test_read_quasiquote() {
915        let result = read("`(a ,b ,@c)").unwrap();
916        assert_eq!(
917            result,
918            Value::list(vec![
919                Value::symbol("quasiquote"),
920                Value::list(vec![
921                    Value::symbol("a"),
922                    Value::list(vec![Value::symbol("unquote"), Value::symbol("b")]),
923                    Value::list(vec![Value::symbol("unquote-splicing"), Value::symbol("c")]),
924                ])
925            ])
926        );
927    }
928
929    #[test]
930    fn test_read_nil() {
931        assert_eq!(read("nil").unwrap(), Value::nil());
932    }
933
934    #[test]
935    fn test_read_many_exprs() {
936        let results = read_many("1 2 3").unwrap();
937        assert_eq!(results, vec![Value::int(1), Value::int(2), Value::int(3)]);
938    }
939
940    #[test]
941    fn test_comments() {
942        let result = read_many("; comment\n(+ 1 2)").unwrap();
943        assert_eq!(result.len(), 1);
944    }
945
946    #[test]
947    fn test_read_zero() {
948        assert_eq!(read("0").unwrap(), Value::int(0));
949    }
950
951    #[test]
952    fn test_read_negative_zero() {
953        assert_eq!(read("-0").unwrap(), Value::int(0));
954    }
955
956    #[test]
957    fn test_read_leading_zeros() {
958        assert_eq!(read("007").unwrap(), Value::int(7));
959    }
960
961    #[test]
962    fn test_read_large_int() {
963        assert_eq!(read("9999999999999").unwrap(), Value::int(9999999999999));
964    }
965
966    #[test]
967    fn test_read_int_overflow() {
968        // i64::MAX + 1 lexes as a bignum rather than erroring or silently
969        // wrapping.
970        let v = read("9999999999999999999999").unwrap();
971        assert!(v.is_bigint());
972        assert_eq!(v.to_string(), "9999999999999999999999");
973    }
974
975    #[test]
976    fn test_read_negative_float() {
977        assert_eq!(read("-2.5").unwrap(), Value::float(-2.5));
978    }
979
980    #[test]
981    fn test_read_float_leading_zero() {
982        assert_eq!(read("0.5").unwrap(), Value::float(0.5));
983    }
984
985    #[test]
986    fn test_read_minus_is_symbol() {
987        // Bare `-` should be a symbol (subtraction operator), not a number
988        assert_eq!(read("-").unwrap(), Value::symbol("-"));
989    }
990
991    #[test]
992    fn test_read_minus_in_list() {
993        // `(- 3)` should parse as call to `-` with arg 3
994        let result = read("(- 3)").unwrap();
995        assert_eq!(result, Value::list(vec![Value::symbol("-"), Value::int(3)]));
996    }
997
998    #[test]
999    fn test_read_negative_in_list() {
1000        // `(-3)` should parse as list containing -3
1001        let result = read("(-3)").unwrap();
1002        assert_eq!(result, Value::list(vec![Value::int(-3)]));
1003    }
1004
1005    #[test]
1006    fn test_read_empty_string() {
1007        assert_eq!(read(r#""""#).unwrap(), Value::string(""));
1008    }
1009
1010    #[test]
1011    fn test_read_string_with_escapes() {
1012        assert_eq!(
1013            read(r#""\n\t\r\\\"" "#).unwrap(),
1014            Value::string("\n\t\r\\\"")
1015        );
1016    }
1017
1018    #[test]
1019    fn test_read_string_unknown_escape() {
1020        // Unknown escape sequences are preserved literally
1021        assert_eq!(read(r#""\z""#).unwrap(), Value::string("\\z"));
1022    }
1023
1024    #[test]
1025    fn test_read_string_with_newline() {
1026        assert_eq!(
1027            read("\"line1\nline2\"").unwrap(),
1028            Value::string("line1\nline2")
1029        );
1030    }
1031
1032    #[test]
1033    fn test_read_unterminated_string() {
1034        assert!(read("\"hello").is_err());
1035    }
1036
1037    #[test]
1038    fn test_read_string_escaped_quote_at_end() {
1039        // `"test\"` — the backslash escapes the quote, string is unterminated
1040        assert!(read(r#""test\""#).is_err());
1041    }
1042
1043    #[test]
1044    fn test_read_string_with_unicode() {
1045        assert_eq!(read("\"héllo\"").unwrap(), Value::string("héllo"));
1046        assert_eq!(read("\"日本語\"").unwrap(), Value::string("日本語"));
1047        assert_eq!(read("\"🎉\"").unwrap(), Value::string("🎉"));
1048    }
1049
1050    #[test]
1051    fn test_read_string_with_parens() {
1052        assert_eq!(read("\"(+ 1 2)\"").unwrap(), Value::string("(+ 1 2)"));
1053    }
1054
1055    #[test]
1056    fn test_read_operator_symbols() {
1057        assert_eq!(read("+").unwrap(), Value::symbol("+"));
1058        assert_eq!(read("*").unwrap(), Value::symbol("*"));
1059        assert_eq!(read("/").unwrap(), Value::symbol("/"));
1060        assert_eq!(read("<=").unwrap(), Value::symbol("<="));
1061        assert_eq!(read(">=").unwrap(), Value::symbol(">="));
1062    }
1063
1064    #[test]
1065    fn test_read_predicate_symbols() {
1066        assert_eq!(read("null?").unwrap(), Value::symbol("null?"));
1067        assert_eq!(read("list?").unwrap(), Value::symbol("list?"));
1068    }
1069
1070    #[test]
1071    fn test_read_arrow_symbols() {
1072        assert_eq!(
1073            read("string->symbol").unwrap(),
1074            Value::symbol("string->symbol")
1075        );
1076    }
1077
1078    #[test]
1079    fn test_read_namespaced_symbols() {
1080        assert_eq!(read("file/read").unwrap(), Value::symbol("file/read"));
1081        assert_eq!(read("http/get").unwrap(), Value::symbol("http/get"));
1082    }
1083
1084    #[test]
1085    fn test_read_true_false_as_bool() {
1086        assert_eq!(read("true").unwrap(), Value::bool(true));
1087        assert_eq!(read("false").unwrap(), Value::bool(false));
1088    }
1089
1090    #[test]
1091    fn test_read_bare_colon_error() {
1092        // `:` alone without a name should error
1093        assert!(read(":").is_err());
1094    }
1095
1096    #[test]
1097    fn test_read_keyword_with_numbers() {
1098        assert_eq!(read(":foo123").unwrap(), Value::keyword("foo123"));
1099    }
1100
1101    #[test]
1102    fn test_read_keyword_with_hyphens() {
1103        assert_eq!(read(":max-turns").unwrap(), Value::keyword("max-turns"));
1104    }
1105
1106    #[test]
1107    fn test_read_hash_invalid() {
1108        assert!(read("#x").is_err());
1109        assert!(read("#").is_err());
1110    }
1111
1112    #[test]
1113    fn test_read_empty() {
1114        assert_eq!(read("").unwrap(), Value::nil());
1115    }
1116
1117    #[test]
1118    fn test_read_whitespace_only() {
1119        assert_eq!(read("   \n\t  ").unwrap(), Value::nil());
1120    }
1121
1122    #[test]
1123    fn test_read_many_empty() {
1124        assert_eq!(read_many("").unwrap(), vec![]);
1125    }
1126
1127    #[test]
1128    fn test_read_many_whitespace_only() {
1129        assert_eq!(read_many("  \n  ").unwrap(), vec![]);
1130    }
1131
1132    #[test]
1133    fn test_read_comment_only() {
1134        assert_eq!(read_many("; just a comment").unwrap(), vec![]);
1135    }
1136
1137    #[test]
1138    fn test_read_empty_list() {
1139        assert_eq!(read("()").unwrap(), Value::list(vec![]));
1140    }
1141
1142    #[test]
1143    fn test_read_deeply_nested() {
1144        let result = read("((((42))))").unwrap();
1145        assert_eq!(
1146            result,
1147            Value::list(vec![Value::list(vec![Value::list(vec![Value::list(
1148                vec![Value::int(42)]
1149            )])])])
1150        );
1151    }
1152
1153    #[test]
1154    fn test_read_unterminated_list() {
1155        assert!(read("(1 2").is_err());
1156    }
1157
1158    #[test]
1159    fn test_read_extra_rparen() {
1160        // `read` only reads one expr, so extra `)` is just ignored (not consumed)
1161        // But `read_many` should fail since `)` is not a valid expr start
1162        let result = read("42").unwrap();
1163        assert_eq!(result, Value::int(42));
1164    }
1165
1166    #[test]
1167    fn test_read_dotted_pair() {
1168        let result = read("(a . b)").unwrap();
1169        assert_eq!(
1170            result,
1171            Value::list(vec![
1172                Value::symbol("a"),
1173                Value::symbol("."),
1174                Value::symbol("b")
1175            ])
1176        );
1177    }
1178
1179    #[test]
1180    fn test_read_empty_vector() {
1181        assert_eq!(read("[]").unwrap(), Value::vector(vec![]));
1182    }
1183
1184    #[test]
1185    fn test_read_unterminated_vector() {
1186        assert!(read("[1 2").is_err());
1187    }
1188
1189    #[test]
1190    fn test_read_empty_map() {
1191        assert_eq!(read("{}").unwrap(), Value::map(BTreeMap::new()));
1192    }
1193
1194    #[test]
1195    fn test_read_unterminated_map() {
1196        assert!(read("{:a 1").is_err());
1197    }
1198
1199    #[test]
1200    fn test_read_map_odd_elements() {
1201        assert!(read("{:a 1 :b}").is_err());
1202    }
1203
1204    #[test]
1205    fn test_read_map_duplicate_keys() {
1206        // Later key wins (BTreeMap insert replaces)
1207        let result = read("{:a 1 :a 2}").unwrap();
1208        let mut expected = BTreeMap::new();
1209        expected.insert(Value::keyword("a"), Value::int(2));
1210        assert_eq!(result, Value::map(expected));
1211    }
1212
1213    #[test]
1214    fn test_read_nested_quote() {
1215        let result = read("''foo").unwrap();
1216        assert_eq!(
1217            result,
1218            Value::list(vec![
1219                Value::symbol("quote"),
1220                Value::list(vec![Value::symbol("quote"), Value::symbol("foo")])
1221            ])
1222        );
1223    }
1224
1225    #[test]
1226    fn test_read_quote_list() {
1227        let result = read("'(1 2 3)").unwrap();
1228        assert_eq!(
1229            result,
1230            Value::list(vec![
1231                Value::symbol("quote"),
1232                Value::list(vec![Value::int(1), Value::int(2), Value::int(3)])
1233            ])
1234        );
1235    }
1236
1237    #[test]
1238    fn test_read_quote_at_eof() {
1239        assert!(read("'").is_err());
1240    }
1241
1242    #[test]
1243    fn test_read_unquote_at_eof() {
1244        assert!(read(",").is_err());
1245    }
1246
1247    #[test]
1248    fn test_read_unquote_splice_at_eof() {
1249        assert!(read(",@").is_err());
1250    }
1251
1252    #[test]
1253    fn test_read_quasiquote_at_eof() {
1254        assert!(read("`").is_err());
1255    }
1256
1257    #[test]
1258    fn test_read_deref_at_eof() {
1259        assert!(read("@").is_err());
1260    }
1261
1262    #[test]
1263    fn test_read_deref_symbol() {
1264        let result = read("@x").unwrap();
1265        assert_eq!(
1266            result,
1267            Value::list(vec![Value::symbol("deref"), Value::symbol("x")])
1268        );
1269    }
1270
1271    #[test]
1272    fn test_read_deref_longer_symbol() {
1273        let result = read("@count").unwrap();
1274        assert_eq!(
1275            result,
1276            Value::list(vec![Value::symbol("deref"), Value::symbol("count")])
1277        );
1278    }
1279
1280    #[test]
1281    fn test_read_deref_list() {
1282        let result = read("@(+ 1 2)").unwrap();
1283        assert_eq!(
1284            result,
1285            Value::list(vec![
1286                Value::symbol("deref"),
1287                Value::list(vec![Value::symbol("+"), Value::int(1), Value::int(2),])
1288            ])
1289        );
1290    }
1291
1292    #[test]
1293    fn test_read_deref_in_list() {
1294        let result = read("(list @a @b)").unwrap();
1295        assert_eq!(
1296            result,
1297            Value::list(vec![
1298                Value::symbol("list"),
1299                Value::list(vec![Value::symbol("deref"), Value::symbol("a")]),
1300                Value::list(vec![Value::symbol("deref"), Value::symbol("b")]),
1301            ])
1302        );
1303    }
1304
1305    #[test]
1306    fn test_read_unquote_splice_not_affected_by_deref() {
1307        // Verify ,@ still works as unquote-splicing
1308        let result = read("`(a ,@b)").unwrap();
1309        assert_eq!(
1310            result,
1311            Value::list(vec![
1312                Value::symbol("quasiquote"),
1313                Value::list(vec![
1314                    Value::symbol("a"),
1315                    Value::list(vec![Value::symbol("unquote-splicing"), Value::symbol("b")]),
1316                ])
1317            ])
1318        );
1319    }
1320
1321    #[test]
1322    fn test_read_comment_after_expr() {
1323        assert_eq!(read_many("42 ; comment").unwrap(), vec![Value::int(42)]);
1324    }
1325
1326    #[test]
1327    fn test_read_multiple_comments() {
1328        let result = read_many("; first\n; second\n42").unwrap();
1329        assert_eq!(result, vec![Value::int(42)]);
1330    }
1331
1332    #[test]
1333    fn test_read_comment_no_newline() {
1334        // Comment at end of input without trailing newline
1335        assert_eq!(read_many("; comment").unwrap(), vec![]);
1336    }
1337
1338    #[test]
1339    fn test_read_crlf_line_endings() {
1340        let result = read_many("1\r\n2\r\n3").unwrap();
1341        assert_eq!(result, vec![Value::int(1), Value::int(2), Value::int(3)]);
1342    }
1343
1344    #[test]
1345    fn test_read_tabs_as_whitespace() {
1346        assert_eq!(
1347            read("(\t+\t1\t2\t)").unwrap(),
1348            Value::list(vec![Value::symbol("+"), Value::int(1), Value::int(2)])
1349        );
1350    }
1351
1352    #[test]
1353    fn test_read_mixed_collections() {
1354        // List containing vector and map
1355        let result = read("([1 2] {:a 3})").unwrap();
1356        let mut map = BTreeMap::new();
1357        map.insert(Value::keyword("a"), Value::int(3));
1358        assert_eq!(
1359            result,
1360            Value::list(vec![
1361                Value::vector(vec![Value::int(1), Value::int(2)]),
1362                Value::map(map)
1363            ])
1364        );
1365    }
1366
1367    #[test]
1368    fn test_read_many_mixed_types() {
1369        let result = read_many(r#"42 3.14 "hello" foo :bar #t nil"#).unwrap();
1370        assert_eq!(result.len(), 7);
1371        assert_eq!(result[0], Value::int(42));
1372        assert_eq!(result[1], Value::float(3.14));
1373        assert_eq!(result[2], Value::string("hello"));
1374        assert_eq!(result[3], Value::symbol("foo"));
1375        assert_eq!(result[4], Value::keyword("bar"));
1376        assert_eq!(result[5], Value::bool(true));
1377        assert_eq!(result[6], Value::nil());
1378    }
1379
1380    #[test]
1381    fn test_span_map_tracks_lists() {
1382        let (exprs, spans) = read_many_with_spans("(+ 1 2)").unwrap();
1383        assert_eq!(exprs.len(), 1);
1384        // The list should have a span entry
1385        let rc = exprs[0].as_list_rc().expect("expected list");
1386        let ptr = Rc::as_ptr(&rc) as usize;
1387        let span = spans.get(&ptr).expect("list should have span");
1388        assert_eq!(span.line, 1);
1389        assert_eq!(span.col, 1);
1390    }
1391
1392    #[test]
1393    fn test_span_map_multiline() {
1394        let (exprs, spans) = read_many_with_spans("(foo)\n(bar)").unwrap();
1395        assert_eq!(exprs.len(), 2);
1396        let rc = exprs[1].as_list_rc().expect("expected list");
1397        let ptr = Rc::as_ptr(&rc) as usize;
1398        let span = spans.get(&ptr).expect("second list should have span");
1399        assert_eq!(span.line, 2);
1400        assert_eq!(span.col, 1);
1401    }
1402
1403    #[test]
1404    fn test_read_unexpected_char() {
1405        assert!(read("$").is_err());
1406    }
1407
1408    #[test]
1409    fn test_read_char_literal() {
1410        assert_eq!(read("#\\a").unwrap(), Value::char('a'));
1411        assert_eq!(read("#\\Z").unwrap(), Value::char('Z'));
1412        assert_eq!(read("#\\0").unwrap(), Value::char('0'));
1413    }
1414
1415    #[test]
1416    fn test_read_char_named() {
1417        assert_eq!(read("#\\space").unwrap(), Value::char(' '));
1418        assert_eq!(read("#\\newline").unwrap(), Value::char('\n'));
1419        assert_eq!(read("#\\tab").unwrap(), Value::char('\t'));
1420        assert_eq!(read("#\\return").unwrap(), Value::char('\r'));
1421        assert_eq!(read("#\\nul").unwrap(), Value::char('\0'));
1422    }
1423
1424    #[test]
1425    fn test_read_char_special() {
1426        assert_eq!(read("#\\(").unwrap(), Value::char('('));
1427        assert_eq!(read("#\\)").unwrap(), Value::char(')'));
1428    }
1429
1430    #[test]
1431    fn test_read_char_in_list() {
1432        let result = read("(#\\a #\\b)").unwrap();
1433        assert_eq!(
1434            result,
1435            Value::list(vec![Value::char('a'), Value::char('b')])
1436        );
1437    }
1438
1439    #[test]
1440    fn test_read_char_unknown_name() {
1441        assert!(read("#\\foobar").is_err());
1442    }
1443
1444    #[test]
1445    fn test_read_char_eof() {
1446        assert!(read("#\\").is_err());
1447    }
1448
1449    #[test]
1450    fn test_read_bytevector_literal() {
1451        assert_eq!(
1452            read("#u8(1 2 3)").unwrap(),
1453            Value::bytevector(vec![1, 2, 3])
1454        );
1455    }
1456
1457    #[test]
1458    fn test_read_bytevector_empty() {
1459        assert_eq!(read("#u8()").unwrap(), Value::bytevector(vec![]));
1460    }
1461
1462    #[test]
1463    fn test_read_bytevector_single() {
1464        assert_eq!(read("#u8(255)").unwrap(), Value::bytevector(vec![255]));
1465    }
1466
1467    #[test]
1468    fn test_read_bytevector_out_of_range() {
1469        assert!(read("#u8(256)").is_err());
1470    }
1471
1472    #[test]
1473    fn test_read_bytevector_negative() {
1474        assert!(read("#u8(-1)").is_err());
1475    }
1476
1477    #[test]
1478    fn test_read_bytevector_non_integer() {
1479        assert!(read("#u8(1.5)").is_err());
1480    }
1481
1482    #[test]
1483    fn test_read_bytevector_unterminated() {
1484        assert!(read("#u8(1 2").is_err());
1485    }
1486
1487    #[test]
1488    fn test_read_bytevector_in_list() {
1489        let result = read("(#u8(1 2) #u8(3))").unwrap();
1490        assert_eq!(
1491            result,
1492            Value::list(vec![
1493                Value::bytevector(vec![1, 2]),
1494                Value::bytevector(vec![3]),
1495            ])
1496        );
1497    }
1498
1499    #[test]
1500    fn test_read_string_hex_escape_basic() {
1501        // \x41; is 'A'
1502        let result = read(r#""\x41;""#).unwrap();
1503        assert_eq!(result, Value::string("A"));
1504    }
1505
1506    #[test]
1507    fn test_read_string_hex_escape_lowercase() {
1508        let result = read(r#""\x6c;""#).unwrap();
1509        assert_eq!(result, Value::string("l"));
1510    }
1511
1512    #[test]
1513    fn test_read_string_hex_escape_mixed_case() {
1514        let result = read(r#""\x4F;""#).unwrap();
1515        assert_eq!(result, Value::string("O"));
1516    }
1517
1518    #[test]
1519    fn test_read_string_hex_escape_esc_char() {
1520        // \x1B; is ESC (0x1b) — the main motivating use case
1521        let result = read(r#""\x1B;""#).unwrap();
1522        assert_eq!(result, Value::string("\x1B"));
1523    }
1524
1525    #[test]
1526    fn test_read_string_hex_escape_null() {
1527        let result = read(r#""\x0;""#).unwrap();
1528        assert_eq!(result, Value::string("\0"));
1529    }
1530
1531    #[test]
1532    fn test_read_string_hex_escape_unicode() {
1533        // \x3BB; is λ (Greek small letter lambda)
1534        let result = read(r#""\x3BB;""#).unwrap();
1535        assert_eq!(result, Value::string("λ"));
1536    }
1537
1538    #[test]
1539    fn test_read_string_hex_escape_emoji() {
1540        // \x1F600; is 😀
1541        let result = read(r#""\x1F600;""#).unwrap();
1542        assert_eq!(result, Value::string("😀"));
1543    }
1544
1545    #[test]
1546    fn test_read_string_hex_escape_in_context() {
1547        // Mix hex escapes with regular text and other escapes
1548        let result = read(r#""hello\x20;world""#).unwrap();
1549        assert_eq!(result, Value::string("hello world"));
1550    }
1551
1552    #[test]
1553    fn test_read_string_hex_escape_multiple() {
1554        let result = read(r#""\x48;\x69;""#).unwrap();
1555        assert_eq!(result, Value::string("Hi"));
1556    }
1557
1558    #[test]
1559    fn test_read_string_hex_escape_missing_semicolon() {
1560        assert!(read(r#""\x41""#).is_err());
1561    }
1562
1563    #[test]
1564    fn test_read_string_hex_escape_no_digits() {
1565        assert!(read(r#""\x;""#).is_err());
1566    }
1567
1568    #[test]
1569    fn test_read_string_hex_escape_invalid_hex() {
1570        assert!(read(r#""\xGG;""#).is_err());
1571    }
1572
1573    #[test]
1574    fn test_read_string_hex_escape_invalid_codepoint() {
1575        // 0xD800 is a surrogate — invalid Unicode scalar
1576        assert!(read(r#""\xD800;""#).is_err());
1577    }
1578
1579    #[test]
1580    fn test_read_string_hex_escape_too_large() {
1581        // 0x110000 is above Unicode max
1582        assert!(read(r#""\x110000;""#).is_err());
1583    }
1584
1585    #[test]
1586    fn test_read_string_u_escape_basic() {
1587        // \u0041 is 'A'
1588        let result = read(r#""\u0041""#).unwrap();
1589        assert_eq!(result, Value::string("A"));
1590    }
1591
1592    #[test]
1593    fn test_read_string_u_escape_lambda() {
1594        let result = read(r#""\u03BB""#).unwrap();
1595        assert_eq!(result, Value::string("λ"));
1596    }
1597
1598    #[test]
1599    fn test_read_string_u_escape_esc() {
1600        let result = read(r#""\u001B""#).unwrap();
1601        assert_eq!(result, Value::string("\x1B"));
1602    }
1603
1604    #[test]
1605    fn test_read_string_u_escape_too_few_digits() {
1606        assert!(read(r#""\u041""#).is_err());
1607    }
1608
1609    #[test]
1610    fn test_read_string_u_escape_surrogate() {
1611        assert!(read(r#""\uD800""#).is_err());
1612    }
1613
1614    #[test]
1615    fn test_read_string_big_u_escape_basic() {
1616        let result = read(r#""\U00000041""#).unwrap();
1617        assert_eq!(result, Value::string("A"));
1618    }
1619
1620    #[test]
1621    fn test_read_string_big_u_escape_emoji() {
1622        let result = read(r#""\U0001F600""#).unwrap();
1623        assert_eq!(result, Value::string("😀"));
1624    }
1625
1626    #[test]
1627    fn test_read_string_big_u_escape_too_few_digits() {
1628        assert!(read(r#""\U0041""#).is_err());
1629    }
1630
1631    #[test]
1632    fn test_read_string_big_u_escape_invalid() {
1633        assert!(read(r#""\U00110000""#).is_err());
1634    }
1635
1636    #[test]
1637    fn test_read_string_null_escape() {
1638        let result = read(r#""\0""#).unwrap();
1639        assert_eq!(result, Value::string("\0"));
1640    }
1641
1642    #[test]
1643    fn test_read_string_mixed_escapes() {
1644        // Mix all escape types in one string
1645        let result = read(r#""\x48;\u0069\n\t""#).unwrap();
1646        assert_eq!(result, Value::string("Hi\n\t"));
1647    }
1648
1649    #[test]
1650    fn test_read_string_ansi_escape_sequence() {
1651        // Real-world: ANSI color code ESC[31m (red)
1652        let result = read(r#""\x1B;[31mRed\x1B;[0m""#).unwrap();
1653        assert_eq!(result, Value::string("\x1B[31mRed\x1B[0m"));
1654    }
1655
1656    // ── f-string tests ──
1657
1658    #[test]
1659    fn test_read_fstring_no_interpolation() {
1660        let result = read(r#"f"hello""#).unwrap();
1661        assert_eq!(
1662            result,
1663            Value::list(vec![Value::symbol("__vm-str"), Value::string("hello")])
1664        );
1665    }
1666
1667    #[test]
1668    fn test_read_fstring_single_var() {
1669        let result = read(r#"f"hello ${name}""#).unwrap();
1670        assert_eq!(
1671            result,
1672            Value::list(vec![
1673                Value::symbol("__vm-str"),
1674                Value::string("hello "),
1675                Value::symbol("name"),
1676            ])
1677        );
1678    }
1679
1680    #[test]
1681    fn test_read_fstring_multiple_vars() {
1682        let result = read(r#"f"${a} and ${b}""#).unwrap();
1683        assert_eq!(
1684            result,
1685            Value::list(vec![
1686                Value::symbol("__vm-str"),
1687                Value::symbol("a"),
1688                Value::string(" and "),
1689                Value::symbol("b"),
1690            ])
1691        );
1692    }
1693
1694    #[test]
1695    fn test_read_fstring_expression() {
1696        let result = read(r#"f"result: ${(+ 1 2)}""#).unwrap();
1697        assert_eq!(
1698            result,
1699            Value::list(vec![
1700                Value::symbol("__vm-str"),
1701                Value::string("result: "),
1702                Value::list(vec![Value::symbol("+"), Value::int(1), Value::int(2),]),
1703            ])
1704        );
1705    }
1706
1707    #[test]
1708    fn test_read_fstring_escaped_dollar() {
1709        let result = read(r#"f"costs \$5""#).unwrap();
1710        assert_eq!(
1711            result,
1712            Value::list(vec![Value::symbol("__vm-str"), Value::string("costs $5")])
1713        );
1714    }
1715
1716    #[test]
1717    fn test_read_fstring_dollar_without_brace() {
1718        let result = read(r#"f"costs $5""#).unwrap();
1719        assert_eq!(
1720            result,
1721            Value::list(vec![Value::symbol("__vm-str"), Value::string("costs $5")])
1722        );
1723    }
1724
1725    #[test]
1726    fn test_read_fstring_escape_sequences() {
1727        let result = read(r#"f"line1\nline2""#).unwrap();
1728        assert_eq!(
1729            result,
1730            Value::list(vec![
1731                Value::symbol("__vm-str"),
1732                Value::string("line1\nline2"),
1733            ])
1734        );
1735    }
1736
1737    #[test]
1738    fn test_read_fstring_empty_interpolation_error() {
1739        assert!(read(r#"f"hello ${}""#).is_err());
1740    }
1741
1742    #[test]
1743    fn test_read_fstring_unterminated_interpolation_error() {
1744        assert!(read(r#"f"hello ${name""#).is_err());
1745    }
1746
1747    #[test]
1748    fn test_read_fstring_unterminated_string_error() {
1749        assert!(read(r#"f"hello"#).is_err());
1750    }
1751
1752    #[test]
1753    fn test_read_fstring_multiple_forms_error() {
1754        // READ-2: `${x y}` carries two forms — must error, not silently drop `y`.
1755        let err = read(r#"f"${x y}""#).unwrap_err();
1756        assert!(
1757            err.to_string().contains("exactly one expression"),
1758            "expected single-expression error, got: {err}"
1759        );
1760    }
1761
1762    #[test]
1763    fn test_read_fstring_respects_depth_limit() {
1764        // READ-1: f-string interpolation must not reset the depth counter to 0.
1765        // A deeply nested form inside `${...}` must still trip MAX_PARSE_DEPTH
1766        // rather than recursing freely and risking a stack overflow. Run on a
1767        // large stack so the result reflects the depth check, not the small
1768        // default test-thread stack.
1769        let result = std::thread::Builder::new()
1770            .stack_size(16 * 1024 * 1024)
1771            .spawn(|| {
1772                let depth = 3000;
1773                let inner = format!("{}{}", "[".repeat(depth), "]".repeat(depth));
1774                let src = format!("f\"${{{inner}}}\"");
1775                read(&src).is_err()
1776            })
1777            .unwrap()
1778            .join()
1779            .expect("parser must not overflow the stack on deeply nested f-string");
1780        assert!(
1781            result,
1782            "expected a depth-limit error for deeply nested f-string interpolation"
1783        );
1784    }
1785
1786    #[test]
1787    fn test_read_fstring_keyword_access() {
1788        let result = read(r#"f"name: ${(:name user)}""#).unwrap();
1789        assert_eq!(
1790            result,
1791            Value::list(vec![
1792                Value::symbol("__vm-str"),
1793                Value::string("name: "),
1794                Value::list(vec![Value::keyword("name"), Value::symbol("user")]),
1795            ])
1796        );
1797    }
1798
1799    #[test]
1800    fn test_read_fstring_in_list() {
1801        let result = read(r#"(println f"hello ${name}")"#).unwrap();
1802        assert_eq!(
1803            result,
1804            Value::list(vec![
1805                Value::symbol("println"),
1806                Value::list(vec![
1807                    Value::symbol("__vm-str"),
1808                    Value::string("hello "),
1809                    Value::symbol("name"),
1810                ]),
1811            ])
1812        );
1813    }
1814
1815    #[test]
1816    fn test_read_fstring_empty() {
1817        let result = read(r#"f"""#).unwrap();
1818        assert_eq!(result, Value::list(vec![Value::symbol("__vm-str")]));
1819    }
1820
1821    #[test]
1822    fn test_read_fstring_only_expr() {
1823        let result = read(r#"f"${x}""#).unwrap();
1824        assert_eq!(
1825            result,
1826            Value::list(vec![Value::symbol("__vm-str"), Value::symbol("x")])
1827        );
1828    }
1829
1830    #[test]
1831    fn test_read_f_symbol_still_works() {
1832        // Plain 'f' symbol (not followed by '"') should still parse as symbol
1833        let result = read("f").unwrap();
1834        assert_eq!(result, Value::symbol("f"));
1835    }
1836
1837    #[test]
1838    fn test_read_f_prefixed_symbol_still_works() {
1839        // 'foo' should still parse as a normal symbol
1840        let result = read("foo").unwrap();
1841        assert_eq!(result, Value::symbol("foo"));
1842    }
1843
1844    // ── short lambda tests ──
1845
1846    #[test]
1847    fn test_read_short_lambda_single_arg() {
1848        // #(+ % 1) → (lambda (%1) (+ %1 1))
1849        let result = read("#(+ % 1)").unwrap();
1850        assert_eq!(
1851            result,
1852            Value::list(vec![
1853                Value::symbol("lambda"),
1854                Value::list(vec![Value::symbol("%1")]),
1855                Value::list(vec![Value::symbol("+"), Value::symbol("%1"), Value::int(1),]),
1856            ])
1857        );
1858    }
1859
1860    #[test]
1861    fn test_read_short_lambda_two_args() {
1862        // #(+ %1 %2) → (lambda (%1 %2) (+ %1 %2))
1863        let result = read("#(+ %1 %2)").unwrap();
1864        assert_eq!(
1865            result,
1866            Value::list(vec![
1867                Value::symbol("lambda"),
1868                Value::list(vec![Value::symbol("%1"), Value::symbol("%2")]),
1869                Value::list(vec![
1870                    Value::symbol("+"),
1871                    Value::symbol("%1"),
1872                    Value::symbol("%2"),
1873                ]),
1874            ])
1875        );
1876    }
1877
1878    #[test]
1879    fn test_read_short_lambda_bare_percent_is_percent1() {
1880        // #(* % %) → (lambda (%1) (* %1 %1))
1881        let result = read("#(* % %)").unwrap();
1882        assert_eq!(
1883            result,
1884            Value::list(vec![
1885                Value::symbol("lambda"),
1886                Value::list(vec![Value::symbol("%1")]),
1887                Value::list(vec![
1888                    Value::symbol("*"),
1889                    Value::symbol("%1"),
1890                    Value::symbol("%1"),
1891                ]),
1892            ])
1893        );
1894    }
1895
1896    #[test]
1897    fn test_read_short_lambda_no_args() {
1898        // #(println "hello") → (lambda () (println "hello"))
1899        let result = read(r#"#(println "hello")"#).unwrap();
1900        assert_eq!(
1901            result,
1902            Value::list(vec![
1903                Value::symbol("lambda"),
1904                Value::list(vec![]),
1905                Value::list(vec![Value::symbol("println"), Value::string("hello"),]),
1906            ])
1907        );
1908    }
1909
1910    #[test]
1911    fn test_read_short_lambda_in_list() {
1912        // (map #(+ % 1) numbers)
1913        let result = read("(map #(+ % 1) numbers)").unwrap();
1914        assert_eq!(
1915            result,
1916            Value::list(vec![
1917                Value::symbol("map"),
1918                Value::list(vec![
1919                    Value::symbol("lambda"),
1920                    Value::list(vec![Value::symbol("%1")]),
1921                    Value::list(vec![Value::symbol("+"), Value::symbol("%1"), Value::int(1),]),
1922                ]),
1923                Value::symbol("numbers"),
1924            ])
1925        );
1926    }
1927
1928    #[test]
1929    fn test_read_short_lambda_unterminated() {
1930        assert!(read("#(+ % 1").is_err());
1931    }
1932
1933    #[test]
1934    fn test_read_short_lambda_nested_expr() {
1935        // #(> (string-length %) 3) → (lambda (%1) (> (string-length %1) 3))
1936        let result = read("#(> (string-length %) 3)").unwrap();
1937        assert_eq!(
1938            result,
1939            Value::list(vec![
1940                Value::symbol("lambda"),
1941                Value::list(vec![Value::symbol("%1")]),
1942                Value::list(vec![
1943                    Value::symbol(">"),
1944                    Value::list(vec![Value::symbol("string-length"), Value::symbol("%1"),]),
1945                    Value::int(3),
1946                ]),
1947            ])
1948        );
1949    }
1950
1951    #[test]
1952    fn test_read_short_lambda_nested_lambda() {
1953        // #(map (lambda (y) (+ y %)) (list 10))
1954        let result = read("#(map (lambda (y) (+ y %)) (list 10))").unwrap();
1955        assert_eq!(
1956            result,
1957            Value::list(vec![
1958                Value::symbol("lambda"),
1959                Value::list(vec![Value::symbol("%1")]),
1960                Value::list(vec![
1961                    Value::symbol("map"),
1962                    Value::list(vec![
1963                        Value::symbol("lambda"),
1964                        Value::list(vec![Value::symbol("y")]),
1965                        Value::list(vec![
1966                            Value::symbol("+"),
1967                            Value::symbol("y"),
1968                            Value::symbol("%1"), // The % was correctly rewritten to %1 from the outer #()
1969                        ]),
1970                    ]),
1971                    Value::list(vec![Value::symbol("list"), Value::int(10)]),
1972                ]),
1973            ])
1974        );
1975    }
1976
1977    #[test]
1978    fn test_read_short_lambda_nested_short_lambda_error() {
1979        // #(#(+ % 1)) should be a read error
1980        let err = read("#(#(+ % 1))").unwrap_err();
1981        assert!(err
1982            .to_string()
1983            .contains("nested short lambdas are not allowed"));
1984    }
1985
1986    #[test]
1987    fn test_read_short_lambda_placeholder_cap() {
1988        // At the cap: fine (255 params materialize).
1989        let ok = read("#(+ %255 1)").unwrap();
1990        let items = ok.as_list().unwrap();
1991        assert_eq!(items[1].as_list().unwrap().len(), 255);
1992        // One past the cap: a clear read error instead of a huge allocation.
1993        let err = read("#(+ %256 1)").unwrap_err();
1994        assert!(err.to_string().contains("exceeds the maximum"));
1995        // The fuzzer's 12-byte slow unit.
1996        let err = read("#(%9999999)").unwrap_err();
1997        assert!(err.to_string().contains("exceeds the maximum"));
1998    }
1999
2000    #[test]
2001    fn test_read_short_lambda_rest_arg() {
2002        // #(apply + %&) → (lambda (. %&) (apply + %&))
2003        let result = read("#(apply + %&)").unwrap();
2004        assert_eq!(
2005            result,
2006            Value::list(vec![
2007                Value::symbol("lambda"),
2008                Value::list(vec![Value::symbol("."), Value::symbol("%&")]),
2009                Value::list(vec![
2010                    Value::symbol("apply"),
2011                    Value::symbol("+"),
2012                    Value::symbol("%&")
2013                ]),
2014            ])
2015        );
2016    }
2017
2018    #[test]
2019    fn test_read_regex_literal_digits() {
2020        let result = read(r#"#"\d+""#).unwrap();
2021        assert_eq!(result, Value::string(r"\d+"));
2022    }
2023
2024    #[test]
2025    fn test_read_regex_literal_char_class() {
2026        let result = read(r#"#"[a-z]+""#).unwrap();
2027        assert_eq!(result, Value::string("[a-z]+"));
2028    }
2029
2030    #[test]
2031    fn test_read_regex_literal_backslashes_literal() {
2032        let result = read(r#"#"hello\.world""#).unwrap();
2033        assert_eq!(result, Value::string(r"hello\.world"));
2034    }
2035
2036    #[test]
2037    fn test_read_regex_literal_escaped_quote() {
2038        let result = read(r#"#"foo\"bar""#).unwrap();
2039        assert_eq!(result, Value::string(r#"foo"bar"#));
2040    }
2041
2042    #[test]
2043    fn test_read_regex_literal_unterminated() {
2044        assert!(read(r#"#"abc"#).is_err());
2045    }
2046
2047    #[test]
2048    fn test_mismatched_paren_bracket() {
2049        let err = read("(list [1 2 3)").unwrap_err();
2050        let msg = err.to_string();
2051        assert!(
2052            msg.contains("mismatched"),
2053            "expected mismatched error, got: {msg}"
2054        );
2055    }
2056
2057    #[test]
2058    fn test_mismatched_bracket_paren() {
2059        let err = read("[1 2 3)").unwrap_err();
2060        let msg = err.to_string();
2061        assert!(
2062            msg.contains("mismatched"),
2063            "expected mismatched error, got: {msg}"
2064        );
2065    }
2066
2067    #[test]
2068    fn test_mismatched_paren_brace() {
2069        let err = read("(+ 1 2}").unwrap_err();
2070        let msg = err.to_string();
2071        assert!(
2072            msg.contains("mismatched"),
2073            "expected mismatched error, got: {msg}"
2074        );
2075    }
2076
2077    #[test]
2078    fn test_mismatched_brace_paren() {
2079        let err = read("{:a 1)").unwrap_err();
2080        let msg = err.to_string();
2081        assert!(
2082            msg.contains("mismatched"),
2083            "expected mismatched error, got: {msg}"
2084        );
2085    }
2086
2087    #[test]
2088    fn test_mismatched_brace_bracket() {
2089        let err = read("{:a 1]").unwrap_err();
2090        let msg = err.to_string();
2091        assert!(
2092            msg.contains("mismatched"),
2093            "expected mismatched error, got: {msg}"
2094        );
2095    }
2096
2097    #[test]
2098    fn test_mismatched_bracket_brace() {
2099        let err = read("[1 2}").unwrap_err();
2100        let msg = err.to_string();
2101        assert!(
2102            msg.contains("mismatched"),
2103            "expected mismatched error, got: {msg}"
2104        );
2105    }
2106
2107    #[test]
2108    fn test_correct_brackets_still_work() {
2109        assert!(read("(list [1 2 3])").is_ok());
2110        assert!(read("{:a 1}").is_ok());
2111        assert!(read("[1 [2 3] 4]").is_ok());
2112    }
2113
2114    #[test]
2115    fn test_auto_gensym_symbol_parsing() {
2116        let val = read("v#").unwrap();
2117        assert_eq!(val.as_symbol().unwrap(), "v#");
2118
2119        let val = read("tmp#").unwrap();
2120        assert_eq!(val.as_symbol().unwrap(), "tmp#");
2121
2122        let val = read("`(let ((v# 1)) v#)").unwrap();
2123        let items = val.as_list().unwrap();
2124        assert_eq!(items[0].as_symbol().unwrap(), "quasiquote");
2125    }
2126
2127    #[test]
2128    fn test_hash_reader_dispatch_still_works() {
2129        let val = read("#t").unwrap();
2130        assert_eq!(val.as_bool(), Some(true));
2131
2132        let val = read("#f").unwrap();
2133        assert_eq!(val.as_bool(), Some(false));
2134
2135        let val = read("#\\space").unwrap();
2136        assert_eq!(val.as_char(), Some(' '));
2137
2138        let val = read("#(+ % 1)").unwrap();
2139        assert!(val.as_list().is_some());
2140    }
2141
2142    #[test]
2143    fn test_auto_gensym_edge_cases() {
2144        let val = read("x##").unwrap();
2145        assert_eq!(val.as_symbol().unwrap(), "x##");
2146
2147        let val = read(":foo").unwrap();
2148        assert!(val.as_keyword().is_some());
2149    }
2150
2151    // ── Error recovery tests ─────────────────────────────────────
2152
2153    #[test]
2154    fn recover_valid_input_no_errors() {
2155        let (exprs, _, _, errors) = read_many_with_spans_recover("(+ 1 2) (- 3 4)");
2156        assert!(errors.is_empty());
2157        assert_eq!(exprs.len(), 2);
2158    }
2159
2160    #[test]
2161    fn recover_stray_closer_then_valid() {
2162        // Stray `)` then a valid form
2163        let (exprs, _, _, errors) = read_many_with_spans_recover(") (+ 1 2)");
2164        assert_eq!(errors.len(), 1);
2165        assert_eq!(exprs.len(), 1);
2166    }
2167
2168    #[test]
2169    fn recover_unclosed_then_valid() {
2170        // Unclosed list, then a valid form on the next line
2171        let (_exprs, _, _, errors) = read_many_with_spans_recover("(define x\n(+ 1 2)");
2172        // The first `(define x` consumes tokens including `(+ 1 2)` as part of
2173        // its unterminated body, then hits EOF → 1 error, the (+ 1 2) is inside it
2174        assert_eq!(errors.len(), 1);
2175        // The second form got consumed by the unterminated first form
2176        // so recovery can't salvage it — this is expected
2177    }
2178
2179    #[test]
2180    fn recover_multiple_stray_closers() {
2181        let (exprs, _, _, errors) = read_many_with_spans_recover(") ] } (define x 1)");
2182        assert_eq!(errors.len(), 3);
2183        assert_eq!(exprs.len(), 1);
2184        assert!(exprs[0].as_list().is_some());
2185    }
2186
2187    #[test]
2188    fn recover_mismatched_bracket() {
2189        // Mismatched bracket: ( closed with ]
2190        let (exprs, _, _, errors) = read_many_with_spans_recover("(define x] (+ 1 2)");
2191        assert!(!errors.is_empty());
2192        // After the mismatch error, recovery should find `(+ 1 2)`
2193        assert!(!exprs.is_empty());
2194    }
2195
2196    #[test]
2197    fn recover_empty_input() {
2198        let (exprs, _, _, errors) = read_many_with_spans_recover("");
2199        assert!(errors.is_empty());
2200        assert!(exprs.is_empty());
2201    }
2202
2203    #[test]
2204    fn recover_only_errors() {
2205        let (exprs, _, _, errors) = read_many_with_spans_recover(") )");
2206        assert_eq!(errors.len(), 2);
2207        assert!(exprs.is_empty());
2208    }
2209
2210    #[test]
2211    fn recover_valid_between_errors() {
2212        // error, valid, error
2213        let (exprs, _, _, errors) = read_many_with_spans_recover(") (+ 1 2) )");
2214        assert_eq!(errors.len(), 2);
2215        assert_eq!(exprs.len(), 1);
2216    }
2217
2218    // ── symbol span tracking ──
2219
2220    #[test]
2221    fn test_symbol_spans_basic() {
2222        let (_, _, sym_spans) = read_many_with_symbol_spans("(define x 42)").unwrap();
2223        // Should record "define" and "x" (not 42 — it's an int, not a symbol)
2224        let names: Vec<&str> = sym_spans.iter().map(|(n, _)| n.as_str()).collect();
2225        assert!(names.contains(&"define"), "missing define in {:?}", names);
2226        assert!(names.contains(&"x"), "missing x in {:?}", names);
2227        assert_eq!(names.len(), 2);
2228    }
2229
2230    #[test]
2231    fn test_symbol_spans_positions() {
2232        let (_, _, sym_spans) = read_many_with_symbol_spans("(defun foo (x) x)").unwrap();
2233        // "foo" should have a precise span
2234        let foo = sym_spans.iter().find(|(n, _)| n == "foo").unwrap();
2235        assert_eq!(foo.1.line, 1);
2236        assert_eq!(foo.1.col, 8); // 1-indexed: "(defun " = 7 chars, foo starts at col 8
2237    }
2238
2239    #[test]
2240    fn test_symbol_spans_no_synthetic() {
2241        // '(a b) desugars to (quote (a b)) — "quote" should NOT appear in symbol_spans
2242        let (_, _, sym_spans) = read_many_with_symbol_spans("'(a b)").unwrap();
2243        let names: Vec<&str> = sym_spans.iter().map(|(n, _)| n.as_str()).collect();
2244        assert!(
2245            !names.contains(&"quote"),
2246            "synthetic 'quote' should not be in symbol_spans"
2247        );
2248        assert!(names.contains(&"a"));
2249        assert!(names.contains(&"b"));
2250    }
2251
2252    #[test]
2253    fn test_symbol_spans_multiple_forms() {
2254        let (_, _, sym_spans) =
2255            read_many_with_symbol_spans("(define x 1)\n(defun f (a) a)").unwrap();
2256        let names: Vec<&str> = sym_spans.iter().map(|(n, _)| n.as_str()).collect();
2257        assert!(names.contains(&"define"));
2258        assert!(names.contains(&"x"));
2259        assert!(names.contains(&"defun"));
2260        assert!(names.contains(&"f"));
2261        assert!(names.contains(&"a"));
2262        // "a" should appear twice (param + body reference)
2263        assert_eq!(names.iter().filter(|&&n| n == "a").count(), 2);
2264    }
2265
2266    #[test]
2267    fn test_symbol_spans_nil_excluded() {
2268        // "nil" parses as Value::nil(), not a symbol — should not be in symbol_spans
2269        let (_, _, sym_spans) = read_many_with_symbol_spans("nil").unwrap();
2270        assert!(sym_spans.is_empty());
2271    }
2272}