Skip to main content

zerodds_idl_parse/
parser.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! The recursive-descent driver. Spec §3.
4//!
5//! The parser never returns `Err` for a syntax error. It records a
6//! diagnostic, wraps the offending range in an error node, and
7//! resynchronises — because a frontend that stops at the first mistake
8//! makes a large file take as many runs as it has mistakes.
9
10use crate::diagnostic::{Diagnostic, DiagnosticCode};
11use alloc::format;
12use alloc::string::{String, ToString};
13use alloc::sync::Arc;
14use alloc::vec::Vec;
15use zerodds_idl_cst::{
16    Checkpoint, CstKind, GreenNode, Span, SyntaxNode, Token, TokenKind, TreeBuilder, lex,
17};
18
19/// What came out of parsing a file.
20#[derive(Debug, Clone)]
21pub struct Parse {
22    root: Arc<GreenNode>,
23    diagnostics: Vec<Diagnostic>,
24    /// Where each piece of the tree's text came from, in tree order: the
25    /// piece's offset in the tree and its range in the source the parser
26    /// read. See [`Parse::source_span`].
27    placed: Vec<(u32, Span)>,
28}
29
30impl Parse {
31    /// The tree, always present — a file that fails to parse still has
32    /// one, with its bad ranges as error nodes.
33    #[must_use]
34    pub fn syntax(&self) -> Arc<SyntaxNode> {
35        Arc::new(SyntaxNode::root(self.root.clone()))
36    }
37
38    /// The green root.
39    #[must_use]
40    pub fn green(&self) -> &Arc<GreenNode> {
41        &self.root
42    }
43
44    /// Everything wrong with the input, in source order.
45    #[must_use]
46    pub fn diagnostics(&self) -> &[Diagnostic] {
47        &self.diagnostics
48    }
49
50    /// Whether anything was wrong.
51    #[must_use]
52    pub fn has_errors(&self) -> bool {
53        self.diagnostics.iter().any(Diagnostic::is_error)
54    }
55
56    /// Put diagnostics ahead of the ones already there.
57    ///
58    /// For the preprocessor's, which explain most of what the parser
59    /// then makes of the result.
60    pub fn prepend_diagnostics(&mut self, diagnostics: Vec<Diagnostic>) {
61        let mut all = diagnostics;
62        all.append(&mut self.diagnostics);
63        self.diagnostics = all;
64    }
65
66    /// Reconstruct the source. Equals the input byte for byte, whether
67    /// or not it parsed. Spec §2.2.
68    #[must_use]
69    pub fn text(&self) -> String {
70        self.root.text()
71    }
72
73    /// A range of the tree as a range of the source the parser read.
74    ///
75    /// The same range for a file parsed with [`parse`], whose tree is the
76    /// file byte for byte. Not for [`parse_preprocessed`]: the expanded
77    /// text puts one space between two tokens and the tree holds the
78    /// tokens without it, and a pragma's text stands after the token it
79    /// leads there and before it here. A node's range in the tree — what
80    /// lowering records — is an offset into neither file nor expanded
81    /// text until it passes through this.
82    ///
83    /// Each end is moved by the piece of text that holds it; an end past
84    /// the last piece stays at the end of that piece.
85    #[must_use]
86    pub fn source_span(&self, tree: Span) -> Span {
87        let start = self.source_offset(tree.start, false);
88        let end = if tree.end > tree.start {
89            self.source_offset(tree.end, true).max(start)
90        } else {
91            start
92        };
93        Span::new(start, end)
94    }
95
96    /// One offset of the tree in the source. `end` reads the offset as
97    /// the end of a range, so it belongs to the piece that holds the
98    /// byte before it.
99    fn source_offset(&self, offset: u32, end: bool) -> u32 {
100        let probe = if end {
101            offset.saturating_sub(1)
102        } else {
103            offset
104        };
105        let Some(index) = self
106            .placed
107            .partition_point(|(start, _)| *start <= probe)
108            .checked_sub(1)
109        else {
110            return self
111                .placed
112                .first()
113                .map_or(offset, |(_, source)| source.start);
114        };
115        let (start, source) = self.placed[index];
116        source
117            .start
118            .saturating_add(offset.saturating_sub(start))
119            .min(source.end)
120    }
121}
122
123/// Parse a file.
124#[must_use]
125pub fn parse(source: &str) -> Parse {
126    parse_with(source, crate::Dialect::ALL_VENDORS)
127}
128
129/// Parse under a chosen dialect.
130///
131/// [`parse`] admits every vendor extension, which is what a tool reading
132/// files it did not write needs. `Dialect::STRICT` is OMG IDL 4.2 and
133/// nothing else: a file that parses under it is one every vendor's
134/// compiler also reads. Spec §3.5.
135#[must_use]
136pub fn parse_with(source: &str, dialect: crate::Dialect) -> Parse {
137    let mut parser = Parser::new(source);
138    parser.dialect = dialect;
139    parser.parse_file()
140}
141
142/// Parse a preprocessed stream.
143///
144/// The tree covers the expanded text, not the file as written: an
145/// `#include` brought other files into it and a conditional cut parts of
146/// it out. [`parse`] is the byte-exact path; this is the one that sees
147/// what a compiler would.
148///
149/// The preprocessor's own diagnostics come first, because a file that
150/// failed to include something explains most of what follows.
151#[must_use]
152pub fn parse_preprocessed(preprocessed: &crate::preprocess::Preprocessed) -> Parse {
153    parse_preprocessed_with(preprocessed, crate::Dialect::ALL_VENDORS)
154}
155
156/// Parse a preprocessed stream under a chosen dialect.
157///
158/// [`parse_preprocessed`] admits every vendor extension. This is the
159/// entry `--strict` reaches: `Dialect::STRICT` turns every vendor
160/// construct from a warning into an error, so a file that gets through
161/// is one every other vendor's front end reads too. Spec §3.5.
162#[must_use]
163pub fn parse_preprocessed_with(
164    preprocessed: &crate::preprocess::Preprocessed,
165    dialect: crate::Dialect,
166) -> Parse {
167    let mut parser = Parser::over(&preprocessed.text, preprocessed.tokens.clone());
168    parser.dialect = dialect;
169    let mut parse = parser.parse_file();
170    parse.prepend_diagnostics(preprocessed.diagnostics.clone());
171    parse
172}
173
174/// The parser state.
175pub struct Parser<'a> {
176    pub(crate) source: &'a str,
177    pub(crate) tokens: Vec<Token>,
178    pub(crate) at: usize,
179    pub(crate) builder: TreeBuilder,
180    pub(crate) diagnostics: Vec<Diagnostic>,
181    /// Which dialect this parse accepts. Spec §3.5.
182    pub(crate) dialect: crate::Dialect,
183    /// Guards against a rule that consumes nothing being retried
184    /// forever. Bumped whenever the cursor moves.
185    steps: usize,
186    /// How long the tree's text is so far.
187    tree_len: u32,
188    /// Where each piece of the tree's text came from.
189    /// [`Parse::source_span`].
190    placed: Vec<(u32, Span)>,
191}
192
193/// How many parser steps one file may take.
194///
195/// A bound rather than trust: a rule that fails to consume is a defect,
196/// and without this it hangs instead of being reported.
197const MAX_STEPS: usize = 4_000_000;
198
199impl<'a> Parser<'a> {
200    /// A parser over `source`.
201    #[must_use]
202    pub fn new(source: &'a str) -> Self {
203        Self {
204            source,
205            tokens: lex(source),
206            at: 0,
207            builder: TreeBuilder::new(),
208            diagnostics: Vec::new(),
209            dialect: crate::Dialect::ALL_VENDORS,
210            steps: 0,
211            tree_len: 0,
212            placed: Vec::new(),
213        }
214    }
215
216    /// A parser over an already-lexed stream.
217    ///
218    /// The preprocessor produces one: its tokens index into text it
219    /// assembled from every file that contributed, so the parser lexes
220    /// nothing and reads spans into that text instead.
221    #[must_use]
222    pub fn over(source: &'a str, tokens: Vec<Token>) -> Self {
223        Self {
224            source,
225            tokens,
226            at: 0,
227            builder: TreeBuilder::new(),
228            diagnostics: Vec::new(),
229            dialect: crate::Dialect::ALL_VENDORS,
230            steps: 0,
231            tree_len: 0,
232            placed: Vec::new(),
233        }
234    }
235
236    /// Parse the whole file.
237    #[must_use]
238    pub fn parse_file(mut self) -> Parse {
239        self.builder.start(CstKind::File);
240        while !self.at_eof() {
241            let before = self.at;
242            self.parse_definition();
243            if self.at == before {
244                // A rule that consumed nothing would loop; take the
245                // token as an error and move on.
246                self.error_token("expected a declaration");
247            }
248            if self.steps > MAX_STEPS {
249                break;
250            }
251        }
252        self.eat_eof();
253        self.builder.finish();
254
255        let root = self
256            .builder
257            .build()
258            .unwrap_or_else(|| Arc::new(GreenNode::new(CstKind::File, Vec::new())));
259        Parse {
260            root,
261            diagnostics: self.diagnostics,
262            placed: self.placed,
263        }
264    }
265
266    // -- cursor ----------------------------------------------------------
267
268    /// What the current token is.
269    #[must_use]
270    pub fn current(&self) -> TokenKind {
271        self.tokens.get(self.at).map_or(TokenKind::Eof, |t| t.kind)
272    }
273
274    /// What the token `ahead` positions on is.
275    #[must_use]
276    pub fn peek(&self, ahead: usize) -> TokenKind {
277        self.tokens
278            .get(self.at + ahead)
279            .map_or(TokenKind::Eof, |t| t.kind)
280    }
281
282    /// Whether the cursor is at the end.
283    #[must_use]
284    pub fn at_eof(&self) -> bool {
285        self.current() == TokenKind::Eof
286    }
287
288    /// Where the current token is.
289    #[must_use]
290    pub fn current_span(&self) -> Span {
291        self.tokens
292            .get(self.at)
293            .map_or_else(|| Span::empty_at(self.source_end()), |t| t.span)
294    }
295
296    /// The text of the current token.
297    #[must_use]
298    pub fn current_text(&self) -> &str {
299        self.tokens
300            .get(self.at)
301            .and_then(|t| t.span.slice(self.source))
302            .unwrap_or("")
303    }
304
305    #[allow(clippy::cast_possible_truncation)]
306    fn source_end(&self) -> u32 {
307        self.source.len() as u32
308    }
309
310    /// Consume the current token into the tree, trivia first.
311    ///
312    /// Trivia reaches the tree through `Parser::emit_leading_trivia`
313    /// and nowhere else, which is what keeps the round trip exact
314    /// without every rule having to think about it.
315    pub fn bump(&mut self) {
316        self.steps += 1;
317        let Some(token) = self.tokens.get(self.at) else {
318            return;
319        };
320        let text = token.span.slice(self.source).unwrap_or("");
321        let kind = token.kind;
322        let span = token.span;
323        self.emit_leading_trivia();
324        self.emit(kind, text, span);
325        self.at += 1;
326    }
327
328    /// Put one piece of text into the tree, and note where in the source
329    /// it came from ([`Parse::source_span`]).
330    pub(crate) fn emit(&mut self, kind: TokenKind, text: &str, source: Span) {
331        if !text.is_empty() {
332            self.placed.push((self.tree_len, source));
333        }
334        self.builder.token(kind, text);
335        #[allow(clippy::cast_possible_truncation)]
336        let len = text.len() as u32;
337        self.tree_len = self.tree_len.saturating_add(len);
338    }
339
340    /// Put the current token's leading trivia into the tree.
341    ///
342    /// Trivia goes into the tree here and nowhere else. A `//@` comment
343    /// that reaches this point is one no declaration claimed as a suffix
344    /// annotation, and it is reported where it stands: DDS-XTypes 1.3
345    /// §7.3.1.2.5 admits the form only after the declaration it applies
346    /// to, and a `//@key` that is read as a comment leaves the model
347    /// saying "not a key" about a file that says "key".
348    fn emit_leading_trivia(&mut self) {
349        let Some(token) = self.tokens.get(self.at) else {
350            return;
351        };
352        let leading: Vec<(zerodds_idl_cst::TriviaKind, Span)> = token
353            .leading
354            .iter()
355            .map(|trivia| (trivia.kind, trivia.span))
356            .collect();
357        for (kind, span) in leading {
358            let text = span.slice(self.source).unwrap_or("");
359            if kind == zerodds_idl_cst::TriviaKind::LineComment
360                && crate::suffix::is_suffix_comment(text)
361            {
362                self.diagnose_stray_suffix(span);
363            }
364            self.emit(trivia_token(kind), text, span);
365        }
366    }
367
368    /// Consume the end-of-file token, so its trailing trivia lands in
369    /// the tree.
370    pub(crate) fn eat_eof(&mut self) {
371        self.emit_leading_trivia();
372    }
373
374    /// The rest of a string literal whose first piece `kind` was just
375    /// consumed: every adjacent literal of the same width.
376    ///
377    /// §7.2.6.3: "Adjacent string literals are concatenated", and
378    /// §7.2.6.4 says the same of wide ones. A lexical rule, so it holds
379    /// wherever a string literal stands — a constant, an annotation
380    /// argument, a `typeid`, a `typeprefix`, an `import`, a `context`
381    /// clause. The pieces stay separate tokens, so the round trip keeps
382    /// them as written; the lowering joins their values, each decoded on
383    /// its own, which keeps the characters of the pieces "distinct".
384    /// A literal of the other width next to it is diagnosed: neither
385    /// clause concatenates a narrow and a wide literal.
386    pub(crate) fn eat_adjacent_string_literals(&mut self, kind: TokenKind) {
387        loop {
388            let next = self.current();
389            if next == kind {
390                self.bump();
391            } else if matches!(next, TokenKind::StringLiteral | TokenKind::WStringLiteral) {
392                self.diagnose(
393                    crate::diagnostic::DiagnosticCode::UnexpectedToken,
394                    "a string literal and a wide string literal are not concatenated; \
395                     adjacent literals have to be of one width",
396                    "OMG IDL 4.2 §7.2.6.3, §7.2.6.4",
397                );
398                self.bump();
399            } else {
400                return;
401            }
402        }
403    }
404
405    /// Consume the current token if it is `kind`.
406    pub fn eat(&mut self, kind: TokenKind) -> bool {
407        if self.current() == kind {
408            self.bump();
409            true
410        } else {
411            false
412        }
413    }
414
415    /// Consume `kind`, or diagnose that it was missing.
416    ///
417    /// Does not consume anything on failure: the caller's recovery
418    /// decides what to skip.
419    pub fn expect(&mut self, kind: TokenKind, what: &str) -> bool {
420        if self.eat(kind) {
421            return true;
422        }
423        let span = Span::empty_at(self.current_span().start);
424        self.diagnostics.push(Diagnostic::error(
425            DiagnosticCode::MissingToken,
426            span,
427            format!("expected {what}"),
428            "OMG IDL 4.2 §7.4",
429        ));
430        false
431    }
432
433    // -- tree ------------------------------------------------------------
434
435    /// Begin a node.
436    pub fn start(&mut self, kind: CstKind) {
437        self.builder.start(kind);
438    }
439
440    /// End the node in progress.
441    pub fn finish(&mut self) {
442        self.builder.finish();
443    }
444
445    /// Mark the current position, for wrapping already-emitted children.
446    #[must_use]
447    pub fn checkpoint(&self) -> Checkpoint {
448        self.builder.checkpoint()
449    }
450
451    /// Wrap everything since `checkpoint` in a node.
452    pub fn start_at(&mut self, checkpoint: Checkpoint, kind: CstKind) {
453        self.builder.start_at(checkpoint, kind);
454    }
455
456    /// How many tokens are left.
457    #[must_use]
458    pub fn tokens_left(&self) -> usize {
459        self.tokens.len().saturating_sub(self.at)
460    }
461
462    /// Consume the first half of a `>>` token, leaving the second.
463    ///
464    /// `sequence<sequence<long>>` ends in one token that closes two
465    /// argument lists. The tree records two `>` covering one source
466    /// token, so `text()` still reproduces the input. Spec §2.3.
467    pub fn split_shift_right(&mut self) {
468        self.steps += 1;
469        let Some(token) = self.tokens.get(self.at).cloned() else {
470            return;
471        };
472        if token.kind != TokenKind::GreaterGreater {
473            return;
474        }
475        // The leading trivia belongs to the first half only.
476        self.emit_leading_trivia();
477        self.emit(
478            TokenKind::Greater,
479            ">",
480            Span::new(token.span.start, token.span.start.saturating_add(1)),
481        );
482
483        // Replace the token with a bare `>` covering the second byte, so
484        // the outer list closes on real input rather than on nothing.
485        let rest = Token {
486            kind: TokenKind::Greater,
487            span: Span::new(token.span.start.saturating_add(1), token.span.end),
488            leading: Vec::new(),
489            value: token.value,
490        };
491        if let Some(slot) = self.tokens.get_mut(self.at) {
492            *slot = rest;
493        }
494    }
495
496    // -- diagnostics and recovery ---------------------------------------
497
498    /// Record a diagnostic without consuming anything.
499    pub fn diagnose(&mut self, code: DiagnosticCode, message: &str, spec_reference: &'static str) {
500        self.diagnostics.push(Diagnostic::error(
501            code,
502            self.current_span(),
503            message.to_string(),
504            spec_reference,
505        ));
506    }
507
508    /// Record that a construct outside OMG IDL 4.2 was written here.
509    ///
510    /// **Always recorded, whether or not the dialect admits it.** The
511    /// severity is what the dialect decides: an error when the flag is
512    /// off, a warning when it is on.
513    ///
514    /// Admitting a construct and saying nothing about it are two
515    /// different things, and only the first is what `Dialect` is for.
516    /// The gate here was written the other way round — silent when
517    /// admitted — and `parse` passes `Dialect::ALL_VENDORS` while every
518    /// caller in this tree calls `parse`, so the gate never fired for
519    /// anybody and `keylist` reached every one of them with nothing said
520    /// about it. A file that is not IDL 4.2 is an interoperability fact
521    /// of the same kind as a byte difference, and a caller cannot act on
522    /// a fact it is never given.
523    ///
524    /// Spec §3.5.
525    pub(crate) fn diagnose_vendor(
526        &mut self,
527        flag: crate::dialect::VendorFlag,
528        message: &str,
529        spec_reference: &'static str,
530    ) {
531        let span = self.current_span();
532        let diagnostic = if self.dialect.admits(flag) {
533            Diagnostic::warning(
534                DiagnosticCode::VendorExtension,
535                span,
536                message.to_string(),
537                spec_reference,
538            )
539        } else {
540            Diagnostic::error(
541                DiagnosticCode::VendorExtension,
542                span,
543                message.to_string(),
544                spec_reference,
545            )
546        };
547        self.diagnostics.push(diagnostic);
548    }
549
550    /// Wrap the current token in an error node and move past it.
551    pub fn error_token(&mut self, message: &str) {
552        self.diagnose(DiagnosticCode::UnexpectedToken, message, "OMG IDL 4.2 §7.4");
553        self.start(CstKind::Error);
554        self.bump();
555        self.finish();
556    }
557
558    /// Skip to the next place a declaration can begin.
559    ///
560    /// Recovery points are `;`, `}` and the top-level keywords. The
561    /// skipped range becomes an error node so the round trip holds.
562    /// Spec §3.2.
563    pub fn recover_to_declaration(&mut self) {
564        if self.at_eof() || self.at_recovery_point() {
565            return;
566        }
567        self.start(CstKind::Error);
568        let mut depth = 0i32;
569        while !self.at_eof() {
570            match self.current() {
571                TokenKind::LBrace => depth += 1,
572                TokenKind::RBrace => {
573                    if depth == 0 {
574                        break;
575                    }
576                    depth -= 1;
577                }
578                TokenKind::Semicolon if depth == 0 => {
579                    self.bump();
580                    break;
581                }
582                _ if depth == 0 && self.at_recovery_point() => break,
583                _ => {}
584            }
585            self.bump();
586            if self.steps > MAX_STEPS {
587                break;
588            }
589        }
590        self.finish();
591    }
592
593    /// Whether the current token can begin a declaration.
594    #[must_use]
595    pub fn at_recovery_point(&self) -> bool {
596        matches!(
597            self.current(),
598            TokenKind::Module
599                | TokenKind::Const
600                | TokenKind::Typedef
601                | TokenKind::Struct
602                | TokenKind::Union
603                | TokenKind::Enum
604                | TokenKind::Bitmask
605                | TokenKind::Bitset
606                | TokenKind::Native
607                | TokenKind::Exception
608                | TokenKind::Interface
609                | TokenKind::Valuetype
610                | TokenKind::Eventtype
611                | TokenKind::Component
612                | TokenKind::Home
613                | TokenKind::Connector
614                | TokenKind::Porttype
615                | TokenKind::Local
616                | TokenKind::Abstract
617                | TokenKind::Custom
618                | TokenKind::Typeid
619                | TokenKind::Typeprefix
620                | TokenKind::Import
621                | TokenKind::At
622        )
623    }
624
625    /// The diagnostics gathered so far.
626    #[must_use]
627    pub fn diagnostics(&self) -> &[Diagnostic] {
628        &self.diagnostics
629    }
630}
631
632/// The token kind one piece of trivia takes in the tree.
633///
634/// `Newline` and `Whitespace` are one kind here: the tree keeps the text
635/// itself, so nothing about the round trip needs them apart.
636pub(crate) const fn trivia_token(kind: zerodds_idl_cst::TriviaKind) -> TokenKind {
637    match kind {
638        zerodds_idl_cst::TriviaKind::Whitespace | zerodds_idl_cst::TriviaKind::Newline => {
639            TokenKind::Whitespace
640        }
641        zerodds_idl_cst::TriviaKind::LineComment => TokenKind::LineComment,
642        zerodds_idl_cst::TriviaKind::BlockComment => TokenKind::BlockComment,
643        zerodds_idl_cst::TriviaKind::Directive => TokenKind::Directive,
644    }
645}