Skip to main content

zerodds_idl_parse/preprocess/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! The preprocessor. Spec §3.4.
4//!
5//! Directives reach here as trivia — the lexer recognises a `#` line and
6//! keeps it, without interpreting it. This module reads them, resolves
7//! includes, expands macros, and produces a token stream in which every
8//! token knows where it textually came from **and** which macros
9//! produced it.
10//!
11//! An include cycle is diagnosed, not followed.
12//!
13//! IDL 4.2 §7.3: "IDL shall be preprocessed according to the
14//! specification of the preprocessor in ISO/IEC 14882:2003." That is
15//! object-like and function-like macros (§16.3) with `#` and `##`
16//! (§16.3.2, §16.3.3), the rescan and its hide sets (§16.3.4), `#if`
17//! over a full integral constant expression (§16.1), `#line` (§16.4),
18//! `#error` (§16.5), `__LINE__` and `__FILE__` (§16.8), and lines
19//! continued with a backslash (§2.1). `__DATE__` and `__TIME__` are left
20//! to the caller's defines: a front end that stamps the clock into what
21//! it reads gives a different unit on every run.
22
23mod condition;
24pub mod macro_;
25mod pptoken;
26
27use crate::diagnostic::{Diagnostic, DiagnosticCode};
28use alloc::borrow::ToOwned;
29use alloc::collections::VecDeque;
30use alloc::format;
31use alloc::string::{String, ToString};
32use alloc::vec::Vec;
33use macro_::{MacroTable, MacroUse};
34use zerodds_idl_cst::{Span, Token, Trivia, TriviaKind, lex};
35
36/// Which file a piece of text came from.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
38pub struct FileId(pub u32);
39
40/// A file the preprocessor read.
41#[derive(Debug, Clone)]
42pub struct SourceFile {
43    /// How it was named.
44    pub path: String,
45    /// What it contained.
46    pub text: String,
47}
48
49/// Where one expanded token came from.
50///
51/// Both halves matter: the range says where a reader should look, and
52/// the chain says which macro put it there. A diagnostic inside an
53/// expansion needs to name both. Spec §3.4.
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct Origin {
56    /// The file the text is in.
57    pub file: FileId,
58    /// Where in that file.
59    pub span: Span,
60    /// The macro expansions that produced it, outermost first. Empty for
61    /// a token written literally.
62    pub macro_chain: Vec<MacroUse>,
63}
64
65impl Origin {
66    /// A token written literally in a file.
67    #[must_use]
68    pub const fn literal(file: FileId, span: Span) -> Self {
69        Self {
70            file,
71            span,
72            macro_chain: Vec::new(),
73        }
74    }
75
76    /// Whether this token came out of a macro rather than being written.
77    #[must_use]
78    pub fn is_expanded(&self) -> bool {
79        !self.macro_chain.is_empty()
80    }
81}
82
83/// Where every token of the expanded stream came from.
84#[derive(Debug, Clone, Default)]
85pub struct SourceMap {
86    files: Vec<SourceFile>,
87    origins: Vec<Origin>,
88    /// Every stretch of the expanded text with the place it was taken
89    /// from — each token, and each pragma or `//@` comment carried as
90    /// trivia — in the order the text was written, which is ascending.
91    ranges: Vec<(Span, Origin)>,
92}
93
94impl SourceMap {
95    /// An empty map.
96    #[must_use]
97    pub const fn new() -> Self {
98        Self {
99            files: Vec::new(),
100            origins: Vec::new(),
101            ranges: Vec::new(),
102        }
103    }
104
105    /// The file a unit is: the first one read.
106    pub const ROOT: FileId = FileId(0);
107
108    /// Record a file, returning its id.
109    pub fn add_file(&mut self, path: &str, text: &str) -> FileId {
110        let id = u32::try_from(self.files.len()).unwrap_or(u32::MAX);
111        self.files.push(SourceFile {
112            path: path.to_owned(),
113            text: text.to_owned(),
114        });
115        FileId(id)
116    }
117
118    /// A file's recorded path.
119    #[must_use]
120    pub fn path(&self, file: FileId) -> Option<&str> {
121        self.files.get(file.0 as usize).map(|f| f.path.as_str())
122    }
123
124    /// A file's text.
125    #[must_use]
126    pub fn text(&self, file: FileId) -> Option<&str> {
127        self.files.get(file.0 as usize).map(|f| f.text.as_str())
128    }
129
130    /// Where the token at `index` in the expanded stream came from.
131    #[must_use]
132    pub fn origin(&self, index: usize) -> Option<&Origin> {
133        self.origins.get(index)
134    }
135
136    /// How many tokens the map covers.
137    #[must_use]
138    pub fn len(&self) -> usize {
139        self.origins.len()
140    }
141
142    /// Whether the map covers nothing.
143    #[must_use]
144    pub fn is_empty(&self) -> bool {
145        self.origins.is_empty()
146    }
147
148    fn push(&mut self, origin: Origin) {
149        self.origins.push(origin);
150    }
151
152    fn push_range(&mut self, expanded: Span, origin: Origin) {
153        self.ranges.push((expanded, origin));
154    }
155
156    /// Where a range of the expanded text was written: the file and the
157    /// range in that file's own text.
158    ///
159    /// A range that starts inside a token written as it stands maps
160    /// octet for octet; one that starts inside a macro's expansion maps
161    /// to the invocation, which is the text a reader can find. A range
162    /// over several tokens of one file runs from the first to the last;
163    /// one that leaves its file — a declaration whose body an include
164    /// supplied — keeps the file it starts in. `None` before the first
165    /// token, which a diagnostic of an empty unit points at.
166    #[must_use]
167    pub fn original(&self, expanded: Span) -> Option<(FileId, Span)> {
168        let containing = |at: u32| {
169            self.ranges
170                .partition_point(|(range, _)| range.start <= at)
171                .checked_sub(1)
172        };
173        let first = containing(expanded.start)?;
174        let last = containing(expanded.end.saturating_sub(1).max(expanded.start))
175            .unwrap_or(first)
176            .max(first);
177        let (from, start) = &self.ranges[first];
178        let (to, end) = &self.ranges[last];
179        let start_at = Self::offset_in(*from, start, expanded.start, false);
180        if start.file != end.file {
181            return Some((start.file, Span::new(start_at, start.span.end)));
182        }
183        let end_at = Self::offset_in(*to, end, expanded.end, true).max(start_at);
184        Some((start.file, Span::new(start_at, end_at)))
185    }
186
187    /// The origin of the range of the expanded text that holds `at`:
188    /// the file, where in it, and the macro expansions that made the
189    /// text, outermost first. `None` before the first token.
190    #[must_use]
191    pub fn origin_at(&self, at: u32) -> Option<&Origin> {
192        let index = self
193            .ranges
194            .partition_point(|(range, _)| range.start <= at)
195            .checked_sub(1)?;
196        self.ranges.get(index).map(|(_, origin)| origin)
197    }
198
199    /// `at`, an offset of the expanded text inside `range`, as an offset
200    /// of the file `origin` names: shifted by the same distance where the
201    /// text is the file's own, the invocation's edge where a macro made
202    /// it.
203    fn offset_in(range: Span, origin: &Origin, at: u32, end: bool) -> u32 {
204        let written =
205            !origin.is_expanded() && range.end - range.start == origin.span.end - origin.span.start;
206        if written {
207            let within = at.clamp(range.start, range.end) - range.start;
208            origin.span.start + within
209        } else if end {
210            origin.span.end
211        } else {
212            origin.span.start
213        }
214    }
215}
216
217/// Where a node of the model was written ([`Preprocessed::provenance`]).
218#[derive(Debug, Clone, PartialEq, Eq)]
219pub struct Provenance {
220    /// The file's path, as the preprocessor recorded it.
221    pub path: String,
222    /// The range in that file's text.
223    pub span: Span,
224    /// The macro expansions that produced the text, outermost first.
225    pub macro_chain: Vec<ProvenanceMacro>,
226}
227
228/// One macro expansion of a [`Provenance`].
229#[derive(Debug, Clone, PartialEq, Eq)]
230pub struct ProvenanceMacro {
231    /// The macro.
232    pub name: String,
233    /// The file its invocation is written in.
234    pub path: Option<String>,
235    /// Where in that file.
236    pub at: Span,
237}
238
239/// How the preprocessor finds an included file.
240///
241/// A trait rather than direct file access, so the whole preprocessor is
242/// testable without a filesystem and so a build system can supply its
243/// own include path.
244pub trait FileLoader {
245    /// Resolve and read `path`, as requested from `from`.
246    ///
247    /// Returns the resolved name and the contents. `None` if it cannot
248    /// be found — the caller diagnoses that rather than the loader.
249    fn load(&self, path: &str, from: Option<&str>) -> Option<(String, String)>;
250}
251
252/// A loader over an in-memory set of files.
253///
254/// What the tests use, and enough for a build system that already has
255/// its sources in hand.
256#[derive(Debug, Clone, Default)]
257pub struct MemoryLoader {
258    files: Vec<(String, String)>,
259}
260
261impl MemoryLoader {
262    /// An empty loader.
263    #[must_use]
264    pub const fn new() -> Self {
265        Self { files: Vec::new() }
266    }
267
268    /// Make `path` resolvable to `text`.
269    #[must_use]
270    pub fn with(mut self, path: &str, text: &str) -> Self {
271        self.files.push((path.to_owned(), text.to_owned()));
272        self
273    }
274}
275
276impl FileLoader for MemoryLoader {
277    fn load(&self, path: &str, _from: Option<&str>) -> Option<(String, String)> {
278        self.files
279            .iter()
280            .find(|(name, _)| name == path)
281            .map(|(name, text)| (name.clone(), text.clone()))
282    }
283}
284
285/// The expanded token stream and where it came from.
286#[derive(Debug, Clone)]
287pub struct Preprocessed {
288    /// The tokens the parser will see.
289    pub tokens: Vec<Token>,
290    /// The text those tokens index into.
291    ///
292    /// Concatenated from every file that contributed, so a token's span
293    /// still slices something. The map says which file a range belongs
294    /// to.
295    pub text: String,
296    /// Where each token came from.
297    pub map: SourceMap,
298    /// Everything wrong with the directives.
299    pub diagnostics: Vec<Diagnostic>,
300}
301
302impl Preprocessed {
303    /// Where a range of the expanded text was written, for a node of the
304    /// model rather than a diagnostic: the file's path, the range in its
305    /// text ([`SourceMap::original`]), and the macro expansions that
306    /// produced the text at its start, outermost first, each with the
307    /// path and range of its invocation. `None` before the first token.
308    #[must_use]
309    pub fn provenance(&self, expanded: Span) -> Option<Provenance> {
310        let (file, span) = self.map.original(expanded)?;
311        let path = self.map.path(file)?.to_string();
312        let macro_chain = self
313            .map
314            .origin_at(expanded.start)
315            .map(|origin| {
316                origin
317                    .macro_chain
318                    .iter()
319                    .map(|used| ProvenanceMacro {
320                        name: used.name.clone(),
321                        path: self.map.path(FileId(used.file)).map(ToString::to_string),
322                        at: used.at,
323                    })
324                    .collect()
325            })
326            .unwrap_or_default();
327        Some(Provenance {
328            path,
329            span,
330            macro_chain,
331        })
332    }
333
334    /// Moves `diagnostic` out of the expanded text into the file it
335    /// points at.
336    ///
337    /// The parser and everything after it see only the expanded text,
338    /// in which comments are gone, directives are gone, includes are
339    /// inlined and every token stands one space from the next — so an
340    /// offset into it is an offset into no file a reader has. Read
341    /// against the unit, the first lines of comment were enough to put
342    /// `file.idl:3:7` where `file.idl:6:10` was meant. This maps
343    /// `primary` and every label through [`SourceMap::original`], and
344    /// sets [`Diagnostic::file`] when the place is an included file. A
345    /// label in a file other than the primary's names that file in its
346    /// note.
347    ///
348    /// For diagnostics raised on the expanded text only: the
349    /// preprocessor's own are in file offsets already.
350    pub fn locate(&self, diagnostic: &mut Diagnostic) {
351        let path = |file: FileId| {
352            (file != SourceMap::ROOT)
353                .then(|| self.map.path(file).map(ToString::to_string))
354                .flatten()
355        };
356        let primary_file = match self.map.original(diagnostic.primary) {
357            Some((file, span)) => {
358                diagnostic.primary = span;
359                diagnostic.file = path(file);
360                Some(file)
361            }
362            None => None,
363        };
364        for (span, note) in &mut diagnostic.labels {
365            if let Some((file, mapped)) = self.map.original(*span) {
366                *span = mapped;
367                if Some(file) != primary_file {
368                    let name = self.map.path(file).unwrap_or("?");
369                    *note = format!("{name}: {note}");
370                }
371            }
372        }
373    }
374}
375
376/// How deep includes may nest.
377///
378/// A bound rather than trust: a cycle is diagnosed, but a chain that is
379/// merely very long should stop too.
380const MAX_INCLUDE_DEPTH: usize = 64;
381
382/// How deep argument pre-expansion may nest: `F(F(F(…)))` expands the
383/// innermost argument first, one level per parenthesis.
384const MAX_EXPANSION_DEPTH: usize = 256;
385
386/// How many tokens the macro expansions of one unit may produce in all.
387///
388/// The hide sets of §16.3.4 make every expansion terminate, but not
389/// every expansion small: a few lines of mutually doubling macros reach
390/// billions of tokens. A bound, reported by name, rather than a host out
391/// of memory.
392const MAX_EXPANDED_TOKENS: usize = 1 << 22;
393
394/// Run the preprocessor over `source`, named `path`.
395#[must_use]
396pub fn preprocess(path: &str, source: &str, loader: &dyn FileLoader) -> Preprocessed {
397    preprocess_with(path, source, loader, &[])
398}
399
400/// As [`preprocess`], with macros the caller defined.
401///
402/// `defines` is what a build system passes as `-D`: each entry is a name
403/// and its body, and an empty body is `-DNAME` — defined, expanding to
404/// nothing, which is what `#ifdef` asks about. They are in place before
405/// the first line is read, so a `#ifndef` guard at the top of the unit
406/// sees them. A name written `NAME(a, b)` defines a function-like macro.
407#[must_use]
408pub fn preprocess_with(
409    path: &str,
410    source: &str,
411    loader: &dyn FileLoader,
412    defines: &[(String, String)],
413) -> Preprocessed {
414    let mut state = Preprocessor {
415        loader,
416        map: SourceMap::new(),
417        macros: MacroTable::new(),
418        diagnostics: Vec::new(),
419        text: String::new(),
420        tokens: Vec::new(),
421        pending_pragmas: Vec::new(),
422        open: Vec::new(),
423        once: Vec::new(),
424        presumed: Vec::new(),
425        expansion_depth: 0,
426        produced: 0,
427    };
428    for (name, body) in defines {
429        let line = format!("{name} {body}");
430        state.define(&line, Span::default());
431    }
432    state.include(path, source, 0);
433    // A pragma at the end of the unit has no following token to attach
434    // to, and `#pragma keylist Sample id` written under the struct it
435    // keys is the common spelling — Cyclone's own examples put it there.
436    // It goes onto the last token instead, which is the same declaration.
437    state.flush_trailing_pragmas();
438    Preprocessed {
439        tokens: state.tokens,
440        text: state.text,
441        map: state.map,
442        diagnostics: state.diagnostics,
443    }
444}
445
446struct Preprocessor<'a> {
447    loader: &'a dyn FileLoader,
448    map: SourceMap,
449    macros: MacroTable,
450    diagnostics: Vec<Diagnostic>,
451    text: String,
452    tokens: Vec<Token>,
453    /// `#pragma` lines waiting for the next token to carry them.
454    ///
455    /// The preprocessor consumes every directive and emits its tokens
456    /// with no leading trivia, so a pragma written in the source did not
457    /// survive into the stream the parser sees — and the lowering looks
458    /// for exactly that trivia. `#pragma keylist Sample id` therefore
459    /// reached the model only when nothing preprocessed the file, which
460    /// meant a `build.rs` produced a type with no key at all.
461    ///
462    /// A pragma carries declaration metadata about what *follows* it, so
463    /// it is attached to the next token rather than the previous one.
464    pending_pragmas: Vec<(String, Trivia, FileId)>,
465    /// The include chain, so a cycle is recognised rather than followed.
466    open: Vec<String>,
467    /// The files that asked to be read once, by resolved path.
468    ///
469    /// `#pragma once` reached the pragma arm below and was held as
470    /// trivia like every other pragma, which is to say it was carried
471    /// to the model and read by nobody. A header that named it was
472    /// expanded once per `#include` all the same, and the second
473    /// expansion redeclared every name in it — the same
474    /// `DuplicateName` a header with no guard at all produces.
475    once: Vec<String>,
476    /// What `#line` (§16.4) said about a file: the difference between
477    /// the line `__LINE__` reports and the physical one, and the name
478    /// `__FILE__` reports instead of the path.
479    presumed: Vec<(FileId, i64, Option<String>)>,
480    /// How deep argument pre-expansion is nested now.
481    expansion_depth: usize,
482    /// How many tokens expansions have produced in this unit.
483    produced: usize,
484}
485
486/// A preprocessing token on its way through expansion.
487#[derive(Debug, Clone)]
488struct PpToken {
489    /// Its spelling.
490    text: String,
491    /// White space stood before it; `#` (§16.3.2) needs to know.
492    space_before: bool,
493    /// Where it came from.
494    origin: Origin,
495    /// The token the IDL lexer read, where this one was written in a
496    /// file and is emitted as written. `None` for a token an expansion
497    /// made, which the IDL lexer reads when it is emitted.
498    written: Option<Token>,
499    /// The macros whose expansion produced it, §16.3.4: none of them is
500    /// expanded again inside it.
501    hide: Vec<String>,
502    /// A macro name §16.3.4/2 has taken out of further replacement for
503    /// good.
504    painted: bool,
505}
506
507impl PpToken {
508    fn made(text: String, space_before: bool, origin: Origin) -> Self {
509        Self {
510            text,
511            space_before,
512            origin,
513            written: None,
514            hide: Vec::new(),
515            painted: false,
516        }
517    }
518
519    fn is_identifier(&self) -> bool {
520        pptoken::is_identifier(&self.text)
521    }
522}
523
524/// One file being read: its tokens, how far, and the conditionals open
525/// in it.
526struct FileCursor<'s> {
527    tokens: Vec<Token>,
528    at: usize,
529    source: &'s str,
530    file: FileId,
531    conditions: Vec<Condition>,
532    depth: usize,
533}
534
535impl FileCursor<'_> {
536    fn keeping(&self) -> bool {
537        self.conditions.iter().all(Condition::keeping)
538    }
539}
540
541/// Where an expansion's tokens go: straight into the stream, or into a
542/// list, for an argument expanded before substitution (§16.3.1) and for
543/// an `#if` line.
544enum Sink<'v> {
545    Emit,
546    Collect(&'v mut Vec<PpToken>),
547}
548
549impl Preprocessor<'_> {
550    fn include(&mut self, path: &str, source: &str, depth: usize) {
551        if self.once.iter().any(|seen| seen == path) {
552            return;
553        }
554        if depth > MAX_INCLUDE_DEPTH {
555            self.report(Diagnostic::error(
556                DiagnosticCode::UnterminatedDeclaration,
557                Span::default(),
558                format!("include nesting past {MAX_INCLUDE_DEPTH} levels at `{path}`"),
559                "OMG IDL 4.2 §7.3",
560            ));
561            return;
562        }
563        if self.open.iter().any(|open| open == path) {
564            self.report(Diagnostic::error(
565                DiagnosticCode::UnexpectedToken,
566                Span::default(),
567                format!(
568                    "`{path}` includes itself, directly or through {}",
569                    self.open.join(" -> ")
570                ),
571                "OMG IDL 4.2 §7.3",
572            ));
573            return;
574        }
575
576        let file = self.map.add_file(path, source);
577        self.open.push(path.to_owned());
578        let mut cursor = FileCursor {
579            tokens: lex(source),
580            at: 0,
581            source,
582            file,
583            conditions: Vec::new(),
584            depth,
585        };
586        let mut queue = VecDeque::new();
587        self.run(&mut queue, Some(&mut cursor), &mut Sink::Emit);
588
589        if !cursor.conditions.is_empty() {
590            self.report(Diagnostic::error(
591                DiagnosticCode::UnterminatedDeclaration,
592                Span::default(),
593                format!(
594                    "{} conditional(s) left open in `{path}`",
595                    cursor.conditions.len()
596                ),
597                "OMG IDL 4.2 §7.3",
598            ));
599        }
600        let _ = self.open.pop();
601    }
602
603    /// The next token: the front of the replacement queue, or else the
604    /// next token of the file that its conditionals keep, after the
605    /// directives written before it have been carried out.
606    fn pull(
607        &mut self,
608        queue: &mut VecDeque<PpToken>,
609        cursor: Option<&mut FileCursor<'_>>,
610    ) -> Option<PpToken> {
611        if let Some(token) = queue.pop_front() {
612            return Some(token);
613        }
614        let cursor = cursor?;
615        while cursor.at < cursor.tokens.len() {
616            let index = cursor.at;
617            cursor.at += 1;
618            let leading = cursor.tokens[index].leading.clone();
619            for trivia in &leading {
620                match trivia.kind {
621                    TriviaKind::Directive => {
622                        let line = trivia.span.slice(cursor.source).unwrap_or("");
623                        self.directive(line, trivia.span, cursor);
624                    }
625                    // A `//@` comment is an annotation — DDS-XTypes 1.3
626                    // §7.3.1.2.5, Table 24 — and the preprocessor emits
627                    // no comments, so it would reach the parser as
628                    // nothing at all. `//@key` written in a file read
629                    // through `preprocess` left the member with no key
630                    // and said nothing, which is the same defect the
631                    // pragma below was fixed for. It is held and
632                    // attached to the next token, like a pragma.
633                    TriviaKind::LineComment
634                        if crate::suffix::is_suffix_comment(
635                            trivia.span.slice(cursor.source).unwrap_or(""),
636                        ) && cursor.keeping() =>
637                    {
638                        self.pending_pragmas.push((
639                            trivia.span.slice(cursor.source).unwrap_or("").to_string(),
640                            Trivia {
641                                kind: TriviaKind::LineComment,
642                                span: trivia.span,
643                            },
644                            cursor.file,
645                        ));
646                    }
647                    _ => {}
648                }
649            }
650            let token = &cursor.tokens[index];
651            if token.kind == zerodds_idl_cst::TokenKind::Eof || !cursor.keeping() {
652                continue;
653            }
654            return Some(PpToken {
655                text: token.span.slice(cursor.source).unwrap_or("").to_string(),
656                space_before: !token.leading.is_empty(),
657                origin: Origin::literal(cursor.file, token.span),
658                written: Some(token.clone()),
659                hide: Vec::new(),
660                painted: false,
661            });
662        }
663        None
664    }
665
666    /// Expand everything `queue` and `cursor` hold, ISO/IEC 14882:2003
667    /// §16.3, and deliver each token that is not replaced to `sink`.
668    ///
669    /// A replacement goes back to the front of the queue, so it is
670    /// rescanned together with the rest of the source (§16.3.4/1) —
671    /// which is how `#define F G` and a function-like `G` written `F(1)`
672    /// expand at all. Every token carries the macros that produced it,
673    /// and none of those is replaced inside it again: the hide sets that
674    /// make the rescan terminate.
675    fn run(
676        &mut self,
677        queue: &mut VecDeque<PpToken>,
678        mut cursor: Option<&mut FileCursor<'_>>,
679        sink: &mut Sink<'_>,
680    ) {
681        while let Some(mut token) = self.pull(queue, cursor.as_deref_mut()) {
682            if token.painted || !token.is_identifier() {
683                self.deliver(token, sink);
684                continue;
685            }
686            if token.hide.contains(&token.text) {
687                // §16.3.4/2: met again inside its own expansion, the name
688                // is not replaced — now or on any later rescan.
689                token.painted = true;
690                self.deliver(token, sink);
691                continue;
692            }
693            if let Some(answer) = self.predefined(&token) {
694                self.deliver(answer, sink);
695                continue;
696            }
697            let Some(found) = self.macros.get(&token.text).cloned() else {
698                self.deliver(token, sink);
699                continue;
700            };
701            let replacement = match &found.params {
702                None => {
703                    let mut hide = token.hide.clone();
704                    hide.push(token.text.clone());
705                    self.substitute(&found, &[], &token, &hide)
706                }
707                Some(params) => {
708                    // A function-like name without `(` after it is not an
709                    // invocation (§16.3/10) and stays as written.
710                    let Some(next) = self.pull(queue, cursor.as_deref_mut()) else {
711                        self.deliver(token, sink);
712                        continue;
713                    };
714                    if next.text != "(" {
715                        queue.push_front(next);
716                        self.deliver(token, sink);
717                        continue;
718                    }
719                    let Some((mut args, closing)) =
720                        self.arguments(queue, cursor.as_deref_mut(), &token)
721                    else {
722                        continue;
723                    };
724                    // `F()` is one empty argument, and that is no
725                    // argument for a macro of no parameter.
726                    if params.is_empty() && args.len() == 1 && args[0].is_empty() {
727                        args.clear();
728                    }
729                    if args.len() != params.len() {
730                        self.preprocessing_error(
731                            token.origin.span,
732                            format!(
733                                "`{}` takes {} argument(s) and is given {}",
734                                token.text,
735                                params.len(),
736                                args.len()
737                            ),
738                            "ISO/IEC 14882:2003 §16.3/4",
739                        );
740                        continue;
741                    }
742                    // §16.3.4 as Prosser's algorithm states it: the
743                    // names hidden in both the macro name and the `)`
744                    // that closes its invocation, and the macro.
745                    let mut hide: Vec<String> = token
746                        .hide
747                        .iter()
748                        .filter(|name| closing.hide.contains(name))
749                        .cloned()
750                        .collect();
751                    hide.push(token.text.clone());
752                    self.substitute(&found, &args, &token, &hide)
753                }
754            };
755            self.produced = self.produced.saturating_add(replacement.len());
756            if self.produced > MAX_EXPANDED_TOKENS {
757                self.preprocessing_error(
758                    token.origin.span,
759                    format!(
760                        "macro expansion produced more than {MAX_EXPANDED_TOKENS} tokens; `{}` is \
761                         not expanded further",
762                        token.text
763                    ),
764                    "zerodds-idl-frontend-1.0 §3.4",
765                );
766                return;
767            }
768            for replaced in replacement.into_iter().rev() {
769                queue.push_front(replaced);
770            }
771        }
772    }
773
774    /// The arguments of an invocation whose `(` was just read, split at
775    /// the commas no parenthesis encloses (§16.3/10), and the `)` that
776    /// ends them. `None`, reported, where the source ends first.
777    fn arguments(
778        &mut self,
779        queue: &mut VecDeque<PpToken>,
780        mut cursor: Option<&mut FileCursor<'_>>,
781        name: &PpToken,
782    ) -> Option<(Vec<Vec<PpToken>>, PpToken)> {
783        let mut args: Vec<Vec<PpToken>> = alloc::vec![Vec::new()];
784        let mut nesting = 0_usize;
785        while let Some(token) = self.pull(queue, cursor.as_deref_mut()) {
786            match token.text.as_str() {
787                ")" if nesting == 0 => return Some((args, token)),
788                "," if nesting == 0 => args.push(Vec::new()),
789                "(" | ")" => {
790                    if token.text == "(" {
791                        nesting += 1;
792                    } else {
793                        nesting -= 1;
794                    }
795                    if let Some(last) = args.last_mut() {
796                        last.push(token);
797                    }
798                }
799                _ => {
800                    if let Some(last) = args.last_mut() {
801                        last.push(token);
802                    }
803                }
804            }
805        }
806        self.preprocessing_error(
807            name.origin.span,
808            format!(
809                "the invocation of `{}` is not closed before the end of the input",
810                name.text
811            ),
812            "ISO/IEC 14882:2003 §16.3/10",
813        );
814        None
815    }
816
817    /// The replacement list of `found` for the invocation `invocation`,
818    /// with its parameters substituted (§16.3.1), `#` applied (§16.3.2)
819    /// and `##` applied (§16.3.3), every token hiding `hide`.
820    fn substitute(
821        &mut self,
822        found: &macro_::Macro,
823        args: &[Vec<PpToken>],
824        invocation: &PpToken,
825        hide: &[String],
826    ) -> Vec<PpToken> {
827        enum Item {
828            Token(PpToken),
829            /// An argument with no tokens, which still takes part in a
830            /// paste. §16.3.3/2's placemarker.
831            Placemarker,
832            Paste,
833        }
834        let mut chain = invocation.origin.macro_chain.clone();
835        chain.push(MacroUse {
836            name: invocation.text.clone(),
837            at: invocation.origin.span,
838            file: invocation.origin.file.0,
839        });
840        let origin = Origin {
841            file: invocation.origin.file,
842            span: invocation.origin.span,
843            macro_chain: chain.clone(),
844        };
845        let params: &[String] = found.params.as_deref().unwrap_or(&[]);
846        let function_like = found.params.is_some();
847        let body = &found.body;
848        let mut expanded: Vec<Option<Vec<PpToken>>> = alloc::vec![None; args.len()];
849        let mut items: Vec<Item> = Vec::new();
850        let mut index = 0;
851        while index < body.len() {
852            let piece = &body[index];
853            let parameter = |text: &str| params.iter().position(|param| param == text);
854            if function_like && piece.text == "#" {
855                if let Some(slot) = body.get(index + 1).and_then(|next| parameter(&next.text)) {
856                    items.push(Item::Token(PpToken::made(
857                        stringize(&args[slot]),
858                        piece.space_before,
859                        origin.clone(),
860                    )));
861                    index += 2;
862                    continue;
863                }
864            }
865            if piece.text == "##" {
866                items.push(Item::Paste);
867                index += 1;
868                continue;
869            }
870            if let Some(slot) = parameter(&piece.text).filter(|_| function_like) {
871                // §16.3.1: an argument next to `#` or `##` is taken as
872                // written; anywhere else it is "completely macro
873                // replaced as if they formed the rest of the
874                // preprocessing file".
875                let pasted = body.get(index + 1).is_some_and(|next| next.text == "##")
876                    || (index > 0 && body[index - 1].text == "##");
877                let tokens = if pasted {
878                    args[slot].clone()
879                } else {
880                    if expanded[slot].is_none() {
881                        expanded[slot] = Some(self.expand_isolated(args[slot].clone()));
882                    }
883                    expanded[slot].clone().unwrap_or_default()
884                };
885                if tokens.is_empty() {
886                    items.push(Item::Placemarker);
887                }
888                for (position, mut token) in tokens.into_iter().enumerate() {
889                    if position == 0 {
890                        token.space_before = piece.space_before;
891                    }
892                    let mut inner = chain.clone();
893                    inner.extend(token.origin.macro_chain);
894                    token.origin.macro_chain = inner;
895                    items.push(Item::Token(token));
896                }
897                index += 1;
898                continue;
899            }
900            items.push(Item::Token(PpToken::made(
901                piece.text.clone(),
902                piece.space_before,
903                origin.clone(),
904            )));
905            index += 1;
906        }
907
908        // §16.3.3/3: each `##` joins the token before it and the one
909        // after it, left to right, and the result has to be one token.
910        let mut joined: Vec<Item> = Vec::new();
911        let mut items = items.into_iter();
912        while let Some(item) = items.next() {
913            if !matches!(item, Item::Paste) {
914                joined.push(item);
915                continue;
916            }
917            let left = joined.pop();
918            let right = items.next();
919            match (left, right) {
920                (Some(Item::Placemarker), Some(right)) => joined.push(right),
921                (Some(left), Some(Item::Placemarker)) => joined.push(left),
922                (Some(Item::Token(left)), Some(Item::Token(right))) => {
923                    let text = format!("{}{}", left.text, right.text);
924                    match pptoken::tokenize(&text) {
925                        Ok(pieces) if pieces.len() == 1 => {
926                            joined.push(Item::Token(PpToken::made(
927                                text,
928                                left.space_before,
929                                origin.clone(),
930                            )));
931                        }
932                        _ => {
933                            self.preprocessing_error(
934                                invocation.origin.span,
935                                format!(
936                                    "pasting `{}` and `{}` gives `{text}`, which is not one \
937                                     preprocessing token",
938                                    left.text, right.text
939                                ),
940                                "ISO/IEC 14882:2003 §16.3.3/3",
941                            );
942                            joined.push(Item::Token(left));
943                            joined.push(Item::Token(right));
944                        }
945                    }
946                }
947                // A `##` at either end of a list is refused at `#define`
948                // (§16.3.3/1), so an operand is always there.
949                (left, right) => {
950                    joined.extend(left);
951                    joined.extend(right);
952                }
953            }
954        }
955        joined
956            .into_iter()
957            .filter_map(|item| match item {
958                Item::Token(mut token) => {
959                    for name in hide {
960                        if !token.hide.contains(name) {
961                            token.hide.push(name.clone());
962                        }
963                    }
964                    Some(token)
965                }
966                Item::Placemarker | Item::Paste => None,
967            })
968            .collect()
969    }
970
971    /// An argument expanded on its own, as §16.3.1 asks: as if it were
972    /// the rest of the file, with nothing after it to complete an
973    /// invocation.
974    ///
975    /// zerodds-lint: recursion-depth 256 (`MAX_EXPANSION_DEPTH`; one
976    /// level per argument nested in an argument, reported past it).
977    fn expand_isolated(&mut self, tokens: Vec<PpToken>) -> Vec<PpToken> {
978        if self.expansion_depth >= MAX_EXPANSION_DEPTH {
979            if let Some(first) = tokens.first() {
980                self.preprocessing_error(
981                    first.origin.span,
982                    format!(
983                        "macro arguments nest past {MAX_EXPANSION_DEPTH} levels; this one is not \
984                         expanded"
985                    ),
986                    "zerodds-idl-frontend-1.0 §3.4",
987                );
988            }
989            return tokens;
990        }
991        self.expansion_depth += 1;
992        let mut queue: VecDeque<PpToken> = tokens.into();
993        let mut out = Vec::new();
994        self.run(&mut queue, None, &mut Sink::Collect(&mut out));
995        self.expansion_depth -= 1;
996        out
997    }
998
999    /// `__LINE__` and `__FILE__`, §16.8/1, for the invocation `token`.
1000    fn predefined(&self, token: &PpToken) -> Option<PpToken> {
1001        let file = token.origin.file;
1002        let presumed = self.presumed.iter().rev().find(|(of, _, _)| *of == file);
1003        let text = match token.text.as_str() {
1004            "__LINE__" => {
1005                let physical = self
1006                    .map
1007                    .text(file)
1008                    .map_or(1, |source| line_of(source, token.origin.span.start));
1009                let delta = presumed.map_or(0, |(_, delta, _)| *delta);
1010                format!("{}", i64::from(physical) + delta)
1011            }
1012            "__FILE__" => {
1013                let name = presumed
1014                    .and_then(|(_, _, name)| name.clone())
1015                    .or_else(|| self.map.path(file).map(ToOwned::to_owned))
1016                    .unwrap_or_default();
1017                quote(&name)
1018            }
1019            _ => return None,
1020        };
1021        let mut answer = PpToken::made(text, token.space_before, token.origin.clone());
1022        answer.hide.clone_from(&token.hide);
1023        Some(answer)
1024    }
1025
1026    /// Hand a token on: into the stream, or into the list being built.
1027    fn deliver(&mut self, token: PpToken, sink: &mut Sink<'_>) {
1028        match sink {
1029            Sink::Collect(list) => list.push(token),
1030            Sink::Emit => self.emit(token),
1031        }
1032    }
1033
1034    /// Put a final token into the stream the parser reads.
1035    ///
1036    /// A token written in a file goes as the IDL lexer read it. One an
1037    /// expansion made is lexed as IDL now, and has to be one IDL token:
1038    /// `&&` is a preprocessing token and no IDL one, and is reported
1039    /// rather than passed on as something else.
1040    fn emit(&mut self, token: PpToken) {
1041        if let Some(written) = &token.written {
1042            let written = written.clone();
1043            self.push_text(written.kind, &token.text, &written, token.origin);
1044            return;
1045        }
1046        let lexed = lex(&token.text);
1047        let mut significant = lexed
1048            .iter()
1049            .filter(|lexed| lexed.kind != zerodds_idl_cst::TokenKind::Eof);
1050        match (significant.next(), significant.next()) {
1051            (Some(only), None) if only.leading.is_empty() => {
1052                let only = only.clone();
1053                self.push_text(only.kind, &token.text, &only, token.origin);
1054            }
1055            _ => {
1056                self.preprocessing_error(
1057                    token.origin.span,
1058                    format!(
1059                        "macro expansion produced `{}`, which is not an IDL token",
1060                        token.text
1061                    ),
1062                    "OMG IDL 4.2 §7.2",
1063                );
1064                let error = Token {
1065                    kind: zerodds_idl_cst::TokenKind::Error,
1066                    span: Span::default(),
1067                    leading: Vec::new(),
1068                    value: zerodds_idl_cst::TokenValue::None,
1069                };
1070                self.push_text(error.kind, &token.text, &error, token.origin);
1071            }
1072        }
1073    }
1074
1075    /// Records a diagnostic of the file being read. Its spans are that
1076    /// file's own, so one raised inside an included file names it.
1077    fn report(&mut self, mut diagnostic: Diagnostic) {
1078        if self.open.len() > 1 {
1079            diagnostic.file = self.open.last().cloned();
1080        }
1081        self.diagnostics.push(diagnostic);
1082    }
1083
1084    fn preprocessing_error(&mut self, span: Span, message: String, rule: &'static str) {
1085        self.report(Diagnostic::error(
1086            DiagnosticCode::Preprocessing,
1087            span,
1088            message,
1089            rule,
1090        ));
1091    }
1092
1093    /// Append a token to the expanded stream, keeping its trivia so the
1094    /// round trip survives preprocessing.
1095    fn push_text(
1096        &mut self,
1097        kind: zerodds_idl_cst::TokenKind,
1098        text: &str,
1099        original: &Token,
1100        origin: Origin,
1101    ) {
1102        // A separator between tokens, so the expanded text is itself
1103        // lexable: without it `struct S` becomes `structS`, and a
1104        // consumer reading the text rather than the token stream reads
1105        // one identifier where there were two.
1106        if !self.text.is_empty() {
1107            self.text.push(' ');
1108        }
1109        #[allow(clippy::cast_possible_truncation)]
1110        let start = self.text.len() as u32;
1111        self.text.push_str(text);
1112        #[allow(clippy::cast_possible_truncation)]
1113        let end = self.text.len() as u32;
1114        // Every pragma seen since the last token goes in front of this
1115        // one. The text they name is appended too, because the lowering
1116        // reads the directive's own bytes out of the span.
1117        self.map.push_range(Span::new(start, end), origin.clone());
1118        let leading = core::mem::take(&mut self.pending_pragmas)
1119            .into_iter()
1120            .map(|(text, held, file)| {
1121                self.text.push(' ');
1122                #[allow(clippy::cast_possible_truncation)]
1123                let from = self.text.len() as u32;
1124                self.text.push_str(&text);
1125                #[allow(clippy::cast_possible_truncation)]
1126                let to = self.text.len() as u32;
1127                self.map
1128                    .push_range(Span::new(from, to), Origin::literal(file, held.span));
1129                Trivia {
1130                    kind: held.kind,
1131                    span: Span::new(from, to),
1132                }
1133            })
1134            .collect();
1135        self.tokens.push(Token {
1136            kind,
1137            span: Span::new(start, end),
1138            leading,
1139            value: original.value.clone(),
1140        });
1141        self.map.push(origin);
1142    }
1143
1144    /// Attach any pragma left over at the end of the unit to the last
1145    /// token, so a directive written after the declaration it names is
1146    /// not lost with the end of the file.
1147    fn flush_trailing_pragmas(&mut self) {
1148        if self.pending_pragmas.is_empty() {
1149            return;
1150        }
1151        let pending = core::mem::take(&mut self.pending_pragmas);
1152        let mut on_last = Vec::new();
1153        let mut on_eof = Vec::new();
1154        let mut eof_file = None;
1155        for (text, held, file) in pending {
1156            self.text.push(' ');
1157            #[allow(clippy::cast_possible_truncation)]
1158            let from = self.text.len() as u32;
1159            self.text.push_str(&text);
1160            #[allow(clippy::cast_possible_truncation)]
1161            let to = self.text.len() as u32;
1162            self.map
1163                .push_range(Span::new(from, to), Origin::literal(file, held.span));
1164            let moved = Trivia {
1165                kind: held.kind,
1166                span: Span::new(from, to),
1167            };
1168            // A pragma names the declaration it follows, so the last
1169            // token is the same declaration and carrying it there is
1170            // right. An annotation is read from the leading trivia of
1171            // the token *after* the declaration, so the last token is
1172            // exactly where it must not go: it would be reported as a
1173            // `//@` in a position that admits none. It gets an
1174            // end-of-file token to lead instead, which is the position
1175            // the unpreprocessed parse gives it.
1176            if held.kind == TriviaKind::LineComment {
1177                on_eof.push(moved);
1178                eof_file = eof_file.or(Some(file));
1179            } else {
1180                on_last.push(moved);
1181            }
1182        }
1183        if let Some(last) = self.tokens.last_mut() {
1184            last.leading.extend(on_last);
1185        }
1186        let (Some(file), false) = (eof_file, on_eof.is_empty()) else {
1187            return;
1188        };
1189        #[allow(clippy::cast_possible_truncation)]
1190        let at = self.text.len() as u32;
1191        self.tokens.push(Token {
1192            kind: zerodds_idl_cst::TokenKind::Eof,
1193            span: Span::new(at, at),
1194            leading: on_eof,
1195            value: zerodds_idl_cst::TokenValue::None,
1196        });
1197        self.map.push(Origin {
1198            file,
1199            span: Span::new(at, at),
1200            macro_chain: Vec::new(),
1201        });
1202    }
1203
1204    /// `#define`, the text after the word: a name, a parameter list
1205    /// where `(` follows the name with no space (§16.3/3 — `#define F
1206    /// (x)` is object-like), and the replacement list.
1207    fn define(&mut self, rest: &str, span: Span) {
1208        let text = rest.trim_start();
1209        let name_end = text
1210            .find(|c: char| !(c.is_ascii_alphanumeric() || c == '_' || !c.is_ascii()))
1211            .unwrap_or(text.len());
1212        let name = &text[..name_end];
1213        if name.is_empty() {
1214            self.report(Diagnostic::error(
1215                DiagnosticCode::MissingToken,
1216                span,
1217                "`#define` without a name".to_string(),
1218                "OMG IDL 4.2 §7.3",
1219            ));
1220            return;
1221        }
1222        let after = &text[name_end..];
1223        let defined = if let Some(list) = after.strip_prefix('(') {
1224            let Some(close) = list.find(')') else {
1225                self.preprocessing_error(
1226                    span,
1227                    format!("the parameter list of `{name}` is not closed"),
1228                    "ISO/IEC 14882:2003 §16.3",
1229                );
1230                return;
1231            };
1232            let params: Vec<String> = if list[..close].trim().is_empty() {
1233                Vec::new()
1234            } else {
1235                list[..close]
1236                    .split(',')
1237                    .map(|param| param.trim().to_string())
1238                    .collect()
1239            };
1240            self.macros
1241                .define_function(name, params, &list[close + 1..])
1242        } else {
1243            self.macros.define(name, after)
1244        };
1245        if let Err(why) = defined {
1246            self.preprocessing_error(
1247                span,
1248                format!("`#define {name}`: {why}"),
1249                "ISO/IEC 14882:2003 §16.3",
1250            );
1251        }
1252    }
1253
1254    #[allow(clippy::too_many_lines)]
1255    fn directive(&mut self, raw: &str, span: Span, cursor: &mut FileCursor<'_>) {
1256        // §2.1/1 phase 2: a backslash at the end of a line splices it to
1257        // the next. The lexer keeps a continued directive one trivia; the
1258        // splice is made here, before the directive is read.
1259        let line = splice(raw);
1260        let body = line.trim_start().trim_start_matches('#').trim();
1261        let (word, rest) = split_word(body);
1262        let keeping = cursor.keeping();
1263        let file = cursor.file;
1264
1265        match word {
1266            "define" if keeping => self.define(rest, span),
1267            // ISO/IEC 14882:2003 §16.5, which IDL 4.2 §7.3 adopts
1268            // wholesale: "#error causes the implementation to produce a
1269            // diagnostic message that includes the specified sequence of
1270            // preprocessing tokens." The directive was read and dropped,
1271            // so a file whose whole purpose was to refuse an unsupported
1272            // configuration compiled clean and emitted an empty
1273            // translation unit.
1274            //
1275            // `if keeping` is the whole of the rule's subtlety: an
1276            // `#error` inside a branch the conditionals cut out is text
1277            // that was never reached, and reporting it would refuse every
1278            // file that guards one — which is nearly every file that has
1279            // one at all.
1280            "error" if keeping => {
1281                let text = rest.trim();
1282                let message = if text.is_empty() {
1283                    String::from("`#error`")
1284                } else {
1285                    format!("`#error {text}`")
1286                };
1287                self.report(Diagnostic::error(
1288                    DiagnosticCode::ErrorDirective,
1289                    span,
1290                    message,
1291                    "OMG IDL 4.2 §7.3, ISO/IEC 14882:2003 §16.5",
1292                ));
1293            }
1294            "undef" if keeping => {
1295                let (name, _) = split_word(rest);
1296                if macro_::RESERVED.contains(&name) {
1297                    self.preprocessing_error(
1298                        span,
1299                        format!("`{name}` cannot be undefined"),
1300                        "ISO/IEC 14882:2003 §16.8/4",
1301                    );
1302                } else {
1303                    self.macros.undefine(name);
1304                }
1305            }
1306            "include" if keeping => {
1307                let Some(target) = include_target(rest) else {
1308                    self.report(Diagnostic::error(
1309                        DiagnosticCode::UnexpectedToken,
1310                        span,
1311                        "`#include` expects \"path\" or <path>".to_string(),
1312                        "OMG IDL 4.2 §7.3",
1313                    ));
1314                    return;
1315                };
1316                let from = self.map.path(file).map(ToOwned::to_owned);
1317                match self.loader.load(&target, from.as_deref()) {
1318                    Some((resolved, text)) => self.include(&resolved, &text, cursor.depth + 1),
1319                    None => self.report(Diagnostic::error(
1320                        DiagnosticCode::UnexpectedToken,
1321                        span,
1322                        format!("cannot find `{target}`"),
1323                        "OMG IDL 4.2 §7.3",
1324                    )),
1325                }
1326            }
1327            "line" if keeping => self.line_control(rest, span, cursor),
1328            "ifdef" => {
1329                let (name, _) = split_word(rest);
1330                cursor
1331                    .conditions
1332                    .push(Condition::new(keeping && self.macros.is_defined(name)));
1333            }
1334            "ifndef" => {
1335                let (name, _) = split_word(rest);
1336                cursor
1337                    .conditions
1338                    .push(Condition::new(keeping && !self.macros.is_defined(name)));
1339            }
1340            "if" => {
1341                // A condition inside a branch already cut out is not
1342                // evaluated: it may name what that branch's own guard
1343                // says is missing, and §16.1/6 skips the group whole.
1344                let taken = keeping && self.evaluate_condition(rest, span, file);
1345                cursor.conditions.push(Condition::new(taken));
1346            }
1347            "elif" => {
1348                let outer = cursor.conditions.len().saturating_sub(1);
1349                let enclosing_keeps = cursor.conditions[..outer].iter().all(Condition::keeping);
1350                let open = cursor
1351                    .conditions
1352                    .last()
1353                    .map(|condition| condition.satisfied);
1354                match open {
1355                    Some(satisfied) => {
1356                        let taken = enclosing_keeps
1357                            && !satisfied
1358                            && self.evaluate_condition(rest, span, file);
1359                        if let Some(condition) = cursor.conditions.last_mut() {
1360                            condition.elif(taken);
1361                        }
1362                    }
1363                    None => self.unmatched(span, "#elif"),
1364                }
1365            }
1366            "else" => match cursor.conditions.last_mut() {
1367                Some(condition) => condition.otherwise(),
1368                None => self.unmatched(span, "#else"),
1369            },
1370            "endif" => {
1371                if cursor.conditions.pop().is_none() {
1372                    self.unmatched(span, "#endif");
1373                }
1374            }
1375            // A pragma is not the preprocessor's business — but it has to
1376            // survive it. It carries declaration metadata the lowering
1377            // reads out of the trivia, so it is held here and attached to
1378            // the next token emitted rather than dropped with the rest of
1379            // the directive.
1380            //
1381            // With one exception: `#pragma once` is the one pragma this
1382            // stage owns. It says something about inclusion and nothing
1383            // about the declaration that follows, so it is answered
1384            // here and never handed on — a directive this stage has
1385            // acted on is not one the lowering has to report as
1386            // uninterpreted.
1387            "pragma" if keeping && rest.trim() == "once" => {
1388                if let Some(current) = self.open.last().cloned()
1389                    && !self.once.contains(&current)
1390                {
1391                    self.once.push(current);
1392                }
1393            }
1394            "pragma" => {
1395                if keeping {
1396                    self.pending_pragmas.push((
1397                        line.trim().to_string(),
1398                        Trivia {
1399                            kind: TriviaKind::Directive,
1400                            span,
1401                        },
1402                        file,
1403                    ));
1404                }
1405            }
1406            _ => {}
1407        }
1408    }
1409
1410    /// `#line digit-sequence ["s-char-sequence"]`, §16.4: the line after
1411    /// the directive is numbered `digit-sequence`, and the file is called
1412    /// what the string says. Macros in the line are replaced first
1413    /// (§16.4/5).
1414    fn line_control(&mut self, rest: &str, span: Span, cursor: &FileCursor<'_>) {
1415        let tokens = self.expand_line(rest, span, cursor.file);
1416        let mut texts = tokens.iter().map(|token| token.text.as_str());
1417        let number = texts.next().and_then(|digits| {
1418            if digits.bytes().all(|b| b.is_ascii_digit()) {
1419                digits.parse::<i64>().ok()
1420            } else {
1421                None
1422            }
1423        });
1424        let name = match texts.next() {
1425            None => None,
1426            Some(literal) => match unquote(literal) {
1427                Some(name) => Some(name),
1428                None => {
1429                    self.preprocessing_error(
1430                        span,
1431                        format!("`#line` names its file with a string literal, not `{literal}`"),
1432                        "ISO/IEC 14882:2003 §16.4/4",
1433                    );
1434                    return;
1435                }
1436            },
1437        };
1438        let Some(number) = number.filter(|n| (1..=2_147_483_647).contains(n)) else {
1439            self.preprocessing_error(
1440                span,
1441                "`#line` takes a line number from 1 to 2147483647".to_string(),
1442                "ISO/IEC 14882:2003 §16.4/3",
1443            );
1444            return;
1445        };
1446        // The physical line the directive's successor starts on: the
1447        // directive's own, plus any line it was continued over, plus one.
1448        let next = line_of(cursor.source, span.end) + 1;
1449        let delta = number - i64::from(next);
1450        let name = name.or_else(|| {
1451            self.presumed
1452                .iter()
1453                .rev()
1454                .find(|(of, _, _)| *of == cursor.file)
1455                .and_then(|(_, _, name)| name.clone())
1456        });
1457        self.presumed.push((cursor.file, delta, name));
1458    }
1459
1460    /// The tokens of a directive's operands with every macro replaced,
1461    /// for `#line` and `#if`.
1462    fn expand_line(&mut self, rest: &str, span: Span, file: FileId) -> Vec<PpToken> {
1463        match pptoken::tokenize(rest) {
1464            Ok(pieces) => self.expand_isolated(
1465                pieces
1466                    .into_iter()
1467                    .map(|piece| {
1468                        PpToken::made(piece.text, piece.space_before, Origin::literal(file, span))
1469                    })
1470                    .collect(),
1471            ),
1472            Err(why) => {
1473                self.preprocessing_error(span, why, "ISO/IEC 14882:2003 §2.4");
1474                Vec::new()
1475            }
1476        }
1477    }
1478
1479    /// The truth of an `#if` or `#elif` line, §16.1.
1480    fn evaluate_condition(&mut self, rest: &str, span: Span, file: FileId) -> bool {
1481        let pieces = match pptoken::tokenize(rest) {
1482            Ok(pieces) => pieces,
1483            Err(why) => {
1484                self.preprocessing_error(span, why, "ISO/IEC 14882:2003 §2.4");
1485                return false;
1486            }
1487        };
1488        // `defined X` and `defined ( X )` are answered before any macro
1489        // is replaced (§16.1/1): replacing first would ask whether the
1490        // *body* of `X` is defined.
1491        let mut tokens = Vec::new();
1492        let mut index = 0;
1493        while index < pieces.len() {
1494            let piece = &pieces[index];
1495            if piece.text != "defined" {
1496                tokens.push(PpToken::made(
1497                    piece.text.clone(),
1498                    piece.space_before,
1499                    Origin::literal(file, span),
1500                ));
1501                index += 1;
1502                continue;
1503            }
1504            let (name, next) = if pieces.get(index + 1).is_some_and(|p| p.text == "(") {
1505                let closed = pieces.get(index + 3).is_some_and(|p| p.text == ")");
1506                (pieces.get(index + 2).filter(|_| closed), index + 4)
1507            } else {
1508                (pieces.get(index + 1), index + 2)
1509            };
1510            let Some(name) = name.filter(|name| pptoken::is_identifier(&name.text)) else {
1511                self.preprocessing_error(
1512                    span,
1513                    "`defined` is followed by a macro name, alone or in parentheses".to_string(),
1514                    "ISO/IEC 14882:2003 §16.1/1",
1515                );
1516                return false;
1517            };
1518            let answer = if self.macros.is_defined(&name.text)
1519                || macro_::RESERVED[1..].contains(&name.text.as_str())
1520            {
1521                "1"
1522            } else {
1523                "0"
1524            };
1525            tokens.push(PpToken::made(
1526                answer.to_string(),
1527                piece.space_before,
1528                Origin::literal(file, span),
1529            ));
1530            index = next;
1531        }
1532        let expanded = self.expand_isolated(tokens);
1533        if expanded.iter().any(|token| token.text == "defined") {
1534            // §16.1/4: "If the token defined is generated as a result of
1535            // this replacement process … the behavior is undefined."
1536            self.preprocessing_error(
1537                span,
1538                "a macro in this condition expands to `defined`".to_string(),
1539                "ISO/IEC 14882:2003 §16.1/4",
1540            );
1541            return false;
1542        }
1543        let texts: Vec<String> = expanded.into_iter().map(|token| token.text).collect();
1544        match condition::evaluate(&texts) {
1545            Ok(outcome) => {
1546                for name in outcome.zeroed {
1547                    self.report(Diagnostic::warning(
1548                        DiagnosticCode::Preprocessing,
1549                        span,
1550                        format!("`{name}` in this condition names no macro and is taken as 0"),
1551                        "ISO/IEC 14882:2003 §16.1/4",
1552                    ));
1553                }
1554                outcome.value
1555            }
1556            Err(why) => {
1557                self.preprocessing_error(
1558                    span,
1559                    format!("`#if {}`: {why}", rest.trim()),
1560                    "ISO/IEC 14882:2003 §16.1",
1561                );
1562                false
1563            }
1564        }
1565    }
1566
1567    fn unmatched(&mut self, span: Span, what: &str) {
1568        self.report(Diagnostic::error(
1569            DiagnosticCode::UnexpectedToken,
1570            span,
1571            format!("`{what}` without a matching `#if`"),
1572            "OMG IDL 4.2 §7.3",
1573        ));
1574    }
1575}
1576
1577/// A directive line with every backslash-newline removed, §2.1/1
1578/// phase 2.
1579fn splice(line: &str) -> String {
1580    line.replace("\\\r\n", "").replace("\\\n", "")
1581}
1582
1583/// The 1-based line `at` lies on in `source`.
1584fn line_of(source: &str, at: u32) -> u32 {
1585    let end = usize::try_from(at).unwrap_or(usize::MAX).min(source.len());
1586    let newlines = source.as_bytes()[..end]
1587        .iter()
1588        .filter(|byte| **byte == b'\n')
1589        .count();
1590    u32::try_from(newlines)
1591        .unwrap_or(u32::MAX)
1592        .saturating_add(1)
1593}
1594
1595/// `text` as a string literal, `\` and `"` escaped.
1596fn quote(text: &str) -> String {
1597    let mut out = String::with_capacity(text.len() + 2);
1598    out.push('"');
1599    for ch in text.chars() {
1600        if ch == '\\' || ch == '"' {
1601            out.push('\\');
1602        }
1603        out.push(ch);
1604    }
1605    out.push('"');
1606    out
1607}
1608
1609/// The contents of a string literal, escapes undone, or `None` for
1610/// anything else.
1611fn unquote(literal: &str) -> Option<String> {
1612    let inner = literal.strip_prefix('"')?.strip_suffix('"')?;
1613    let mut out = String::with_capacity(inner.len());
1614    let mut chars = inner.chars();
1615    while let Some(ch) = chars.next() {
1616        if ch == '\\' {
1617            out.push(chars.next()?);
1618        } else {
1619            out.push(ch);
1620        }
1621    }
1622    Some(out)
1623}
1624
1625/// `#parameter`, §16.3.2/2: the argument's spelling as a string
1626/// literal — one space where white space stood between two tokens, none
1627/// at either end, and `\` and `"` escaped inside a string or character
1628/// literal of the argument.
1629fn stringize(argument: &[PpToken]) -> String {
1630    let mut spelled = String::new();
1631    for (index, token) in argument.iter().enumerate() {
1632        if index > 0 && token.space_before {
1633            spelled.push(' ');
1634        }
1635        let literal = token.text.starts_with(['"', '\''])
1636            || token.text.starts_with("L\"")
1637            || token.text.starts_with("L'");
1638        if literal {
1639            for ch in token.text.chars() {
1640                if ch == '\\' || ch == '"' {
1641                    spelled.push('\\');
1642                }
1643                spelled.push(ch);
1644            }
1645        } else {
1646            spelled.push_str(&token.text);
1647        }
1648    }
1649    format!("\"{spelled}\"")
1650}
1651
1652/// One level of conditional nesting.
1653#[derive(Debug, Clone, Copy)]
1654struct Condition {
1655    /// Whether the branch now open is being kept.
1656    active: bool,
1657    /// Whether any branch of this conditional has been taken, so a later
1658    /// `#elif` or `#else` knows to stay closed.
1659    satisfied: bool,
1660}
1661
1662impl Condition {
1663    const fn new(taken: bool) -> Self {
1664        Self {
1665            active: taken,
1666            satisfied: taken,
1667        }
1668    }
1669
1670    const fn keeping(&self) -> bool {
1671        self.active
1672    }
1673
1674    const fn elif(&mut self, taken: bool) {
1675        self.active = taken && !self.satisfied;
1676        if self.active {
1677            self.satisfied = true;
1678        }
1679    }
1680
1681    const fn otherwise(&mut self) {
1682        self.active = !self.satisfied;
1683        self.satisfied = true;
1684    }
1685}
1686
1687/// Split off the first whitespace-delimited word.
1688fn split_word(text: &str) -> (&str, &str) {
1689    let trimmed = text.trim_start();
1690    match trimmed.find(char::is_whitespace) {
1691        Some(at) => (&trimmed[..at], &trimmed[at..]),
1692        None => (trimmed, ""),
1693    }
1694}
1695
1696/// The path out of `"name"` or `<name>`.
1697fn include_target(rest: &str) -> Option<String> {
1698    let text = rest.trim();
1699    let inner = text
1700        .strip_prefix('"')
1701        .and_then(|s| s.strip_suffix('"'))
1702        .or_else(|| text.strip_prefix('<').and_then(|s| s.strip_suffix('>')))?;
1703    Some(inner.to_owned())
1704}