Skip to main content

dazzle_core/scheme/
parser.rs

1//! Scheme parser (lexer + S-expression parser)
2//!
3//! Ported from OpenJade's `SchemeParser.cxx` (~2,500 lines).
4//!
5//! ## Key Features
6//!
7//! - **Whitespace-agnostic**: Unlike Steel, this parser handles all valid R4RS whitespace
8//! - **Line number tracking**: Error messages report line:column, not byte spans
9//! - **Full R4RS syntax**:
10//!   - Numbers: integers, reals, hex (#x), octal (#o), binary (#b)
11//!   - Strings: escapes (\n, \t, \", \\, \xNN)
12//!   - Symbols and keywords
13//!   - Quote/quasiquote/unquote/unquote-splicing
14//!   - Comments: line (;) and block (#| ... |#)
15//!   - Vectors: #(...)
16//!   - Booleans: #t, #f
17//!   - Characters: #\a, #\space, #\newline
18//!
19//! ## Architecture
20//!
21//! 1. **Lexer** (`Tokenizer`): Character stream → Token stream
22//! 2. **Parser** (`Parser`): Token stream → Value (S-expressions)
23//!
24//! ## OpenJade Correspondence
25//!
26//! | Dazzle        | OpenJade             | Purpose                |
27//! |---------------|----------------------|------------------------|
28//! | `Token`       | `SchemeParser::tok_` | Token types            |
29//! | `Tokenizer`   | `SchemeParser`       | Lexical analysis       |
30//! | `Parser`      | `SchemeParser::get*` | Syntax analysis        |
31//!
32//! ## Error Handling
33//!
34//! Parse errors include:
35//! - Line and column numbers (not byte offsets!)
36//! - Descriptive messages
37//! - Context (what was expected)
38
39use crate::scheme::value::{Value, Unit};
40use std::fmt;
41
42// =============================================================================
43// Token Types
44// =============================================================================
45
46/// Token type (corresponds to OpenJade's token enum)
47#[derive(Debug, Clone, PartialEq)]
48pub enum Token {
49    // Literals
50    Integer(i64),
51    Real(f64),
52    Quantity(f64, String), // DSSSL quantity: magnitude + unit suffix (pt, pi, in, mm, cm, em)
53    String(String),
54    Char(char),
55    Symbol(String),
56    Keyword(String),
57    Bool(bool),
58
59    // Delimiters
60    LeftParen,   // (
61    RightParen,  // )
62    LeftBracket, // [  (optional R5RS)
63    RightBracket, // ] (optional R5RS)
64    Dot,         // .
65
66    // Quotation
67    Quote,            // '
68    Quasiquote,       // `
69    Unquote,          // ,
70    UnquoteSplicing,  // ,@
71
72    // Vector
73    VectorStart, // #(
74
75    // End of input
76    Eof,
77}
78
79impl fmt::Display for Token {
80    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81        match self {
82            Token::Integer(n) => write!(f, "{}", n),
83            Token::Real(n) => write!(f, "{}", n),
84            Token::Quantity(magnitude, unit) => write!(f, "{}{}", magnitude, unit),
85            Token::String(s) => write!(f, "\"{}\"", s),
86            Token::Char(ch) => write!(f, "#\\{}", ch),
87            Token::Symbol(s) => write!(f, "{}", s),
88            Token::Keyword(s) => write!(f, "#:{}", s),
89            Token::Bool(b) => write!(f, "{}", if *b { "#t" } else { "#f" }),
90            Token::LeftParen => write!(f, "("),
91            Token::RightParen => write!(f, ")"),
92            Token::LeftBracket => write!(f, "["),
93            Token::RightBracket => write!(f, "]"),
94            Token::Dot => write!(f, "."),
95            Token::Quote => write!(f, "'"),
96            Token::Quasiquote => write!(f, "`"),
97            Token::Unquote => write!(f, ","),
98            Token::UnquoteSplicing => write!(f, ",@"),
99            Token::VectorStart => write!(f, "#("),
100            Token::Eof => write!(f, "<EOF>"),
101        }
102    }
103}
104
105// =============================================================================
106// Source Position (for error reporting)
107// =============================================================================
108
109/// Source code position (line and column)
110///
111/// **Important**: Line and column numbers start at 1 (human-readable).
112///
113/// Derives Trace and Finalize for GC compatibility (used in PairData).
114/// Note: Can't derive Copy with Finalize due to destructor conflict.
115#[derive(Debug, Clone, PartialEq, Eq, gc::Trace, gc::Finalize)]
116pub struct Position {
117    pub line: usize,
118    pub column: usize,
119}
120
121impl Position {
122    pub fn new() -> Self {
123        Position { line: 1, column: 1 }
124    }
125}
126
127impl Default for Position {
128    fn default() -> Self {
129        Self::new()
130    }
131}
132
133impl fmt::Display for Position {
134    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135        write!(f, "{}:{}", self.line, self.column)
136    }
137}
138
139// =============================================================================
140// Parse Error
141// =============================================================================
142
143/// Parse error with line:column position and optional filename
144#[derive(Debug, Clone)]
145pub struct ParseError {
146    pub message: String,
147    pub position: Position,
148    pub filename: Option<String>,
149}
150
151impl ParseError {
152    pub fn new(message: String, position: Position) -> Self {
153        ParseError { message, position, filename: None }
154    }
155
156    pub fn with_filename(message: String, position: Position, filename: String) -> Self {
157        ParseError { message, position, filename: Some(filename) }
158    }
159}
160
161impl fmt::Display for ParseError {
162    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163        if let Some(ref filename) = self.filename {
164            write!(f, "{}:{}:E: {}", filename, self.position, self.message)
165        } else {
166            write!(f, "Parse error at {}: {}", self.position, self.message)
167        }
168    }
169}
170
171impl std::error::Error for ParseError {}
172
173pub type ParseResult<T> = Result<T, ParseError>;
174
175// =============================================================================
176// Tokenizer (Lexer)
177// =============================================================================
178
179/// Tokenizer for Scheme source code
180///
181/// Corresponds to OpenJade's `SchemeParser` lexical analysis methods.
182///
183/// ## Whitespace Handling
184///
185/// **Critical difference from Steel**: This tokenizer is fully whitespace-agnostic.
186/// Whitespace can appear anywhere between tokens without affecting parsing.
187///
188/// Example that breaks Steel but works here:
189/// ```scheme
190/// (let ((x 1)
191///       (y 2))  ; Multi-line let bindings
192///   (+ x y))
193/// ```
194pub struct Tokenizer {
195    /// Input source code
196    input: Vec<char>,
197
198    /// Current position in input
199    pos: usize,
200
201    /// Current line (1-based)
202    line: usize,
203
204    /// Current column (1-based)
205    column: usize,
206
207    /// Peeked token (for lookahead)
208    peeked: Option<Token>,
209
210    /// Optional filename for error reporting
211    filename: Option<String>,
212}
213
214impl Tokenizer {
215    /// Create a new tokenizer from source code
216    pub fn new(input: &str) -> Self {
217        Tokenizer {
218            input: input.chars().collect(),
219            pos: 0,
220            line: 1,
221            column: 1,
222            peeked: None,
223            filename: None,
224        }
225    }
226
227    /// Create a new tokenizer from source code with a filename for error reporting
228    pub fn new_with_filename(input: &str, filename: String) -> Self {
229        Tokenizer {
230            input: input.chars().collect(),
231            pos: 0,
232            line: 1,
233            column: 1,
234            peeked: None,
235            filename: Some(filename),
236        }
237    }
238
239    /// Helper to create a ParseError with the current filename
240    fn error(&self, message: String, position: Position) -> ParseError {
241        if let Some(ref filename) = self.filename {
242            ParseError::with_filename(message, position, filename.clone())
243        } else {
244            ParseError::new(message, position)
245        }
246    }
247
248    /// Get current position (for error reporting)
249    pub fn position(&self) -> Position {
250        Position {
251            line: self.line,
252            column: self.column,
253        }
254    }
255
256    /// Peek at current character without consuming
257    fn peek_char(&self) -> Option<char> {
258        if self.pos < self.input.len() {
259            Some(self.input[self.pos])
260        } else {
261            None
262        }
263    }
264
265    /// Peek at character at offset from current position
266    fn peek_char_at(&self, offset: usize) -> Option<char> {
267        let index = self.pos + offset;
268        if index < self.input.len() {
269            Some(self.input[index])
270        } else {
271            None
272        }
273    }
274
275    /// Consume and return current character
276    fn next_char(&mut self) -> Option<char> {
277        if self.pos < self.input.len() {
278            let ch = self.input[self.pos];
279            self.pos += 1;
280
281            // Update line/column tracking
282            if ch == '\n' {
283                self.line += 1;
284                self.column = 1;
285            } else {
286                self.column += 1;
287            }
288
289            Some(ch)
290        } else {
291            None
292        }
293    }
294
295    /// Skip whitespace and comments
296    ///
297    /// **Key feature**: Handles both line comments (;) and block comments (#| ... |#)
298    fn skip_whitespace(&mut self) {
299        loop {
300            match self.peek_char() {
301                // Whitespace
302                Some(ch) if ch.is_whitespace() => {
303                    self.next_char();
304                }
305
306                // Line comment: ; to end of line
307                Some(';') => {
308                    self.next_char();
309                    while let Some(ch) = self.peek_char() {
310                        self.next_char();
311                        if ch == '\n' {
312                            break;
313                        }
314                    }
315                }
316
317                // Block comment: #| ... |#
318                Some('#') if self.peek_char_at(1) == Some('|') => {
319                    self.next_char(); // #
320                    self.next_char(); // |
321
322                    // Find closing |#
323                    let mut depth = 1;
324                    while depth > 0 {
325                        match self.next_char() {
326                            Some('|') if self.peek_char() == Some('#') => {
327                                self.next_char(); // #
328                                depth -= 1;
329                            }
330                            Some('#') if self.peek_char() == Some('|') => {
331                                self.next_char(); // |
332                                depth += 1; // Nested block comment
333                            }
334                            Some(_) => {} // Continue
335                            None => break, // EOF in comment (error, but tolerate)
336                        }
337                    }
338                }
339
340                // Not whitespace or comment
341                _ => break,
342            }
343        }
344    }
345
346    /// Check if character is a delimiter (ends a token)
347    fn is_delimiter(ch: char) -> bool {
348        ch.is_whitespace()
349            || matches!(
350                ch,
351                '(' | ')' | '[' | ']' | '"' | ';' | ',' | '`' | '\''
352            )
353    }
354
355    /// Check if the next characters form a DSSSL quantity suffix
356    /// Returns the number of characters in the suffix (0 if no suffix found)
357    fn peek_quantity_suffix(&self) -> usize {
358        // DSSSL quantity units: pt (points), pi (picas), in (inches), mm (millimeters), cm (centimeters), pc (picas), em (ems)
359        // Check for 2-letter suffixes
360        if let Some(ch1) = self.peek_char_at(0) {
361            if let Some(ch2) = self.peek_char_at(1) {
362                match (ch1, ch2) {
363                    ('p', 't') | ('p', 'i') | ('p', 'c') | ('p', 'x') |
364                    ('i', 'n') | ('m', 'm') | ('c', 'm') | ('e', 'm') => return 2,
365                    _ => {}
366                }
367            }
368        }
369        0
370    }
371
372    /// Parse an integer or real number
373    fn parse_number(&mut self, start_pos: Position) -> ParseResult<Token> {
374        let mut num_str = String::new();
375
376        // Collect digits and special characters
377        while let Some(ch) = self.peek_char() {
378            if ch.is_ascii_digit() || matches!(ch, '.' | '+' | '-') {
379                num_str.push(ch);
380                self.next_char();
381            } else if matches!(ch, 'e' | 'E') {
382                // Check if this is scientific notation (1e5) or quantity suffix (1em)
383                // Peek ahead: if next char is 'm', it's the 'em' unit
384                if self.peek_char_at(1) == Some('m') {
385                    // This is a quantity like 1em, 2.5em
386                    // Don't consume 'e', let quantity suffix handler deal with it
387                    break;
388                } else {
389                    // This is scientific notation like 1e5, 2.3e-10
390                    num_str.push(ch);
391                    self.next_char();
392                }
393            } else if Self::is_delimiter(ch) {
394                break;
395            } else {
396                // Check for DSSSL quantity suffix (pt, pi, in, mm, cm, pc, em)
397                // These are not delimiters but indicate a quantity literal
398                let suffix_len = self.peek_quantity_suffix();
399                if suffix_len > 0 {
400                    // Capture the suffix string before consuming
401                    let mut suffix = String::new();
402                    for _ in 0..suffix_len {
403                        if let Some(ch) = self.peek_char() {
404                            suffix.push(ch);
405                        }
406                        self.next_char();
407                    }
408                    // Parse as quantity with unit
409                    // DSSSL quantities: 12pt, 0.5in, 210mm, 1pi, 1em, etc.
410                    if let Ok(n) = num_str.parse::<f64>() {
411                        return Ok(Token::Quantity(n, suffix));
412                    } else if let Ok(n) = num_str.parse::<i64>() {
413                        return Ok(Token::Quantity(n as f64, suffix));
414                    }
415                    return Err(self.error(
416                        format!("Invalid quantity: {}{}", num_str, suffix),
417                        start_pos,
418                    ));
419                }
420
421                // Invalid character in number
422                return Err(self.error(
423                    format!("Invalid character in number: {}", ch),
424                    start_pos,
425                ));
426            }
427        }
428
429        // After loop: check for quantity suffix (handles em, pt, mm, etc.)
430        let suffix_len = self.peek_quantity_suffix();
431        if suffix_len > 0 {
432            // Capture the suffix string before consuming
433            let mut suffix = String::new();
434            for _ in 0..suffix_len {
435                if let Some(ch) = self.peek_char() {
436                    suffix.push(ch);
437                }
438                self.next_char();
439            }
440            // Parse as quantity with unit
441            if let Ok(n) = num_str.parse::<f64>() {
442                return Ok(Token::Quantity(n, suffix));
443            } else if let Ok(n) = num_str.parse::<i64>() {
444                return Ok(Token::Quantity(n as f64, suffix));
445            }
446            return Err(self.error(
447                format!("Invalid quantity: {}{}", num_str, suffix),
448                start_pos,
449            ));
450        }
451
452        // Try parsing as integer first
453        if let Ok(n) = num_str.parse::<i64>() {
454            return Ok(Token::Integer(n));
455        }
456
457        // Try parsing as float
458        if let Ok(n) = num_str.parse::<f64>() {
459            return Ok(Token::Real(n));
460        }
461
462        Err(self.error(
463            format!("Invalid number: {}", num_str),
464            start_pos,
465        ))
466    }
467
468    /// Parse a hexadecimal number (#x prefix)
469    fn parse_hex_number(&mut self, start_pos: Position) -> ParseResult<Token> {
470        let mut num_str = String::new();
471
472        while let Some(ch) = self.peek_char() {
473            if ch.is_ascii_hexdigit() {
474                num_str.push(ch);
475                self.next_char();
476            } else if Self::is_delimiter(ch) {
477                break;
478            } else {
479                return Err(self.error(
480                    format!("Invalid character in hex number: {}", ch),
481                    start_pos,
482                ));
483            }
484        }
485
486        if num_str.is_empty() {
487            return Err(self.error("Empty hex number".to_string(), start_pos));
488        }
489
490        i64::from_str_radix(&num_str, 16)
491            .map(Token::Integer)
492            .map_err(|_| self.error(format!("Invalid hex number: {}", num_str), start_pos))
493    }
494
495    /// Parse an octal number (#o prefix)
496    fn parse_octal_number(&mut self, start_pos: Position) -> ParseResult<Token> {
497        let mut num_str = String::new();
498
499        while let Some(ch) = self.peek_char() {
500            if ch.is_digit(8) {
501                num_str.push(ch);
502                self.next_char();
503            } else if Self::is_delimiter(ch) {
504                break;
505            } else {
506                return Err(self.error(
507                    format!("Invalid character in octal number: {}", ch),
508                    start_pos,
509                ));
510            }
511        }
512
513        if num_str.is_empty() {
514            return Err(self.error("Empty octal number".to_string(), start_pos));
515        }
516
517        i64::from_str_radix(&num_str, 8)
518            .map(Token::Integer)
519            .map_err(|_| self.error(format!("Invalid octal number: {}", num_str), start_pos))
520    }
521
522    /// Parse a binary number (#b prefix)
523    fn parse_binary_number(&mut self, start_pos: Position) -> ParseResult<Token> {
524        let mut num_str = String::new();
525
526        while let Some(ch) = self.peek_char() {
527            if matches!(ch, '0' | '1') {
528                num_str.push(ch);
529                self.next_char();
530            } else if Self::is_delimiter(ch) {
531                break;
532            } else {
533                return Err(self.error(
534                    format!("Invalid character in binary number: {}", ch),
535                    start_pos,
536                ));
537            }
538        }
539
540        if num_str.is_empty() {
541            return Err(self.error("Empty binary number".to_string(), start_pos));
542        }
543
544        i64::from_str_radix(&num_str, 2)
545            .map(Token::Integer)
546            .map_err(|_| self.error(format!("Invalid binary number: {}", num_str), start_pos))
547    }
548
549    /// Parse a symbol or keyword
550    fn parse_symbol(&mut self) -> String {
551        let mut sym = String::new();
552
553        while let Some(ch) = self.peek_char() {
554            if Self::is_delimiter(ch) {
555                break;
556            }
557            sym.push(ch);
558            self.next_char();
559        }
560
561        sym
562    }
563
564    /// Parse a string literal
565    fn parse_string(&mut self, start_pos: Position) -> ParseResult<String> {
566        self.next_char(); // Consume opening "
567
568        let mut result = String::new();
569
570        loop {
571            match self.next_char() {
572                Some('"') => {
573                    // Closing quote
574                    // Normalize CRLF to LF (to match OpenJade behavior)
575                    // OpenJade always outputs Unix line endings regardless of template line endings
576                    let normalized = result.replace("\r\n", "\n");
577                    return Ok(normalized);
578                }
579                Some('\\') => {
580                    // Escape sequence
581                    match self.next_char() {
582                        Some('n') => result.push('\n'),
583                        Some('t') => result.push('\t'),
584                        Some('r') => result.push('\r'),
585                        Some('\\') => result.push('\\'),
586                        Some('"') => result.push('"'),
587                        Some(ch) => result.push(ch), // Unknown escape, keep literal
588                        None => {
589                            return Err(self.error(
590                                "Unexpected EOF in string escape".to_string(),
591                                start_pos,
592                            ))
593                        }
594                    }
595                }
596                Some(ch) => {
597                    result.push(ch);
598                }
599                None => {
600                    return Err(self.error(
601                        "Unexpected EOF in string".to_string(),
602                        start_pos,
603                    ))
604                }
605            }
606        }
607    }
608
609    /// Parse a character literal (#\a, #\space, #\newline)
610    /// Called after # has been consumed
611    fn parse_char(&mut self, start_pos: Position) -> ParseResult<char> {
612        // Expect backslash
613        if self.next_char() != Some('\\') {
614            return Err(self.error(
615                "Expected \\ after # in character literal".to_string(),
616                start_pos,
617            ));
618        }
619
620        // Read character name
621        let mut name = String::new();
622        while let Some(ch) = self.peek_char() {
623            if Self::is_delimiter(ch) {
624                break;
625            }
626            name.push(ch);
627            self.next_char();
628        }
629
630        // If name is empty, it means the next character is a delimiter
631        // In this case, treat the delimiter itself as a single-character literal
632        // This allows #\[ #\] #\( #\) etc.
633        if name.is_empty() {
634            if let Some(ch) = self.next_char() {
635                return Ok(ch);
636            } else {
637                return Err(self.error(
638                    "Unexpected end of input in character literal".to_string(),
639                    start_pos,
640                ));
641            }
642        }
643
644        // Named characters
645        match name.as_str() {
646            "space" => Ok(' '),
647            "newline" => Ok('\n'),
648            "tab" => Ok('\t'),
649            "return" => Ok('\r'),
650            // OpenJade Unicode character literal: #\U-XXXX (e.g., #\U-00E4 for ä)
651            // Format: U-XXXX where XXXX is hexadecimal Unicode code point
652            s if s.starts_with("U-") => {
653                let hex_str = &s[2..]; // Skip "U-"
654                u32::from_str_radix(hex_str, 16)
655                    .ok()
656                    .and_then(std::char::from_u32)
657                    .ok_or_else(|| self.error(
658                        format!("Invalid Unicode character literal: #\\{}", name),
659                        start_pos,
660                    ))
661            }
662            // Accept any single Unicode character (handles UTF-8 multi-byte sequences)
663            // OpenJade accepts UTF-8 characters in character literals for define-language
664            s if s.chars().count() == 1 => Ok(s.chars().next().unwrap()),
665            _ => Err(self.error(
666                format!("Invalid character literal: #\\{}", name),
667                start_pos,
668            )),
669        }
670    }
671
672    /// Parse a CDATA string literal: <![CDATA[...]]>
673    /// This is an OpenJade extension for multi-line string literals
674    /// The content between <![CDATA[ and ]]> is returned as a string token
675    fn parse_cdata_string(&mut self, start_pos: Position) -> ParseResult<Token> {
676        // Skip "<![CDATA["
677        for _ in 0..9 {
678            self.next_char();
679        }
680
681        let mut content = String::new();
682
683        // Read until we find "]]>"
684        loop {
685            match self.peek_char() {
686                None => {
687                    return Err(self.error(
688                        "Unclosed CDATA section: missing ]]>".to_string(),
689                        start_pos,
690                    ));
691                }
692                Some(']') => {
693                    // Check if this is the closing ]]>
694                    if self.pos + 2 < self.input.len()
695                        && self.input[self.pos] == ']'
696                        && self.input[self.pos + 1] == ']'
697                        && self.input[self.pos + 2] == '>'
698                    {
699                        // Skip ]]>
700                        self.next_char(); // ]
701                        self.next_char(); // ]
702                        self.next_char(); // >
703                        break;
704                    } else {
705                        content.push(']');
706                        self.next_char();
707                    }
708                }
709                Some(ch) => {
710                    content.push(ch);
711                    self.next_char();
712                }
713            }
714        }
715
716        Ok(Token::String(content))
717    }
718
719    /// Get the next token
720    pub fn next_token(&mut self) -> ParseResult<Token> {
721        // Check if we have a peeked token
722        if let Some(tok) = self.peeked.take() {
723            return Ok(tok);
724        }
725
726        // Skip whitespace and comments
727        self.skip_whitespace();
728
729        let start_pos = self.position();
730
731        match self.peek_char() {
732            None => Ok(Token::Eof),
733
734            Some('(') => {
735                self.next_char();
736                Ok(Token::LeftParen)
737            }
738
739            Some(')') => {
740                self.next_char();
741                Ok(Token::RightParen)
742            }
743
744            Some('[') => {
745                self.next_char();
746                Ok(Token::LeftBracket)
747            }
748
749            Some(']') => {
750                self.next_char();
751                Ok(Token::RightBracket)
752            }
753
754            Some('\'') => {
755                self.next_char();
756                Ok(Token::Quote)
757            }
758
759            Some('`') => {
760                self.next_char();
761                Ok(Token::Quasiquote)
762            }
763
764            Some(',') => {
765                self.next_char();
766                // Check for ,@
767                if self.peek_char() == Some('@') {
768                    self.next_char();
769                    Ok(Token::UnquoteSplicing)
770                } else {
771                    Ok(Token::Unquote)
772                }
773            }
774
775            Some('"') => {
776                let s = self.parse_string(start_pos)?;
777                Ok(Token::String(s))
778            }
779
780            Some('#') => {
781                self.next_char(); // Consume #
782                match self.peek_char() {
783                    Some('t') => {
784                        self.next_char();
785                        Ok(Token::Bool(true))
786                    }
787                    Some('f') => {
788                        self.next_char();
789                        Ok(Token::Bool(false))
790                    }
791                    Some('(') => {
792                        self.next_char();
793                        Ok(Token::VectorStart)
794                    }
795                    Some('\\') => {
796                        let ch = self.parse_char(start_pos)?;
797                        Ok(Token::Char(ch))
798                    }
799                    Some(':') => {
800                        self.next_char(); // Consume :
801                        let name = self.parse_symbol();
802                        Ok(Token::Keyword(name))
803                    }
804                    Some('x') | Some('X') => {
805                        self.next_char(); // Consume x
806                        self.parse_hex_number(start_pos)
807                    }
808                    Some('o') | Some('O') => {
809                        self.next_char(); // Consume o
810                        self.parse_octal_number(start_pos)
811                    }
812                    Some('b') | Some('B') => {
813                        self.next_char(); // Consume b
814                        self.parse_binary_number(start_pos)
815                    }
816                    Some('!') => {
817                        // OpenJade extension: #!optional, #!key, #!rest
818                        // Parse as a symbol starting with #!
819                        self.next_char(); // Consume !
820                        let name = self.parse_symbol();
821                        Ok(Token::Symbol(format!("#!{}", name)))
822                    }
823                    _ => Err(self.error(
824                        format!("Invalid # syntax: #{:?}", self.peek_char()),
825                        start_pos,
826                    )),
827                }
828            }
829
830            Some(ch) if ch.is_ascii_digit() => self.parse_number(start_pos),
831
832            Some('+') | Some('-') => {
833                // Could be number or symbol
834                if let Some(next) = self.peek_char_at(1) {
835                    if next.is_ascii_digit() {
836                        self.parse_number(start_pos)
837                    } else {
838                        let sym = self.parse_symbol();
839                        Ok(Token::Symbol(sym))
840                    }
841                } else {
842                    let sym = self.parse_symbol();
843                    Ok(Token::Symbol(sym))
844                }
845            }
846
847            Some('.') => {
848                // Could be dot or number starting with .
849                if let Some(next) = self.peek_char_at(1) {
850                    if next.is_ascii_digit() {
851                        self.parse_number(start_pos)
852                    } else {
853                        self.next_char();
854                        Ok(Token::Dot)
855                    }
856                } else {
857                    self.next_char();
858                    Ok(Token::Dot)
859                }
860            }
861
862            Some('<') => {
863                // Check for CDATA section: <![CDATA[...]]>
864                // This is an OpenJade extension for multi-line string literals
865                let cdata_prefix = ['<', '!', '[', 'C', 'D', 'A', 'T', 'A', '['];
866                let is_cdata = self.pos + cdata_prefix.len() <= self.input.len()
867                    && self.input[self.pos..self.pos + cdata_prefix.len()] == cdata_prefix;
868
869                if is_cdata {
870                    self.parse_cdata_string(start_pos)
871                } else {
872                    // Regular < symbol
873                    let sym = self.parse_symbol();
874                    Ok(Token::Symbol(sym))
875                }
876            }
877
878            Some(_) => {
879                // Symbol or DSSSL keyword (trailing colon)
880                let sym = self.parse_symbol();
881
882                // DSSSL uses trailing colon for keywords: name:
883                if sym.ends_with(':') {
884                    let keyword_name = sym[..sym.len()-1].to_string();
885                    Ok(Token::Keyword(keyword_name))
886                } else {
887                    Ok(Token::Symbol(sym))
888                }
889            }
890        }
891    }
892
893    /// Peek at the next token without consuming it
894    pub fn peek_token(&mut self) -> ParseResult<&Token> {
895        if self.peeked.is_none() {
896            let tok = self.next_token()?;
897            self.peeked = Some(tok);
898        }
899        Ok(self.peeked.as_ref().unwrap())
900    }
901}
902
903// =============================================================================
904// Parser (S-expression builder)
905// =============================================================================
906
907/// Parser for building Scheme values from tokens
908///
909/// Corresponds to OpenJade's `SchemeParser::get*` methods.
910///
911/// ## Usage
912///
913/// ```ignore
914/// let parser = Parser::new("(+ 1 2)");
915/// let expr = parser.parse().unwrap();
916/// ```
917pub struct Parser {
918    tokenizer: Tokenizer,
919    filename: Option<String>,
920}
921
922impl Parser {
923    /// Create a new parser from source code
924    pub fn new(input: &str) -> Self {
925        Parser {
926            tokenizer: Tokenizer::new(input),
927            filename: None,
928        }
929    }
930
931    /// Create a new parser from source code with a filename for error reporting
932    pub fn new_with_filename(input: &str, filename: String) -> Self {
933        Parser {
934            tokenizer: Tokenizer::new_with_filename(input, filename.clone()),
935            filename: Some(filename),
936        }
937    }
938
939    /// Helper to create a ParseError with the current filename
940    fn error(&self, message: String, position: Position) -> ParseError {
941        if let Some(ref filename) = self.filename {
942            ParseError::with_filename(message, position, filename.clone())
943        } else {
944            ParseError::new(message, position)
945        }
946    }
947
948    /// Parse a single S-expression
949    ///
950    /// Returns `Ok(Value)` on success, `Err(ParseError)` on failure.
951    pub fn parse(&mut self) -> ParseResult<Value> {
952        self.parse_expr()
953    }
954
955    /// Peek at the next token without consuming it
956    ///
957    /// Returns `Ok(&Token)` if successful, `Err(ParseError)` on tokenizer error.
958    /// This is useful for checking if we're at EOF before attempting to parse.
959    pub fn peek_token(&mut self) -> ParseResult<&Token> {
960        self.tokenizer.peek_token()
961    }
962
963    /// Get the current position in the source code
964    ///
965    /// Returns the position of the last token consumed by the parser.
966    /// Useful for error reporting.
967    pub fn current_position(&self) -> Position {
968        self.tokenizer.position()
969    }
970
971    /// Parse all S-expressions in input
972    ///
973    /// Returns a list of all top-level expressions.
974    pub fn parse_all(&mut self) -> ParseResult<Vec<Value>> {
975        let mut exprs = Vec::new();
976
977        loop {
978            let tok = self.tokenizer.peek_token()?;
979            if *tok == Token::Eof {
980                break;
981            }
982            exprs.push(self.parse_expr()?);
983        }
984
985        Ok(exprs)
986    }
987
988    /// Parse a single expression
989    fn parse_expr(&mut self) -> ParseResult<Value> {
990        let start_pos = self.tokenizer.position();
991        let tok = self.tokenizer.next_token()?;
992
993        match tok {
994            // Literals
995            Token::Integer(n) => Ok(Value::integer(n)),
996            Token::Real(n) => Ok(Value::real(n)),
997            Token::Quantity(magnitude, suffix) => {
998                // Convert suffix string to Unit enum
999                if let Some(unit) = Unit::from_suffix(&suffix) {
1000                    Ok(Value::Quantity { magnitude, unit })
1001                } else {
1002                    Err(self.error(
1003                        format!("Invalid quantity unit: {}", suffix),
1004                        start_pos,
1005                    ))
1006                }
1007            }
1008            Token::String(s) => Ok(Value::string(s)),
1009            Token::Char(ch) => Ok(Value::char(ch)),
1010            Token::Bool(b) => Ok(Value::bool(b)),
1011            Token::Symbol(s) => Ok(Value::symbol(&s)),
1012            Token::Keyword(s) => Ok(Value::keyword(&s)),
1013
1014            // Lists
1015            Token::LeftParen | Token::LeftBracket => self.parse_list(start_pos),
1016
1017            // Vectors
1018            Token::VectorStart => self.parse_vector(start_pos),
1019
1020            // Quote
1021            Token::Quote => {
1022                let quoted = self.parse_expr()?;
1023                Ok(Value::cons_with_pos(Value::symbol("quote"), Value::cons(quoted, Value::Nil), start_pos))
1024            }
1025
1026            // Quasiquote
1027            Token::Quasiquote => {
1028                let quoted = self.parse_expr()?;
1029                Ok(Value::cons_with_pos(
1030                    Value::symbol("quasiquote"),
1031                    Value::cons(quoted, Value::Nil),
1032                    start_pos
1033                ))
1034            }
1035
1036            // Unquote
1037            Token::Unquote => {
1038                let quoted = self.parse_expr()?;
1039                Ok(Value::cons_with_pos(
1040                    Value::symbol("unquote"),
1041                    Value::cons(quoted, Value::Nil),
1042                    start_pos
1043                ))
1044            }
1045
1046            // Unquote-splicing
1047            Token::UnquoteSplicing => {
1048                let quoted = self.parse_expr()?;
1049                Ok(Value::cons_with_pos(
1050                    Value::symbol("unquote-splicing"),
1051                    Value::cons(quoted, Value::Nil),
1052                    start_pos
1053                ))
1054            }
1055
1056            // Unexpected tokens
1057            Token::RightParen | Token::RightBracket => Err(self.error(
1058                format!("Unexpected closing delimiter: {}", tok),
1059                start_pos,
1060            )),
1061
1062            Token::Dot => Err(self.error(
1063                "Unexpected dot outside of list".to_string(),
1064                start_pos,
1065            )),
1066
1067            Token::Eof => Err(self.error(
1068                "Unexpected end of input".to_string(),
1069                start_pos,
1070            )),
1071        }
1072    }
1073
1074    /// Parse a list (after opening paren consumed)
1075    fn parse_list(&mut self, start_pos: Position) -> ParseResult<Value> {
1076        let mut elements = Vec::new();
1077        let mut element_positions = Vec::new();
1078        let mut dotted_tail = None;
1079
1080        loop {
1081            let tok = self.tokenizer.peek_token()?;
1082
1083            match tok {
1084                Token::RightParen | Token::RightBracket => {
1085                    self.tokenizer.next_token()?; // Consume closing paren
1086                    break;
1087                }
1088
1089                Token::Dot => {
1090                    // Check if we have any elements before the dot
1091                    // A dot immediately after ( is invalid: (.foo) is not valid Scheme
1092                    // Valid dotted pairs require at least one element: (a . b)
1093                    if elements.is_empty() {
1094                        let dot_pos = self.tokenizer.position();
1095                        return Err(self.error(
1096                            "Invalid syntax: dot cannot appear immediately after opening parenthesis\n\
1097                            Note: Identifiers cannot start with '.' (dot character is reserved for dotted pairs)\n\
1098                            Example of valid dotted pair: (a . b)\n\
1099                            Example of invalid syntax: (.gitignore)".to_string(),
1100                            dot_pos,
1101                        ));
1102                    }
1103
1104                    self.tokenizer.next_token()?; // Consume dot
1105
1106                    // Parse the tail
1107                    dotted_tail = Some(self.parse_expr()?);
1108
1109                    // Expect closing paren
1110                    let tok = self.tokenizer.next_token()?;
1111                    if !matches!(tok, Token::RightParen | Token::RightBracket) {
1112                        return Err(self.error(
1113                            format!("Expected ) after dotted tail, got {}", tok),
1114                            start_pos,
1115                        ));
1116                    }
1117                    break;
1118                }
1119
1120                Token::Eof => {
1121                    return Err(self.error(
1122                        "Unexpected EOF in list".to_string(),
1123                        start_pos,
1124                    ))
1125                }
1126
1127                _ => {
1128                    // Capture the position BEFORE parsing each element
1129                    // This gives us the position where this element appears in source
1130                    let elem_pos = self.tokenizer.position();
1131                    elements.push(self.parse_expr()?);
1132                    element_positions.push(elem_pos);
1133                }
1134            }
1135        }
1136
1137        // Build the list from right to left, using each element's individual position
1138        let mut result = dotted_tail.unwrap_or(Value::Nil);
1139        for (elem, elem_pos) in elements.into_iter().zip(element_positions.into_iter()).rev() {
1140            result = Value::cons_with_pos(elem, result, elem_pos);
1141        }
1142
1143        Ok(result)
1144    }
1145
1146    /// Parse a vector (after #( consumed)
1147    fn parse_vector(&mut self, start_pos: Position) -> ParseResult<Value> {
1148        let mut elements = Vec::new();
1149
1150        loop {
1151            let tok = self.tokenizer.peek_token()?;
1152
1153            match tok {
1154                Token::RightParen => {
1155                    self.tokenizer.next_token()?; // Consume )
1156                    break;
1157                }
1158
1159                Token::Eof => {
1160                    return Err(self.error(
1161                        "Unexpected EOF in vector".to_string(),
1162                        start_pos,
1163                    ))
1164                }
1165
1166                _ => {
1167                    elements.push(self.parse_expr()?);
1168                }
1169            }
1170        }
1171
1172        Ok(Value::vector(elements))
1173    }
1174}
1175
1176// =============================================================================
1177// Tests
1178// =============================================================================
1179
1180#[cfg(test)]
1181mod tests {
1182    use super::*;
1183
1184    #[test]
1185    fn test_tokenize_simple() {
1186        let mut tok = Tokenizer::new("(+ 1 2)");
1187        assert_eq!(tok.next_token().unwrap(), Token::LeftParen);
1188        assert_eq!(tok.next_token().unwrap(), Token::Symbol("+".to_string()));
1189        assert_eq!(tok.next_token().unwrap(), Token::Integer(1));
1190        assert_eq!(tok.next_token().unwrap(), Token::Integer(2));
1191        assert_eq!(tok.next_token().unwrap(), Token::RightParen);
1192        assert_eq!(tok.next_token().unwrap(), Token::Eof);
1193    }
1194
1195    #[test]
1196    fn test_tokenize_whitespace_agnostic() {
1197        // This is the critical test that Steel fails!
1198        let input = r#"(let ((x 1)
1199                            (y 2))
1200                         (+ x y))"#;
1201        let mut tok = Tokenizer::new(input);
1202
1203        assert_eq!(tok.next_token().unwrap(), Token::LeftParen);
1204        assert_eq!(tok.next_token().unwrap(), Token::Symbol("let".to_string()));
1205        assert_eq!(tok.next_token().unwrap(), Token::LeftParen);
1206        assert_eq!(tok.next_token().unwrap(), Token::LeftParen);
1207        assert_eq!(tok.next_token().unwrap(), Token::Symbol("x".to_string()));
1208        assert_eq!(tok.next_token().unwrap(), Token::Integer(1));
1209        assert_eq!(tok.next_token().unwrap(), Token::RightParen);
1210        // Multi-line whitespace handled correctly!
1211        assert_eq!(tok.next_token().unwrap(), Token::LeftParen);
1212        assert_eq!(tok.next_token().unwrap(), Token::Symbol("y".to_string()));
1213        assert_eq!(tok.next_token().unwrap(), Token::Integer(2));
1214    }
1215
1216    #[test]
1217    fn test_tokenize_strings() {
1218        let mut tok = Tokenizer::new(r#""hello world""#);
1219        assert_eq!(
1220            tok.next_token().unwrap(),
1221            Token::String("hello world".to_string())
1222        );
1223
1224        let mut tok = Tokenizer::new(r#""with\nnewline""#);
1225        assert_eq!(
1226            tok.next_token().unwrap(),
1227            Token::String("with\nnewline".to_string())
1228        );
1229    }
1230
1231    #[test]
1232    fn test_tokenize_cdata() {
1233        // Test CDATA section parsing (OpenJade extension)
1234        let mut tok = Tokenizer::new(r#"<![CDATA[<!DOCTYPE HTML>]]>"#);
1235        assert_eq!(
1236            tok.next_token().unwrap(),
1237            Token::String("<!DOCTYPE HTML>".to_string())
1238        );
1239
1240        // Test CDATA with newlines
1241        let mut tok = Tokenizer::new("<![CDATA[\nLine 1\nLine 2\n]]>");
1242        assert_eq!(
1243            tok.next_token().unwrap(),
1244            Token::String("\nLine 1\nLine 2\n".to_string())
1245        );
1246
1247        // Test CDATA in expression context
1248        let mut tok = Tokenizer::new("(define x <![CDATA[test]]>)");
1249        assert_eq!(tok.next_token().unwrap(), Token::LeftParen);
1250        assert_eq!(tok.next_token().unwrap(), Token::Symbol("define".to_string()));
1251        assert_eq!(tok.next_token().unwrap(), Token::Symbol("x".to_string()));
1252        assert_eq!(tok.next_token().unwrap(), Token::String("test".to_string()));
1253        assert_eq!(tok.next_token().unwrap(), Token::RightParen);
1254    }
1255
1256    #[test]
1257    fn test_tokenize_comments() {
1258        let mut tok = Tokenizer::new("(+ 1 ; comment\n 2)");
1259        assert_eq!(tok.next_token().unwrap(), Token::LeftParen);
1260        assert_eq!(tok.next_token().unwrap(), Token::Symbol("+".to_string()));
1261        assert_eq!(tok.next_token().unwrap(), Token::Integer(1));
1262        // Comment skipped!
1263        assert_eq!(tok.next_token().unwrap(), Token::Integer(2));
1264        assert_eq!(tok.next_token().unwrap(), Token::RightParen);
1265    }
1266
1267    #[test]
1268    fn test_tokenize_block_comments() {
1269        let mut tok = Tokenizer::new("(+ 1 #| block comment |# 2)");
1270        assert_eq!(tok.next_token().unwrap(), Token::LeftParen);
1271        assert_eq!(tok.next_token().unwrap(), Token::Symbol("+".to_string()));
1272        assert_eq!(tok.next_token().unwrap(), Token::Integer(1));
1273        // Block comment skipped!
1274        assert_eq!(tok.next_token().unwrap(), Token::Integer(2));
1275        assert_eq!(tok.next_token().unwrap(), Token::RightParen);
1276    }
1277
1278    #[test]
1279    fn test_tokenize_booleans() {
1280        let mut tok = Tokenizer::new("#t #f");
1281        assert_eq!(tok.next_token().unwrap(), Token::Bool(true));
1282        assert_eq!(tok.next_token().unwrap(), Token::Bool(false));
1283    }
1284
1285    #[test]
1286    fn test_tokenize_characters() {
1287        let mut tok = Tokenizer::new(r#"#\a #\space #\newline"#);
1288        assert_eq!(tok.next_token().unwrap(), Token::Char('a'));
1289        assert_eq!(tok.next_token().unwrap(), Token::Char(' '));
1290        assert_eq!(tok.next_token().unwrap(), Token::Char('\n'));
1291    }
1292
1293    #[test]
1294    fn test_tokenize_hex_numbers() {
1295        // Lowercase #x
1296        let mut tok = Tokenizer::new("#xff");
1297        assert_eq!(tok.next_token().unwrap(), Token::Integer(255));
1298
1299        // Uppercase #X
1300        let mut tok = Tokenizer::new("#X10");
1301        assert_eq!(tok.next_token().unwrap(), Token::Integer(16));
1302
1303        // Mixed case
1304        let mut tok = Tokenizer::new("#xDEADBEEF");
1305        assert_eq!(tok.next_token().unwrap(), Token::Integer(0xDEADBEEF));
1306
1307        // Zero
1308        let mut tok = Tokenizer::new("#x0");
1309        assert_eq!(tok.next_token().unwrap(), Token::Integer(0));
1310    }
1311
1312    #[test]
1313    fn test_tokenize_octal_numbers() {
1314        // Lowercase #o
1315        let mut tok = Tokenizer::new("#o77");
1316        assert_eq!(tok.next_token().unwrap(), Token::Integer(63));
1317
1318        // Uppercase #O
1319        let mut tok = Tokenizer::new("#O10");
1320        assert_eq!(tok.next_token().unwrap(), Token::Integer(8));
1321
1322        // Zero
1323        let mut tok = Tokenizer::new("#o0");
1324        assert_eq!(tok.next_token().unwrap(), Token::Integer(0));
1325
1326        // Max valid octal digits
1327        let mut tok = Tokenizer::new("#o777");
1328        assert_eq!(tok.next_token().unwrap(), Token::Integer(511));
1329    }
1330
1331    #[test]
1332    fn test_tokenize_binary_numbers() {
1333        // Lowercase #b
1334        let mut tok = Tokenizer::new("#b1010");
1335        assert_eq!(tok.next_token().unwrap(), Token::Integer(10));
1336
1337        // Uppercase #B
1338        let mut tok = Tokenizer::new("#B1111");
1339        assert_eq!(tok.next_token().unwrap(), Token::Integer(15));
1340
1341        // Zero
1342        let mut tok = Tokenizer::new("#b0");
1343        assert_eq!(tok.next_token().unwrap(), Token::Integer(0));
1344
1345        // All ones
1346        let mut tok = Tokenizer::new("#b11111111");
1347        assert_eq!(tok.next_token().unwrap(), Token::Integer(255));
1348    }
1349
1350    #[test]
1351    fn test_tokenize_quote() {
1352        let mut tok = Tokenizer::new("'(1 2)");
1353        assert_eq!(tok.next_token().unwrap(), Token::Quote);
1354        assert_eq!(tok.next_token().unwrap(), Token::LeftParen);
1355        assert_eq!(tok.next_token().unwrap(), Token::Integer(1));
1356        assert_eq!(tok.next_token().unwrap(), Token::Integer(2));
1357        assert_eq!(tok.next_token().unwrap(), Token::RightParen);
1358    }
1359
1360    #[test]
1361    fn test_error_positions() {
1362        let mut tok = Tokenizer::new("(+ 1\n  \"unclosed string");
1363        tok.next_token().unwrap(); // (
1364        tok.next_token().unwrap(); // +
1365        tok.next_token().unwrap(); // 1
1366
1367        let err = tok.next_token().unwrap_err();
1368        assert_eq!(err.position.line, 2); // Error on line 2
1369        assert!(err.message.contains("EOF in string"));
1370    }
1371
1372    // =================================================================
1373    // Parser Tests
1374    // =================================================================
1375
1376    #[test]
1377    fn test_parse_integer() {
1378        let mut parser = Parser::new("42");
1379        let val = parser.parse().unwrap();
1380        assert!(val.is_integer());
1381        if let Value::Integer(n) = val {
1382            assert_eq!(n, 42);
1383        }
1384    }
1385
1386    #[test]
1387    fn test_parse_simple_list() {
1388        let mut parser = Parser::new("(+ 1 2)");
1389        let val = parser.parse().unwrap();
1390        assert!(val.is_list());
1391
1392        // Check structure: (+ 1 2)
1393        if let Value::Pair(ref p) = val {
1394            let pair = p.borrow();
1395            assert!(pair.car.is_symbol());
1396        }
1397    }
1398
1399    #[test]
1400    fn test_parse_nested_list() {
1401        let mut parser = Parser::new("(+ (* 2 3) 4)");
1402        let val = parser.parse().unwrap();
1403        assert!(val.is_list());
1404    }
1405
1406    #[test]
1407    fn test_parse_quoted() {
1408        let mut parser = Parser::new("'(1 2 3)");
1409        let val = parser.parse().unwrap();
1410
1411        // Should be (quote (1 2 3))
1412        if let Value::Pair(ref p) = val {
1413            let pair = p.borrow();
1414            if let Value::Symbol(s) = &pair.car {
1415                assert_eq!(&**s, "quote");
1416            } else {
1417                panic!("Expected symbol 'quote'");
1418            }
1419        } else {
1420            panic!("Expected pair");
1421        }
1422    }
1423
1424    #[test]
1425    fn test_parse_vector() {
1426        let mut parser = Parser::new("#(1 2 3)");
1427        let val = parser.parse().unwrap();
1428        assert!(val.is_vector());
1429
1430        if let Value::Vector(ref v) = val {
1431            let vec = v.borrow();
1432            assert_eq!(vec.len(), 3);
1433        }
1434    }
1435
1436    #[test]
1437    fn test_parse_dotted_list() {
1438        let mut parser = Parser::new("(1 . 2)");
1439        let val = parser.parse().unwrap();
1440
1441        if let Value::Pair(ref p) = val {
1442            let pair = p.borrow();
1443            assert!(matches!(pair.car, Value::Integer(1)));
1444            assert!(matches!(pair.cdr, Value::Integer(2)));
1445        } else {
1446            panic!("Expected pair");
1447        }
1448    }
1449
1450    #[test]
1451    fn test_parse_string() {
1452        let mut parser = Parser::new(r#""hello world""#);
1453        let val = parser.parse().unwrap();
1454        assert!(val.is_string());
1455    }
1456
1457    #[test]
1458    fn test_parse_bool() {
1459        let mut parser = Parser::new("#t");
1460        let val = parser.parse().unwrap();
1461        assert!(val.is_bool());
1462        assert!(val.is_true());
1463
1464        let mut parser = Parser::new("#f");
1465        let val = parser.parse().unwrap();
1466        assert!(val.is_bool());
1467        assert!(!val.is_true());
1468    }
1469
1470    #[test]
1471    fn test_parse_multiline_let() {
1472        // THE CRITICAL TEST: Multi-line let bindings (breaks Steel!)
1473        let input = r#"
1474            (let ((x 1)
1475                  (y 2))
1476              (+ x y))
1477        "#;
1478
1479        let mut parser = Parser::new(input);
1480        let val = parser.parse().unwrap();
1481        assert!(val.is_list());
1482
1483        // Should parse successfully despite multi-line formatting
1484        // This is what Steel cannot handle!
1485    }
1486
1487    #[test]
1488    fn test_parse_all() {
1489        let input = "(define x 1) (define y 2) (+ x y)";
1490        let mut parser = Parser::new(input);
1491        let exprs = parser.parse_all().unwrap();
1492        assert_eq!(exprs.len(), 3);
1493    }
1494
1495    #[test]
1496    fn test_parse_empty_list() {
1497        let mut parser = Parser::new("()");
1498        let val = parser.parse().unwrap();
1499        assert!(val.is_nil());
1500    }
1501
1502    #[test]
1503    fn test_parse_keyword() {
1504        let mut parser = Parser::new("#:foo");
1505        let val = parser.parse().unwrap();
1506        if let Value::Keyword(ref k) = val {
1507            assert_eq!(&**k, "foo");
1508        } else {
1509            panic!("Expected keyword");
1510        }
1511    }
1512
1513    #[test]
1514    fn test_parse_error_with_filename() {
1515        // Test that parser errors include the filename when provided
1516        let mut parser = Parser::new_with_filename("(define x", "test.scm".to_string());
1517        let err = parser.parse().unwrap_err();
1518        let err_string = err.to_string();
1519
1520        // Error should include filename
1521        assert!(err_string.contains("test.scm"), "Error should contain filename: {}", err_string);
1522        // Error should include line and column (error is at the opening paren position)
1523        assert!(err_string.contains("1:1"), "Error should contain position: {}", err_string);
1524        // Error should use format: filename:line:column:E: message
1525        assert!(err_string.contains("test.scm:1:1:E:"), "Error should use OpenJade format: {}", err_string);
1526    }
1527
1528    #[test]
1529    fn test_parse_error_without_filename() {
1530        // Test that parser errors still work without filename
1531        let mut parser = Parser::new("(define x");
1532        let err = parser.parse().unwrap_err();
1533        let err_string = err.to_string();
1534
1535        // Error should NOT include filename
1536        assert!(!err_string.contains("test.scm"), "Error should not contain filename when not provided");
1537        // But should still include position (error is at the opening paren position)
1538        assert!(err_string.contains("1:1"), "Error should still contain position: {}", err_string);
1539    }
1540
1541    #[test]
1542    fn test_parse_error_dot_after_open_paren() {
1543        // Test that (.gitignore) gives a clear error
1544        // This is invalid Scheme syntax - identifiers cannot start with dot
1545        let mut parser = Parser::new("(.gitignore)");
1546        let err = parser.parse().unwrap_err();
1547        let err_string = err.to_string();
1548
1549        // Error should mention that dot cannot appear after opening paren
1550        assert!(err_string.contains("dot cannot appear immediately after opening parenthesis"),
1551                "Error should mention invalid dot position: {}", err_string);
1552        // Error should mention that identifiers cannot start with dot
1553        assert!(err_string.contains("Identifiers cannot start with '.'"),
1554                "Error should explain why: {}", err_string);
1555    }
1556
1557    #[test]
1558    fn test_parse_error_dot_function_definition() {
1559        // Test that (define (.gitignore) ...) gives a clear error
1560        let mut parser = Parser::new("(define (.gitignore) (list))");
1561        let err = parser.parse().unwrap_err();
1562        let err_string = err.to_string();
1563
1564        // Should get same error about invalid dot syntax
1565        assert!(err_string.contains("dot cannot appear immediately after opening parenthesis"),
1566                "Error should mention invalid dot position: {}", err_string);
1567    }
1568
1569    #[test]
1570    fn test_parse_valid_dotted_pair_still_works() {
1571        // Verify that valid dotted pairs like (a . b) still work
1572        let mut parser = Parser::new("(a . b)");
1573        let val = parser.parse().unwrap();
1574
1575        // Should successfully parse as a dotted pair
1576        if let Value::Pair(ref p) = val {
1577            let pair = p.borrow();
1578            if let Value::Symbol(s) = &pair.car {
1579                assert_eq!(&**s, "a");
1580            } else {
1581                panic!("Expected symbol 'a'");
1582            }
1583            if let Value::Symbol(s) = &pair.cdr {
1584                assert_eq!(&**s, "b");
1585            } else {
1586                panic!("Expected symbol 'b'");
1587            }
1588        } else {
1589            panic!("Expected pair");
1590        }
1591    }
1592}
1593