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    fn peek(&self) -> &TokenKind {
89        // The lexer always terminates the stream with Eof, so this is in range.
90        &self.tokens[self.pos].kind
91    }
92
93    /// The token one past the cursor, used for the two-token `not in` operator.
94    /// Past the end (Eof is the last token) this reports Eof.
95    fn peek_next(&self) -> &TokenKind {
96        let next = (self.pos + 1).min(self.tokens.len() - 1);
97        &self.tokens[next].kind
98    }
99
100    fn current_span(&self) -> Span {
101        self.tokens[self.pos].span
102    }
103
104    fn advance(&mut self) -> Token {
105        let token = self.tokens[self.pos].clone();
106        if self.pos + 1 < self.tokens.len() {
107            self.pos += 1;
108        }
109        token
110    }
111
112    fn is(&self, kind: &TokenKind) -> bool {
113        self.peek() == kind
114    }
115
116    /// Consume a token of the given (payload-less) kind, or produce a
117    /// "expected X but found Y" diagnostic.
118    fn eat(&mut self, kind: TokenKind) -> Result<Token, Diagnostic> {
119        if self.peek() == &kind {
120            Ok(self.advance())
121        } else {
122            let span = self.current_span();
123            Err(self.diag(
124                span,
125                format!(
126                    "doge expected {} here, but found {}",
127                    kind.describe(),
128                    self.peek().describe()
129                ),
130            ))
131        }
132    }
133
134    /// Consume an identifier and return its name, or a friendly diagnostic.
135    fn eat_ident(&mut self, what: &str) -> Result<(String, Span), Diagnostic> {
136        let span = self.current_span();
137        if let TokenKind::Ident(name) = self.peek() {
138            let name = name.clone();
139            self.advance();
140            Ok((name, span))
141        } else {
142            Err(self.diag(
143                span,
144                format!(
145                    "doge expected {what} here, but found {}",
146                    self.peek().describe()
147                ),
148            ))
149        }
150    }
151
152    fn diag(&self, span: Span, message: impl Into<String>) -> Diagnostic {
153        let source_line = crate::diagnostics::source_line(&self.lines, span.line);
154        Diagnostic::new(&self.path, span.line, span.col, source_line, message)
155    }
156
157    fn missing_wow(&self, span: Span, what: &str) -> Diagnostic {
158        self.diag(span, format!("expected wow to close this {what}"))
159            .with_headline("very incomplete. such missing wow.")
160            .with_hint(
161                "every function, object, and script ends with wow (did the script end early?)",
162            )
163    }
164
165    fn parse_script(&mut self) -> Result<Script, Diagnostic> {
166        let mut stmts = Vec::new();
167        while !self.is(&TokenKind::Wow) {
168            if self.is(&TokenKind::Eof) {
169                return Err(self.missing_wow(self.current_span(), "script"));
170            }
171            stmts.push(self.parse_statement()?);
172        }
173        self.eat(TokenKind::Wow)?;
174        // The lexer always follows the `wow` line with a Newline.
175        if self.is(&TokenKind::Newline) {
176            self.advance();
177        }
178        if !self.is(&TokenKind::Eof) {
179            let span = self.current_span();
180            return Err(self
181                .diag(
182                    span,
183                    "doge stops reading at wow — nothing may come after it",
184                )
185                .with_headline("very extra. much after wow.")
186                .with_hint("remove the lines after wow, or move them above it"));
187        }
188        Ok(Script { stmts })
189    }
190
191    /// Parse a REPL snippet's statements, stopping at end-of-input or a top-level
192    /// `wow`. A statement that fails to parse ends the walk: a failure whose
193    /// current token is `Eof` means the snippet was truncated ([`Incomplete`]),
194    /// any other failure is a real [`Error`].
195    ///
196    /// [`Incomplete`]: ReplParse::Incomplete
197    /// [`Error`]: ReplParse::Error
198    fn parse_repl_script(&mut self) -> ReplParse {
199        let mut stmts = Vec::new();
200        while !self.is(&TokenKind::Eof) && !self.is(&TokenKind::Wow) {
201            match self.parse_statement() {
202                Ok(stmt) => stmts.push(stmt),
203                Err(diag) => {
204                    return if self.is(&TokenKind::Eof) {
205                        ReplParse::Incomplete(diag)
206                    } else {
207                        ReplParse::Error(diag)
208                    };
209                }
210            }
211        }
212        ReplParse::Complete(Script { stmts })
213    }
214
215    fn python_habit(&self, message: &str, hint: &str) -> Diagnostic {
216        let span = self.current_span();
217        self.diag(span, message)
218            .with_headline("very python. much habit.")
219            .with_hint(hint)
220    }
221}