Skip to main content

powerio_dist/glm/
syntax.rs

1use std::ops::Range;
2
3use powerio_core::{Error, Source, SourceBuffer, SourceSpan};
4
5use crate::diagnostics::codes;
6
7const MAX_BYTES: usize = 64 << 20;
8const MAX_TOKENS: usize = 1 << 20;
9const MAX_TOKEN_BYTES: usize = 1 << 20;
10const MAX_NESTING: usize = 64;
11
12/// Lexical category. Words keep punctuation used in units and complex literals.
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14#[non_exhaustive]
15pub enum TokenKind {
16    Word,
17    Quoted,
18    OpenBrace,
19    CloseBrace,
20    Semicolon,
21    Directive,
22}
23
24/// A token borrows its bytes through a shared source-buffer owner.
25#[derive(Clone, Debug)]
26pub struct Token {
27    kind: TokenKind,
28    buffer: SourceBuffer,
29    range: Range<usize>,
30}
31
32impl Token {
33    #[must_use]
34    pub const fn kind(&self) -> TokenKind {
35        self.kind
36    }
37
38    /// Exact spelling, including quotes, escapes, or a directive's leading `#`.
39    #[must_use]
40    #[expect(
41        clippy::missing_panics_doc,
42        reason = "private token fields are created only from validated UTF-8 ranges"
43    )]
44    pub fn text(&self) -> &str {
45        // Only the UTF-8-validated lexer constructs tokens; all token
46        // boundaries are ASCII delimiters or complete UTF-8 character edges.
47        std::str::from_utf8(&self.buffer.bytes()[self.range.clone()])
48            .expect("GLM token boundaries remain valid UTF-8")
49    }
50
51    #[must_use]
52    pub fn span(&self) -> SourceSpan {
53        span(&self.buffer, self.range.clone())
54    }
55}
56
57/// Source syntax retains duplicate properties and unknown declarations in order.
58#[derive(Clone, Debug)]
59#[non_exhaustive]
60pub enum Statement {
61    /// A statement ending in a semicolon, such as a property or module import.
62    Simple {
63        tokens: Vec<Token>,
64        span: SourceSpan,
65    },
66    /// A brace-delimited declaration, including nested and inline objects.
67    Block {
68        header: Vec<Token>,
69        body: Vec<Statement>,
70        span: SourceSpan,
71    },
72    /// A preprocessing line, retained without executing or evaluating it.
73    Directive(Token),
74}
75
76/// Parsed syntax with its source owner. Source bytes, comments and whitespace
77/// remain available exactly as supplied; the tree does not replace those bytes.
78#[derive(Clone, Debug)]
79pub struct RawGlm {
80    pub source: Source,
81    pub statements: Vec<Statement>,
82}
83
84/// Parse one primary GLM buffer without preprocessing or electrical projection.
85/// Includes and conditionals remain directive statements in this syntax API.
86/// Input is bounded to 64 MiB, one million tokens, 1 MiB per token and 64
87/// nested blocks. All spans address the original bytes, including a UTF-8 BOM.
88pub fn parse_raw_source(source: Source) -> Result<RawGlm, Error> {
89    let result = (|| {
90        let buffer = source.primary_buffer()?;
91        let tokens = lex(&buffer)?;
92        let mut parser = Parser {
93            buffer: &buffer,
94            tokens: tokens.into_iter().peekable(),
95        };
96        parser.statements(0, None)
97    })();
98    match result {
99        Ok(statements) => Ok(RawGlm { source, statements }),
100        Err(error) => Err(error.with_source(source)),
101    }
102}
103
104fn span(buffer: &SourceBuffer, range: Range<usize>) -> SourceSpan {
105    SourceSpan::new(buffer.id().clone(), range.start as u64, range.end as u64)
106        .expect("lexer ranges are ordered")
107}
108
109fn malformed(buffer: &SourceBuffer, range: Range<usize>, message: &str) -> Error {
110    Error::new(&codes::PARSE_GLM_SYNTAX, message).with_span(span(buffer, range))
111}
112
113fn limited(buffer: &SourceBuffer, range: Range<usize>, message: &str) -> Error {
114    Error::new(&codes::PARSE_GLM_LIMIT_EXCEEDED, message).with_span(span(buffer, range))
115}
116
117fn lex(buffer: &SourceBuffer) -> Result<Vec<Token>, Error> {
118    let bytes = buffer.bytes();
119    if bytes.len() > MAX_BYTES {
120        return Err(limited(buffer, 0..bytes.len(), "GLM input exceeds 64 MiB"));
121    }
122    std::str::from_utf8(bytes).map_err(|error| {
123        let start = error.valid_up_to();
124        malformed(
125            buffer,
126            start..start + error.error_len().unwrap_or(1),
127            "GLM source is not UTF-8",
128        )
129    })?;
130    if let Some(offset) = bytes.iter().position(|byte| *byte == 0) {
131        return Err(malformed(
132            buffer,
133            offset..offset + 1,
134            "GLM source contains a NUL byte",
135        ));
136    }
137    let mut tokens = Vec::new();
138    let mut cursor = if buffer.has_utf8_bom() { 3 } else { 0 };
139    let mut line_start = true;
140    while cursor < bytes.len() {
141        let start = cursor;
142        if bytes[cursor].is_ascii_whitespace() {
143            if matches!(bytes[cursor], b'\n' | b'\r') {
144                line_start = true;
145            }
146            cursor += 1;
147            continue;
148        }
149        if bytes[cursor..].starts_with(b"//") {
150            cursor += 2;
151            while cursor < bytes.len() && !matches!(bytes[cursor], b'\n' | b'\r') {
152                cursor += 1;
153            }
154            continue;
155        }
156        if bytes[cursor..].starts_with(b"/*") {
157            cursor += 2;
158            while cursor < bytes.len() && !bytes[cursor..].starts_with(b"*/") {
159                if matches!(bytes[cursor], b'\n' | b'\r') {
160                    line_start = true;
161                }
162                cursor += 1;
163            }
164            if cursor == bytes.len() {
165                return Err(malformed(
166                    buffer,
167                    start..cursor,
168                    "unterminated GLM block comment",
169                ));
170            }
171            cursor += 2;
172            continue;
173        }
174        let kind = match bytes[cursor] {
175            b'{' => {
176                cursor += 1;
177                TokenKind::OpenBrace
178            }
179            b'}' => {
180                cursor += 1;
181                TokenKind::CloseBrace
182            }
183            b';' => {
184                cursor += 1;
185                TokenKind::Semicolon
186            }
187            b'#' if line_start => {
188                while cursor < bytes.len() && !matches!(bytes[cursor], b'\n' | b'\r') {
189                    cursor += 1;
190                }
191                TokenKind::Directive
192            }
193            b'\'' | b'"' => {
194                cursor = quoted_end(buffer, start)?;
195                TokenKind::Quoted
196            }
197            _ => {
198                while cursor < bytes.len() {
199                    if bytes[cursor..].starts_with(b"${") {
200                        cursor += 2;
201                        while cursor < bytes.len() && bytes[cursor] != b'}' {
202                            cursor += 1;
203                        }
204                        if cursor == bytes.len() {
205                            return Err(malformed(
206                                buffer,
207                                start..cursor,
208                                "unterminated GLM variable reference",
209                            ));
210                        }
211                        cursor += 1;
212                    } else if bytes[cursor].is_ascii_whitespace()
213                        || matches!(bytes[cursor], b'{' | b'}' | b';' | b'\'' | b'"')
214                        || bytes[cursor..].starts_with(b"//")
215                        || bytes[cursor..].starts_with(b"/*")
216                    {
217                        break;
218                    } else {
219                        cursor += 1;
220                    }
221                }
222                TokenKind::Word
223            }
224        };
225        line_start = false;
226        if cursor - start > MAX_TOKEN_BYTES || tokens.len() == MAX_TOKENS {
227            return Err(limited(
228                buffer,
229                start..cursor,
230                "GLM token size or count exceeds its limit",
231            ));
232        }
233        tokens.push(Token {
234            kind,
235            buffer: buffer.clone(),
236            range: start..cursor,
237        });
238    }
239    Ok(tokens)
240}
241
242fn quoted_end(buffer: &SourceBuffer, start: usize) -> Result<usize, Error> {
243    let bytes = buffer.bytes();
244    let quote = bytes[start];
245    let mut cursor = start + 1;
246    while cursor < bytes.len() {
247        let byte = bytes[cursor];
248        cursor += 1;
249        if byte == quote {
250            return Ok(cursor);
251        }
252        if byte == b'\\' && cursor < bytes.len() {
253            cursor += 1;
254        }
255    }
256    Err(malformed(
257        buffer,
258        start..bytes.len(),
259        "unterminated GLM quoted token",
260    ))
261}
262
263struct Parser<'a> {
264    buffer: &'a SourceBuffer,
265    tokens: std::iter::Peekable<std::vec::IntoIter<Token>>,
266}
267
268impl Parser<'_> {
269    fn statements(
270        &mut self,
271        depth: usize,
272        opening: Option<usize>,
273    ) -> Result<Vec<Statement>, Error> {
274        let mut output = Vec::new();
275        let mut header = Vec::<Token>::new();
276        if opening.is_some()
277            && self
278                .tokens
279                .peek()
280                .is_some_and(|t| t.kind == TokenKind::CloseBrace)
281        {
282            return Ok(output);
283        }
284        while let Some(token) = self.tokens.next() {
285            match token.kind {
286                TokenKind::Directive => {
287                    if !header.is_empty() {
288                        return Err(malformed(
289                            self.buffer,
290                            token.range,
291                            "directive interrupts a GLM statement",
292                        ));
293                    }
294                    output.push(Statement::Directive(token));
295                }
296                TokenKind::Word | TokenKind::Quoted => header.push(token),
297                TokenKind::Semicolon => {
298                    if let Some(first) = header.first() {
299                        output.push(Statement::Simple {
300                            span: span(self.buffer, first.range.start..token.range.end),
301                            tokens: std::mem::take(&mut header),
302                        });
303                    }
304                }
305                TokenKind::OpenBrace => {
306                    output.push(self.block(std::mem::take(&mut header), token, depth)?);
307                }
308                TokenKind::CloseBrace => {
309                    return Err(malformed(
310                        self.buffer,
311                        token.range,
312                        "unexpected GLM closing brace",
313                    ));
314                }
315            }
316            if opening.is_some()
317                && self
318                    .tokens
319                    .peek()
320                    .is_some_and(|t| t.kind == TokenKind::CloseBrace)
321            {
322                if let Some(first) = header.first() {
323                    return Err(malformed(
324                        self.buffer,
325                        first.range.clone(),
326                        "GLM property is missing its semicolon",
327                    ));
328                }
329                return Ok(output);
330            }
331        }
332        if let Some(start) = opening {
333            return Err(malformed(
334                self.buffer,
335                start..self.buffer.bytes().len(),
336                "unterminated GLM block",
337            ));
338        }
339        if let Some(first) = header.first() {
340            return Err(malformed(
341                self.buffer,
342                first.range.clone(),
343                "GLM statement is missing its semicolon or block",
344            ));
345        }
346        Ok(output)
347    }
348
349    fn block(
350        &mut self,
351        header: Vec<Token>,
352        opening: Token,
353        depth: usize,
354    ) -> Result<Statement, Error> {
355        let Some(first) = header.first() else {
356            return Err(malformed(
357                self.buffer,
358                opening.range,
359                "GLM block has no declaration",
360            ));
361        };
362        if depth == MAX_NESTING {
363            return Err(limited(
364                self.buffer,
365                opening.range,
366                "GLM blocks exceed 64 nesting levels",
367            ));
368        }
369        let start = first.range.start;
370        let body = self.statements(depth + 1, Some(opening.range.start))?;
371        // The recursive call leaves the matching closing brace.
372        let closing = self
373            .tokens
374            .next()
375            .expect("a successful nested parse leaves a brace");
376        let end = if self
377            .tokens
378            .peek()
379            .is_some_and(|t| t.kind == TokenKind::Semicolon)
380        {
381            self.tokens.next().expect("peeked token exists").range.end
382        } else {
383            closing.range.end
384        };
385        Ok(Statement::Block {
386            header,
387            body,
388            span: span(self.buffer, start..end),
389        })
390    }
391}