Rust · 4987 bytes Raw Blame History
1 //! Recursive descent Fortran parser.
2 //!
3 //! Produces a typed AST from a token stream. No parser generators.
4 //! Expression parsing uses a Pratt parser with 12 precedence levels
5 //! matching the Fortran standard exactly.
6
7 pub mod decl;
8 pub mod expr;
9 pub mod stmt;
10 pub mod unit;
11
12 use crate::lexer::{Span, Token, TokenKind};
13
14 use std::fmt;
15
16 /// Parser error.
17 #[derive(Debug, Clone)]
18 pub struct ParseError {
19 pub span: Span,
20 pub msg: String,
21 }
22
23 impl fmt::Display for ParseError {
24 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25 write!(
26 f,
27 "{}:{}: error: {}",
28 self.span.start.line, self.span.start.col, self.msg
29 )
30 }
31 }
32
33 impl std::error::Error for ParseError {}
34
35 /// The parser state — holds the token stream and current position.
36 pub struct Parser<'a> {
37 tokens: &'a [Token],
38 pos: usize,
39 expr_depth: usize,
40 }
41
42 impl<'a> Parser<'a> {
43 pub fn new(tokens: &'a [Token]) -> Self {
44 Self {
45 tokens,
46 pos: 0,
47 expr_depth: 0,
48 }
49 }
50
51 /// Peek at the current token kind without consuming.
52 pub fn peek(&self) -> &TokenKind {
53 if self.pos < self.tokens.len() {
54 &self.tokens[self.pos].kind
55 } else {
56 &TokenKind::Eof
57 }
58 }
59
60 /// Peek at the kind of the token `n` positions ahead of the cursor.
61 /// Returns `None` past the end of the stream.
62 pub fn peek_kind_at(&self, n: usize) -> Option<&TokenKind> {
63 self.tokens.get(self.pos + n).map(|t| &t.kind)
64 }
65
66 /// Peek at the current token (with span info).
67 pub fn current(&self) -> &Token {
68 if self.pos < self.tokens.len() {
69 &self.tokens[self.pos]
70 } else if let Some(last) = self.tokens.last() {
71 last
72 } else {
73 // Should never happen — tokenize always produces at least Eof.
74 panic!("parser created with empty token stream");
75 }
76 }
77
78 /// Peek at the current token's text.
79 pub fn peek_text(&self) -> &str {
80 if self.pos < self.tokens.len() {
81 &self.tokens[self.pos].text
82 } else {
83 ""
84 }
85 }
86
87 /// Advance past the current token and return it.
88 pub fn advance(&mut self) -> &Token {
89 let tok = &self.tokens[self.pos.min(self.tokens.len() - 1)];
90 if self.pos < self.tokens.len() {
91 self.pos += 1;
92 }
93 tok
94 }
95
96 /// Advance if the current token matches the expected kind. Returns true if consumed.
97 pub fn eat(&mut self, kind: &TokenKind) -> bool {
98 if self.peek() == kind {
99 self.advance();
100 true
101 } else {
102 false
103 }
104 }
105
106 /// Advance if the current token is an identifier with the given text (case-insensitive).
107 pub fn eat_ident(&mut self, name: &str) -> bool {
108 if self.peek() == &TokenKind::Identifier && self.peek_text().eq_ignore_ascii_case(name) {
109 self.advance();
110 true
111 } else {
112 false
113 }
114 }
115
116 /// Expect the current token to match, or return an error.
117 pub fn expect(&mut self, kind: &TokenKind) -> Result<&Token, ParseError> {
118 if self.peek() == kind {
119 Ok(self.advance())
120 } else {
121 Err(self.error(format!("expected {}, got {}", kind, self.peek())))
122 }
123 }
124
125 /// Skip statement boundaries (newlines, comments, and semicolons).
126 /// F2018 §6.3.2: a semicolon separates statements on the same line
127 /// and is semantically equivalent to a newline.
128 pub fn skip_newlines(&mut self) {
129 while matches!(
130 self.peek(),
131 TokenKind::Newline | TokenKind::Comment | TokenKind::Semicolon
132 ) {
133 self.advance();
134 }
135 }
136
137 /// Create an error at the current position.
138 pub fn error(&self, msg: String) -> ParseError {
139 ParseError {
140 span: self.current().span,
141 msg,
142 }
143 }
144
145 /// Get the span of the current token.
146 pub fn current_span(&self) -> Span {
147 self.current().span
148 }
149
150 /// Get the span of the previous token.
151 pub fn prev_span(&self) -> Span {
152 if self.pos > 0 {
153 self.tokens[self.pos - 1].span
154 } else {
155 self.current_span()
156 }
157 }
158
159 /// Check if we're at a statement-ending token.
160 pub fn at_stmt_end(&self) -> bool {
161 matches!(
162 self.peek(),
163 TokenKind::Newline | TokenKind::Semicolon | TokenKind::Eof | TokenKind::Comment
164 )
165 }
166
167 /// Check if the token at offset `n` from current position is a statement terminator.
168 pub fn at_stmt_end_after(&self, n: usize) -> bool {
169 let idx = self.pos + n;
170 if idx >= self.tokens.len() {
171 return true;
172 }
173 matches!(
174 self.tokens[idx].kind,
175 TokenKind::Newline | TokenKind::Semicolon | TokenKind::Eof | TokenKind::Comment
176 )
177 }
178 }
179