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}