Skip to main content

doge_compiler/parser/
mod.rs

1pub(super) use crate::ast::{BinOp, Expr, InterpPart, Param, Params, Script, Stmt, UnOp};
2pub(super) use crate::diagnostics::Diagnostic;
3use crate::lexer;
4pub(super) use crate::token::{Span, StrSegment, Token, TokenKind};
5
6mod expr;
7mod stmt;
8#[cfg(test)]
9mod tests;
10
11pub fn parse(path: &str, source: &str) -> Result<Script, Diagnostic> {
12    let tokens = lexer::lex(path, source)?;
13    Parser::new(path, source, tokens).parse_script()
14}
15
16/// The outcome of parsing an interactive REPL snippet.
17pub enum ReplParse {
18    /// A snippet that parsed cleanly and is ready to run.
19    Complete(Script),
20    /// A snippet that is not finished — a half-typed block or definition, or an
21    /// open bracket/string — carrying the diagnostic it would raise if forced.
22    /// The REPL keeps reading lines until it completes.
23    Incomplete(Diagnostic),
24    /// A genuine syntax error, which the REPL reports and moves past.
25    Error(Diagnostic),
26}
27
28/// Parse a REPL snippet. Unlike [`parse`], the top level does not require a
29/// closing `wow`, so a clean parse to end-of-input is [`ReplParse::Complete`]. A
30/// parse that fails *at* end-of-input — the snippet was cut off mid-construct —
31/// is [`ReplParse::Incomplete`] so the caller can read another line; any failure
32/// at a real token is [`ReplParse::Error`]. A leading top-level `wow` ends the
33/// snippet, so the statements before it parse as complete.
34pub fn parse_repl(path: &str, source: &str) -> ReplParse {
35    let tokens = match lexer::lex(path, source) {
36        Ok(tokens) => tokens,
37        Err(diag) => {
38            return if incomplete_lex(&diag) {
39                ReplParse::Incomplete(diag)
40            } else {
41                ReplParse::Error(diag)
42            };
43        }
44    };
45    Parser::new(path, source, tokens).parse_repl_script()
46}
47
48/// A lexer error that only means "the snippet is not finished yet": input ran
49/// out while a bracket, string, or `{…}` interpolation hole was still open, each
50/// of which a later line can close.
51fn incomplete_lex(diag: &Diagnostic) -> bool {
52    matches!(
53        diag.headline.as_str(),
54        "very open. much bracket." | "very string. much unfinished." | "very hole. much open."
55    )
56}
57
58struct Parser {
59    path: String,
60    lines: Vec<String>,
61    tokens: Vec<Token>,
62    pos: usize,
63}
64
65impl Parser {
66    fn new(path: &str, source: &str, tokens: Vec<Token>) -> Parser {
67        let lines = crate::diagnostics::split_source_lines(source);
68        Parser {
69            path: path.to_string(),
70            lines,
71            tokens,
72            pos: 0,
73        }
74    }
75
76    /// A fresh parser over `tokens`, sharing this parser's path and source lines
77    /// so diagnostics still render the right file and line. Used to parse the
78    /// expression inside a `{…}` interpolation hole.
79    fn sub(&self, tokens: Vec<Token>) -> Parser {
80        Parser {
81            path: self.path.clone(),
82            lines: self.lines.clone(),
83            tokens,
84            pos: 0,
85        }
86    }
87
88    // ----- token cursor helpers -----
89
90    fn peek(&self) -> &TokenKind {
91        // The lexer always terminates the stream with Eof, so this is in range.
92        &self.tokens[self.pos].kind
93    }
94
95    /// The token one past the cursor, used for the two-token `not in` operator.
96    /// Past the end (Eof is the last token) this reports Eof.
97    fn peek_next(&self) -> &TokenKind {
98        let next = (self.pos + 1).min(self.tokens.len() - 1);
99        &self.tokens[next].kind
100    }
101
102    fn current_span(&self) -> Span {
103        self.tokens[self.pos].span
104    }
105
106    fn advance(&mut self) -> Token {
107        let token = self.tokens[self.pos].clone();
108        if self.pos + 1 < self.tokens.len() {
109            self.pos += 1;
110        }
111        token
112    }
113
114    fn is(&self, kind: &TokenKind) -> bool {
115        self.peek() == kind
116    }
117
118    /// Consume a token of the given (payload-less) kind, or produce a
119    /// "expected X but found Y" diagnostic.
120    fn eat(&mut self, kind: TokenKind) -> Result<Token, Diagnostic> {
121        if self.peek() == &kind {
122            Ok(self.advance())
123        } else {
124            let span = self.current_span();
125            Err(self.diag(
126                span,
127                format!(
128                    "doge expected {} here, but found {}",
129                    kind.describe(),
130                    self.peek().describe()
131                ),
132            ))
133        }
134    }
135
136    /// Consume an identifier and return its name, or a friendly diagnostic.
137    fn eat_ident(&mut self, what: &str) -> Result<(String, Span), Diagnostic> {
138        let span = self.current_span();
139        if let TokenKind::Ident(name) = self.peek() {
140            let name = name.clone();
141            self.advance();
142            Ok((name, span))
143        } else {
144            Err(self.diag(
145                span,
146                format!(
147                    "doge expected {what} here, but found {}",
148                    self.peek().describe()
149                ),
150            ))
151        }
152    }
153
154    fn diag(&self, span: Span, message: impl Into<String>) -> Diagnostic {
155        let source_line = crate::diagnostics::source_line(&self.lines, span.line);
156        Diagnostic::new(&self.path, span.line, span.col, source_line, message)
157    }
158
159    fn missing_wow(&self, span: Span, what: &str) -> Diagnostic {
160        self.diag(span, format!("expected wow to close this {what}"))
161            .with_headline("very incomplete. such missing wow.")
162            .with_hint(
163                "every function, object, and script ends with wow (did the script end early?)",
164            )
165    }
166
167    // ----- top level -----
168
169    fn parse_script(&mut self) -> Result<Script, Diagnostic> {
170        let mut stmts = Vec::new();
171        while !self.is(&TokenKind::Wow) {
172            if self.is(&TokenKind::Eof) {
173                return Err(self.missing_wow(self.current_span(), "script"));
174            }
175            stmts.push(self.parse_statement()?);
176        }
177        self.eat(TokenKind::Wow)?;
178        // The lexer always follows the `wow` line with a Newline.
179        if self.is(&TokenKind::Newline) {
180            self.advance();
181        }
182        if !self.is(&TokenKind::Eof) {
183            let span = self.current_span();
184            return Err(self
185                .diag(
186                    span,
187                    "doge stops reading at wow — nothing may come after it",
188                )
189                .with_headline("very extra. much after wow.")
190                .with_hint("remove the lines after wow, or move them above it"));
191        }
192        Ok(Script { stmts })
193    }
194
195    /// Parse a REPL snippet's statements, stopping at end-of-input or a top-level
196    /// `wow`. A statement that fails to parse ends the walk: a failure whose
197    /// current token is `Eof` means the snippet was truncated ([`Incomplete`]),
198    /// any other failure is a real [`Error`].
199    ///
200    /// [`Incomplete`]: ReplParse::Incomplete
201    /// [`Error`]: ReplParse::Error
202    fn parse_repl_script(&mut self) -> ReplParse {
203        let mut stmts = Vec::new();
204        while !self.is(&TokenKind::Eof) && !self.is(&TokenKind::Wow) {
205            match self.parse_statement() {
206                Ok(stmt) => stmts.push(stmt),
207                Err(diag) => {
208                    return if self.is(&TokenKind::Eof) {
209                        ReplParse::Incomplete(diag)
210                    } else {
211                        ReplParse::Error(diag)
212                    };
213                }
214            }
215        }
216        ReplParse::Complete(Script { stmts })
217    }
218
219    fn python_habit(&self, message: &str, hint: &str) -> Diagnostic {
220        let span = self.current_span();
221        self.diag(span, message)
222            .with_headline("very python. much habit.")
223            .with_hint(hint)
224    }
225}