Skip to main content

zerodds_idl/preprocessor/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! C-style preprocessor for OMG IDL 4.2.
4//!
5//! IDL inherits from the C preprocessor — and vendor IDL files (RTI, OpenSplice)
6//! use `#include`, `#define`, `#ifdef`, `#pragma` regularly.
7//! So that the parser can consume such files, the
8//! preprocessor sits BEFORE the lexer and expands the directives into pure
9//! IDL source.
10//!
11//! # Scope
12//!
13//! - **`#include "rel/path.idl"`** and **`#include <abs/path.idl>`**:
14//!   text-based inclusion via the [`Resolver`] trait
15//! - **`#define MACRO value`** (object-like) and
16//!   **`#define NAME(p1, p2) body`** (function-like) incl.
17//!   `#` stringize and `##` token-paste (Spec §7.2.5 + ISO 14882
18//!   §16.3.2/§16.3.3)
19//! - **`#ifdef`** / **`#ifndef`** / **`#if`** / **`#elif`** /
20//!   **`#else`** / **`#endif`**: conditional compilation with
21//!   expression eval (`defined`, `&&`, `||`, `!`, numeric literals)
22//! - **`#pragma <args>`**: stripped (not in the output) — vendor pragmas
23//!   like RTI's `#pragma keylist` are captured as special AST nodes
24//! - **`#undef`**: remove a macro
25//!
26//! Not supported:
27//! - Recursive macro expansion (one pass)
28//! - Variadic macros (`__VA_ARGS__`)
29//! - `#error`, `#warning`, `#line` (parsed, but not functional)
30//! - Full C-PP arithmetic in `#if` (comparisons, bitops, ternary)
31//!
32//! # Source map
33//!
34//! [`SourceMap`] maps every position in the expanded output to
35//! `(file_id, byte_offset_in_original)`. So that diagnostics after
36//! parsing point to the correct original file and line.
37//!
38//! # Example
39//!
40//! ```
41//! use zerodds_idl::preprocessor::{Preprocessor, MemoryResolver};
42//!
43//! let mut resolver = MemoryResolver::new();
44//! resolver.add("Foo.idl", "struct Foo { long x; };");
45//!
46//! let pp = Preprocessor::new(resolver);
47//! let result = pp.process("main.idl", r#"
48//!     #include "Foo.idl"
49//!     #define MAX 100
50//!     struct Bar { long limit; };
51//! "#).expect("preprocess");
52//!
53//! assert!(result.expanded.contains("struct Foo"));
54//! assert!(result.expanded.contains("struct Bar"));
55//! assert!(!result.expanded.contains("#define"));
56//! ```
57
58#![allow(missing_docs)] // field-level doc-comments not complete everywhere
59
60mod source_map;
61
62pub use source_map::{FileId, SourceLocation, SourceMap};
63
64use std::collections::HashMap;
65
66/// Trait for include-file resolution.
67///
68/// Allows file IO (`FsResolver`), in-memory tests (`MemoryResolver`)
69/// or custom strategies.
70pub trait Resolver {
71    /// Resolves a `#include "path"` (relative) or `#include <path>`
72    /// (system) to source text.
73    ///
74    /// # Errors
75    /// Implementation-specific. Should return a meaningful error
76    /// that contains the requested path.
77    fn resolve(&self, requesting_file: &str, include: &Include) -> Result<String, ResolveError>;
78}
79
80/// Describes an include request.
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub enum Include {
83    /// `#include "name"` — relative/local search.
84    Quoted(String),
85    /// `#include <name>` — system-path search.
86    System(String),
87}
88
89impl Include {
90    /// Path component independent of the style.
91    #[must_use]
92    pub fn path(&self) -> &str {
93        match self {
94            Self::Quoted(p) | Self::System(p) => p,
95        }
96    }
97}
98
99/// Resolver error. Missing file, IO error, etc.
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct ResolveError {
102    /// Requested path (as it appeared in the `#include`).
103    pub requested: String,
104    /// Meaningful description.
105    pub message: String,
106}
107
108/// In-memory resolver for tests and CLI tools that manage source without
109/// filesystem access.
110#[derive(Debug, Clone, Default)]
111pub struct MemoryResolver {
112    files: HashMap<String, String>,
113}
114
115impl MemoryResolver {
116    #[must_use]
117    pub fn new() -> Self {
118        Self {
119            files: HashMap::new(),
120        }
121    }
122
123    /// Registers a file.
124    pub fn add(&mut self, name: impl Into<String>, content: impl Into<String>) {
125        self.files.insert(name.into(), content.into());
126    }
127}
128
129impl Resolver for MemoryResolver {
130    fn resolve(&self, _requesting: &str, include: &Include) -> Result<String, ResolveError> {
131        let path = include.path();
132        self.files.get(path).cloned().ok_or_else(|| ResolveError {
133            requested: path.to_string(),
134            message: format!("file not in MemoryResolver: {path}"),
135        })
136    }
137}
138
139/// `#pragma prefix "<prefix>"` — CORBA Part 1 §14.7.5.
140///
141/// Collected by the preprocessor; checked by the consumer (spec validator) against
142/// `typeprefix` decls for a repository-ID conflict (IDL 4.2
143/// §7.4.6.4.1.3).
144#[derive(Debug, Clone, PartialEq, Eq)]
145pub struct PragmaPrefix {
146    /// Prefix string (without surrounding quotes).
147    pub prefix: String,
148    /// Source file.
149    pub file: String,
150    /// Line (1-based).
151    pub line: usize,
152}
153
154/// `#pragma keylist Foo a b c` — Cyclone/OpenSplice convention.
155///
156/// Collected by the preprocessor; can be read by the consumer (idlc, AST builder)
157/// via [`ProcessedSource::pragma_keylists`].
158#[derive(Debug, Clone, PartialEq, Eq)]
159pub struct PragmaKeylist {
160    /// Topic-type name.
161    pub type_name: String,
162    /// Key member names.
163    pub keys: Vec<String>,
164    /// Source file.
165    pub file: String,
166    /// Line (1-based).
167    pub line: usize,
168}
169
170/// OpenSplice-legacy-specific pragmas (`#pragma DCPS_DATA_TYPE`,
171/// `#pragma DCPS_DATA_KEY`, `#pragma cats`, `#pragma genequality`).
172///
173/// In OpenSplice versions 5.x/6.x these pragmas were the primary
174/// method of marking IDL types as DDS topics — before the OMG-IDL-4.2
175/// annotation `@key`. Migration use cases must map them to modern
176/// `@key`/`@topic` annotations.
177#[derive(Debug, Clone, PartialEq, Eq)]
178pub enum OpenSplicePragma {
179    /// `#pragma DCPS_DATA_TYPE "<TypeName>"` — marks the type as a
180    /// DDS-topic type.
181    DataType {
182        type_name: String,
183        file: String,
184        line: usize,
185    },
186    /// `#pragma DCPS_DATA_KEY "<TypeName>.<field>"` (dot form) or
187    /// `#pragma DCPS_DATA_KEY <TypeName> <field>...` (OpenDDS space
188    /// form, one or more fields) — marks members as keys.
189    DataKey {
190        type_name: String,
191        /// One or more key fields.
192        fields: Vec<String>,
193        file: String,
194        line: usize,
195    },
196    /// `#pragma cats <TypeName> <field>` — catenated keys
197    /// (alternative key marking).
198    Cats {
199        type_name: String,
200        keys: Vec<String>,
201        file: String,
202        line: usize,
203    },
204    /// `#pragma genequality` — codegen flag for the equality operator
205    /// in C++/Java bindings.
206    GenEquality { file: String, line: usize },
207}
208
209/// `#pragma dds_xtopics version="1.3"` (XTypes 1.3 §7.3.1.1.1) —
210/// allows an IDL file to mark which XTypes spec
211/// version it was written against. The compiler frontend can then
212/// validate vendor extensions with/without a version match.
213#[derive(Debug, Clone, PartialEq, Eq)]
214pub struct PragmaDdsXtopics {
215    /// Version string (e.g. `"1.3"`). Empty if not specified.
216    pub version: String,
217    /// Source file.
218    pub file: String,
219    /// Line.
220    pub line: usize,
221}
222
223/// Result of a preprocessor run.
224#[derive(Debug, Clone, Default)]
225pub struct ProcessedSource {
226    /// Expanded IDL source, ready for the lexer.
227    pub expanded: String,
228    /// Mapping from output position to original (file, position).
229    pub source_map: SourceMap,
230    /// Collected `#pragma keylist` directives.
231    pub pragma_keylists: Vec<PragmaKeylist>,
232    /// Collected OpenSplice legacy pragmas (`DCPS_DATA_TYPE`,
233    /// `DCPS_DATA_KEY`, `cats`, `genequality`).
234    pub opensplice_pragmas: Vec<OpenSplicePragma>,
235    /// Collected `#pragma prefix "<prefix>"` directives (CORBA Part 1
236    /// §14.7.5). Used by the spec validator for §7.4.6.4.1.3 repository-ID
237    /// conflict detection.
238    pub pragma_prefixes: Vec<PragmaPrefix>,
239    /// Collected `#pragma dds_xtopics version="..."` directives
240    /// (XTypes 1.3 §7.3.1.1.1). Multiple pragmas per file are allowed;
241    /// the validator checks version consistency.
242    pub pragma_dds_xtopics: Vec<PragmaDdsXtopics>,
243}
244
245/// Top-level preprocessor error.
246#[derive(Debug, Clone, PartialEq, Eq)]
247pub enum PreprocessError {
248    /// `#include` could not be resolved.
249    IncludeNotFound(ResolveError),
250    /// `#include` already in the expansion stack — cycle detected.
251    IncludeCycle {
252        /// File that was to be included cyclically.
253        file: String,
254    },
255    /// `#endif` without a matching `#ifdef`/`#ifndef`.
256    UnmatchedEndif {
257        /// Source file of the unmatched directive.
258        file: String,
259        /// 1-indexed line.
260        line: usize,
261    },
262    /// `#else` without a matching `#ifdef`/`#ifndef`.
263    UnmatchedElse { file: String, line: usize },
264    /// `#ifdef`/`#ifndef` without a closing `#endif`.
265    UnclosedConditional { file: String, line: usize },
266    /// Directive with faulty syntax (e.g. `#define` without a name).
267    SyntaxError {
268        file: String,
269        line: usize,
270        message: String,
271    },
272    /// `#error <message>` — explicitly requested build stop.
273    ErrorDirective {
274        /// Source file.
275        file: String,
276        /// Line.
277        line: usize,
278        /// Content of the directive.
279        message: String,
280    },
281    /// Backslash as the last character in the source file — Spec §7.3:
282    /// "A backslash character may not be the last character in a source
283    /// file."
284    TrailingBackslash {
285        /// Source file.
286        file: String,
287    },
288}
289
290impl core::fmt::Display for PreprocessError {
291    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
292        match self {
293            Self::IncludeNotFound(e) => write!(f, "include not found: {}", e.requested),
294            Self::IncludeCycle { file } => write!(f, "include cycle: {file}"),
295            Self::UnmatchedEndif { file, line } => {
296                write!(f, "unmatched #endif at {file}:{line}")
297            }
298            Self::UnmatchedElse { file, line } => {
299                write!(f, "unmatched #else at {file}:{line}")
300            }
301            Self::UnclosedConditional { file, line } => {
302                write!(f, "unclosed conditional starting at {file}:{line}")
303            }
304            Self::SyntaxError {
305                file,
306                line,
307                message,
308            } => {
309                write!(f, "preprocessor syntax error at {file}:{line}: {message}")
310            }
311            Self::ErrorDirective {
312                file,
313                line,
314                message,
315            } => {
316                write!(f, "#error at {file}:{line}: {message}")
317            }
318            Self::TrailingBackslash { file } => {
319                write!(f, "trailing backslash at end of source file: {file}")
320            }
321        }
322    }
323}
324
325impl std::error::Error for PreprocessError {}
326
327/// Top-level preprocessor.
328///
329/// Construction via [`Preprocessor::new`] with a [`Resolver`].
330/// Then call `process(file_name, source)`.
331pub struct Preprocessor<R: Resolver> {
332    resolver: R,
333}
334
335impl<R: Resolver> Preprocessor<R> {
336    pub fn new(resolver: R) -> Self {
337        Self { resolver }
338    }
339
340    /// Processes a source string and expands all directives.
341    ///
342    /// `file_name` is shown in diagnostics and is the "current
343    /// file" for relative `#include` resolution.
344    ///
345    /// # Errors
346    /// See [`PreprocessError`].
347    pub fn process(
348        &self,
349        file_name: &str,
350        source: &str,
351    ) -> Result<ProcessedSource, PreprocessError> {
352        let mut state = State::new();
353        let root_id = state.source_map.add_file(file_name);
354        let mut output = String::new();
355        // Backslash-Newline-Continuation (Spec §7.3, ISO 14882 5.2):
356        // A `\` directly before `\n` is replaced by a token splice — i.e.
357        // both characters are removed, the following line continues
358        // syntactically. We do this before the line iteration, so that
359        // multi-line `#define X foo \\\n bar` is recognized correctly.
360        let spliced = splice_backslash_newlines(source);
361        // Spec §7.3: "A backslash character may not be the last character
362        // in a source file." If a backslash remains at the file end
363        // after splicing, it was not followed by a `\n`.
364        if spliced.ends_with('\\') {
365            return Err(PreprocessError::TrailingBackslash {
366                file: file_name.to_string(),
367            });
368        }
369        self.expand_into(file_name, &spliced, root_id, &mut state, &mut output, 0)?;
370        Ok(ProcessedSource {
371            expanded: output,
372            source_map: state.source_map,
373            pragma_keylists: state.pragma_keylists,
374            opensplice_pragmas: state.opensplice_pragmas,
375            pragma_prefixes: state.pragma_prefixes,
376            pragma_dds_xtopics: state.pragma_dds_xtopics,
377        })
378    }
379
380    fn expand_into(
381        &self,
382        file_name: &str,
383        source: &str,
384        file_id: FileId,
385        state: &mut State,
386        output: &mut String,
387        depth: usize,
388    ) -> Result<(), PreprocessError> {
389        if state.include_stack.iter().any(|f| f == file_name) {
390            return Err(PreprocessError::IncludeCycle {
391                file: file_name.to_string(),
392            });
393        }
394        state.include_stack.push(file_name.to_string());
395
396        let mut conditional_stack: Vec<ConditionalFrame> = Vec::new();
397        let mut byte_offset = 0usize;
398
399        for (line_idx, line) in source.split_inclusive('\n').enumerate() {
400            let line_no = line_idx + 1;
401            let trimmed = line.trim_start();
402
403            // Conditional skipping: if currently in an inactive
404            // frame, skip everything except #else/#endif/#ifdef/#ifndef.
405            let active = conditional_stack.iter().all(|f| f.active);
406
407            if let Some(directive) = parse_directive(trimmed) {
408                match directive {
409                    Directive::Ifdef(name) => {
410                        let parent_active = active;
411                        let cond = parent_active && state.macros.contains_key(name);
412                        conditional_stack.push(ConditionalFrame {
413                            active: cond,
414                            else_seen: false,
415                            parent_active,
416                            taken: cond,
417                        });
418                    }
419                    Directive::Ifndef(name) => {
420                        let parent_active = active;
421                        let cond = parent_active && !state.macros.contains_key(name);
422                        conditional_stack.push(ConditionalFrame {
423                            active: cond,
424                            else_seen: false,
425                            parent_active,
426                            taken: cond,
427                        });
428                    }
429                    Directive::Else => {
430                        let frame = conditional_stack.last_mut().ok_or_else(|| {
431                            PreprocessError::UnmatchedElse {
432                                file: file_name.to_string(),
433                                line: line_no,
434                            }
435                        })?;
436                        if frame.else_seen {
437                            return Err(PreprocessError::SyntaxError {
438                                file: file_name.to_string(),
439                                line: line_no,
440                                message: "duplicate #else".to_string(),
441                            });
442                        }
443                        frame.else_seen = true;
444                        // Else activates only if the parent is active AND
445                        // NO branch has been taken so far.
446                        frame.active = frame.parent_active && !frame.taken;
447                        if frame.active {
448                            frame.taken = true;
449                        }
450                    }
451                    Directive::Endif => {
452                        if conditional_stack.pop().is_none() {
453                            return Err(PreprocessError::UnmatchedEndif {
454                                file: file_name.to_string(),
455                                line: line_no,
456                            });
457                        }
458                    }
459                    Directive::If(expr) => {
460                        let parent_active = active;
461                        let cond = parent_active && eval_if_expr(expr, &state.macros);
462                        conditional_stack.push(ConditionalFrame {
463                            parent_active,
464                            active: cond,
465                            else_seen: false,
466                            taken: cond,
467                        });
468                    }
469                    Directive::Elif(expr) => {
470                        let Some(frame) = conditional_stack.last_mut() else {
471                            return Err(PreprocessError::UnmatchedEndif {
472                                file: file_name.to_string(),
473                                line: line_no,
474                            });
475                        };
476                        if frame.else_seen {
477                            return Err(PreprocessError::SyntaxError {
478                                file: file_name.to_string(),
479                                line: line_no,
480                                message: "#elif after #else".to_string(),
481                            });
482                        }
483                        // Active only if the parent is active AND no
484                        // branch has been taken yet AND expr=true.
485                        let cond = frame.parent_active
486                            && !frame.taken
487                            && eval_if_expr(expr, &state.macros);
488                        frame.active = cond;
489                        if cond {
490                            frame.taken = true;
491                        }
492                    }
493                    _ if !active => {
494                        // In an inactive block — do not execute other
495                        // directives.
496                    }
497                    Directive::Define(name, def) => {
498                        state.macros.insert(name.to_string(), def);
499                    }
500                    Directive::Undef(name) => {
501                        state.macros.remove(name);
502                    }
503                    Directive::Include(inc) => {
504                        if depth > MAX_INCLUDE_DEPTH {
505                            return Err(PreprocessError::SyntaxError {
506                                file: file_name.to_string(),
507                                line: line_no,
508                                message: format!("include depth exceeded {MAX_INCLUDE_DEPTH}"),
509                            });
510                        }
511                        let inc_path = inc.path().to_string();
512                        // Cycle detection before resolve, so that cycles
513                        // are detected independently of the resolver.
514                        if state.include_stack.iter().any(|f| f == &inc_path) {
515                            return Err(PreprocessError::IncludeCycle { file: inc_path });
516                        }
517                        let included = self
518                            .resolver
519                            .resolve(file_name, &inc)
520                            .map_err(PreprocessError::IncludeNotFound)?;
521                        let inc_id = state.source_map.add_file(&inc_path);
522                        self.expand_into(&inc_path, &included, inc_id, state, output, depth + 1)?;
523                    }
524                    Directive::Pragma(args) => {
525                        if let Some(keylist) = parse_pragma_keylist(args, file_name, line_no) {
526                            state.pragma_keylists.push(keylist);
527                        } else if let Some(osp) = parse_opensplice_pragma(args, file_name, line_no)
528                        {
529                            state.opensplice_pragmas.push(osp);
530                        } else if let Some(pp) = parse_pragma_prefix(args, file_name, line_no) {
531                            state.pragma_prefixes.push(pp);
532                        } else if let Some(xt) = parse_pragma_dds_xtopics(args, file_name, line_no)
533                        {
534                            state.pragma_dds_xtopics.push(xt);
535                        }
536                        // Other pragmas: stripped.
537                    }
538                    Directive::Error(msg) => {
539                        return Err(PreprocessError::ErrorDirective {
540                            file: file_name.to_string(),
541                            line: line_no,
542                            message: msg.trim().to_string(),
543                        });
544                    }
545                    Directive::Warning(_msg) => {
546                        // `#warning` is a diagnostic without abort.
547                        // stripped; the UI layer can render warnings via a hook
548                        // (comes with the diagnostic reporter).
549                    }
550                    Directive::Line(_args) => {
551                        // `#line N "file"` — position override.
552                        // SourceMap integration is still pending.
553                    }
554                }
555            } else if active {
556                // Normal source line: expand macros and append to the
557                // output.
558                let expanded = expand_macros(line, &state.macros);
559                state
560                    .source_map
561                    .record_segment(output.len(), expanded.len(), file_id, byte_offset);
562                output.push_str(&expanded);
563            }
564
565            byte_offset += line.len();
566        }
567
568        if let Some(frame) = conditional_stack.first() {
569            let _ = frame;
570            return Err(PreprocessError::UnclosedConditional {
571                file: file_name.to_string(),
572                line: 0,
573            });
574        }
575
576        state.include_stack.pop();
577        Ok(())
578    }
579}
580
581const MAX_INCLUDE_DEPTH: usize = 64;
582
583struct State {
584    macros: HashMap<String, MacroDef>,
585    include_stack: Vec<String>,
586    source_map: SourceMap,
587    pragma_keylists: Vec<PragmaKeylist>,
588    opensplice_pragmas: Vec<OpenSplicePragma>,
589    pragma_prefixes: Vec<PragmaPrefix>,
590    pragma_dds_xtopics: Vec<PragmaDdsXtopics>,
591}
592
593impl State {
594    fn new() -> Self {
595        Self {
596            macros: HashMap::new(),
597            include_stack: Vec::new(),
598            source_map: SourceMap::new(),
599            pragma_keylists: Vec::new(),
600            opensplice_pragmas: Vec::new(),
601            pragma_prefixes: Vec::new(),
602            pragma_dds_xtopics: Vec::new(),
603        }
604    }
605}
606
607/// Definition of a `#define` macro — either object-like or
608/// function-like (with a parameter list).
609#[derive(Clone, Debug, PartialEq, Eq)]
610struct MacroDef {
611    /// `Some(params)` for function-like macros (`#define NAME(p1, p2) body`),
612    /// `None` for object-like (`#define NAME body`).
613    params: Option<Vec<String>>,
614    /// Macro-Body (unexpandiert).
615    body: String,
616}
617
618impl MacroDef {
619    fn object_like(body: &str) -> Self {
620        Self {
621            params: None,
622            body: body.to_string(),
623        }
624    }
625
626    fn function_like(params: Vec<String>, body: &str) -> Self {
627        Self {
628            params: Some(params),
629            body: body.to_string(),
630        }
631    }
632}
633
634/// Splice backslash-newline pairs (token splicing) per §7.3 / ISO 14882.
635fn splice_backslash_newlines(src: &str) -> String {
636    // We work byte-oriented; UTF-8 compatible because `\` and `\n` are ASCII.
637    let bytes = src.as_bytes();
638    let mut out = Vec::with_capacity(bytes.len());
639    let mut i = 0;
640    while i < bytes.len() {
641        if bytes[i] == b'\\' && i + 1 < bytes.len() && bytes[i + 1] == b'\n' {
642            // Skip both bytes.
643            i += 2;
644            continue;
645        }
646        if bytes[i] == b'\\'
647            && i + 2 < bytes.len()
648            && bytes[i + 1] == b'\r'
649            && bytes[i + 2] == b'\n'
650        {
651            i += 3;
652            continue;
653        }
654        out.push(bytes[i]);
655        i += 1;
656    }
657    // Safe: only ASCII bytes were removed → stays valid UTF-8.
658    String::from_utf8(out).unwrap_or_default()
659}
660
661struct ConditionalFrame {
662    /// `true` if the current branch is active (tokens are emitted).
663    active: bool,
664    /// `true` if `#else` has already been seen (a second `#else` is an error).
665    else_seen: bool,
666    /// Was the parent frame active? If not, this branch is inactive
667    /// anyway — important for correct #else toggling in nested cases.
668    parent_active: bool,
669    /// `true` as soon as any branch (`#if`/`#elif`) was chosen as active.
670    /// Subsequent `#elif`/`#else` are ignored. (#if/#elif eval)
671    taken: bool,
672}
673
674/// Erkennbare Preprocessor-Direktiven.
675#[derive(Debug, PartialEq, Eq)]
676enum Directive<'a> {
677    Include(Include),
678    Define(&'a str, MacroDef),
679    Undef(&'a str),
680    Ifdef(&'a str),
681    Ifndef(&'a str),
682    /// `#if <const-expr>` — simplified expression eval:
683    /// `defined(MACRO)`, `0`/`1`, as well as `&&`/`||`/`!`.
684    /// (spec stage 2; gated via the `preprocessor_full` feature.)
685    If(&'a str),
686    /// `#elif <const-expr>` — variant of `#if`.
687    Elif(&'a str),
688    Else,
689    Endif,
690    Pragma(&'a str),
691    /// `#error <message>` — aborts the build.
692    Error(&'a str),
693    /// `#warning <message>` — diagnostic without abort
694    /// (gated via the `preprocessor_warning_line` feature).
695    Warning(&'a str),
696    /// `#line <linenum> ["filename"]` — source position override
697    /// (gated via the `preprocessor_warning_line` feature; /// stripped, SourceMap update follows when needed).
698    Line(&'a str),
699}
700
701fn parse_directive(line: &str) -> Option<Directive<'_>> {
702    let stripped = line.strip_prefix('#')?.trim_start();
703    let (head, rest) = match stripped.find(|c: char| c.is_whitespace()) {
704        Some(idx) => (&stripped[..idx], stripped[idx..].trim()),
705        None => (stripped.trim_end(), ""),
706    };
707    match head {
708        "include" => parse_include(rest).map(Directive::Include),
709        "define" => parse_define(rest),
710        "undef" => Some(Directive::Undef(rest)),
711        "ifdef" => Some(Directive::Ifdef(rest)),
712        "ifndef" => Some(Directive::Ifndef(rest)),
713        "if" => Some(Directive::If(rest)),
714        "elif" => Some(Directive::Elif(rest)),
715        "else" => Some(Directive::Else),
716        "endif" => Some(Directive::Endif),
717        "pragma" => Some(Directive::Pragma(rest)),
718        "error" => Some(Directive::Error(rest)),
719        "warning" => Some(Directive::Warning(rest)),
720        "line" => Some(Directive::Line(rest)),
721        _ => None,
722    }
723}
724
725/// Simplified `#if`-expression evaluation (Spec §7.3.2 + ISO 14882
726/// constant-expression subset).
727///
728/// Supports:
729/// - Numeric literals: `0` (false), everything else (true).
730/// - `defined(MACRO)` and `defined MACRO` — true if the macro is defined.
731/// - Boolean operators `&&`/`||`/`!` (left-to-right, no precedence).
732/// - Macro identifiers are interpreted as undefined (false),
733///   unless they are explicitly wrapped as `defined(...)`.
734///
735/// Full C-preprocessor expression eval (arithmetic, comparisons,
736/// bitops, ternary) is not implemented.
737fn eval_if_expr(expr: &str, macros: &HashMap<String, MacroDef>) -> bool {
738    let trimmed = expr.trim();
739    if trimmed.is_empty() {
740        return false;
741    }
742    // Tokenize einfach.
743    let normalized = normalize_if_tokens(trimmed);
744    eval_if_tokens(&normalized, macros)
745}
746
747fn normalize_if_tokens(expr: &str) -> Vec<String> {
748    let mut out = Vec::new();
749    let mut chars = expr.chars().peekable();
750    while let Some(c) = chars.next() {
751        match c {
752            ' ' | '\t' => {}
753            '(' | ')' | '!' => out.push(c.to_string()),
754            '&' if chars.peek() == Some(&'&') => {
755                chars.next();
756                out.push("&&".into());
757            }
758            '|' if chars.peek() == Some(&'|') => {
759                chars.next();
760                out.push("||".into());
761            }
762            c if c.is_ascii_alphabetic() || c == '_' => {
763                let mut buf = String::from(c);
764                while let Some(&n) = chars.peek() {
765                    if n.is_ascii_alphanumeric() || n == '_' {
766                        buf.push(n);
767                        chars.next();
768                    } else {
769                        break;
770                    }
771                }
772                out.push(buf);
773            }
774            c if c.is_ascii_digit() => {
775                let mut buf = String::from(c);
776                while let Some(&n) = chars.peek() {
777                    if n.is_ascii_digit() {
778                        buf.push(n);
779                        chars.next();
780                    } else {
781                        break;
782                    }
783                }
784                out.push(buf);
785            }
786            _ => {} // ignore unknown characters (defensive)
787        }
788    }
789    out
790}
791
792/// zerodds-lint: recursion-depth 64 (If-Expr; bounded by IDL nesting)
793fn eval_if_tokens(tokens: &[String], macros: &HashMap<String, MacroDef>) -> bool {
794    let (val, _) = eval_or(tokens, 0, macros);
795    val
796}
797
798fn eval_or(tokens: &[String], idx: usize, macros: &HashMap<String, MacroDef>) -> (bool, usize) {
799    let (mut left, mut i) = eval_and(tokens, idx, macros);
800    while tokens.get(i).map(String::as_str) == Some("||") {
801        let (right, ni) = eval_and(tokens, i + 1, macros);
802        left = left || right;
803        i = ni;
804    }
805    (left, i)
806}
807
808fn eval_and(tokens: &[String], idx: usize, macros: &HashMap<String, MacroDef>) -> (bool, usize) {
809    let (mut left, mut i) = eval_not(tokens, idx, macros);
810    while tokens.get(i).map(String::as_str) == Some("&&") {
811        let (right, ni) = eval_not(tokens, i + 1, macros);
812        left = left && right;
813        i = ni;
814    }
815    (left, i)
816}
817
818/// zerodds-lint: recursion-depth 16 (logical-not chain; bounded by IDL macro nesting)
819fn eval_not(tokens: &[String], idx: usize, macros: &HashMap<String, MacroDef>) -> (bool, usize) {
820    if tokens.get(idx).map(String::as_str) == Some("!") {
821        let (v, ni) = eval_not(tokens, idx + 1, macros);
822        return (!v, ni);
823    }
824    eval_atom(tokens, idx, macros)
825}
826
827fn eval_atom(tokens: &[String], idx: usize, macros: &HashMap<String, MacroDef>) -> (bool, usize) {
828    let Some(tok) = tokens.get(idx) else {
829        return (false, idx);
830    };
831    if tok == "(" {
832        let (v, ni) = eval_or(tokens, idx + 1, macros);
833        let after = if tokens.get(ni).map(String::as_str) == Some(")") {
834            ni + 1
835        } else {
836            ni
837        };
838        return (v, after);
839    }
840    if tok == "defined" {
841        // `defined(MACRO)` or `defined MACRO`.
842        let (next_idx, ident) = if tokens.get(idx + 1).map(String::as_str) == Some("(") {
843            (
844                idx + 3,
845                tokens.get(idx + 2).map(String::as_str).unwrap_or(""),
846            )
847        } else {
848            (
849                idx + 2,
850                tokens.get(idx + 1).map(String::as_str).unwrap_or(""),
851            )
852        };
853        let v = macros.contains_key(ident);
854        let after = if tokens.get(idx + 1).map(String::as_str) == Some("(") {
855            // Skip closing ')'.
856            if tokens.get(next_idx).map(String::as_str) == Some(")") {
857                next_idx + 1
858            } else {
859                next_idx
860            }
861        } else {
862            next_idx
863        };
864        return (v, after);
865    }
866    // Numeric literal: `0` = false, otherwise true.
867    if let Ok(n) = tok.parse::<i64>() {
868        return (n != 0, idx + 1);
869    }
870    // Identifier without `defined()` — treat as a macro-value lookup;
871    // if the macro body parses as an int → accordingly; otherwise true (macro
872    // exists). Function-like macros are treated as true here
873    // (call parameters in the `#if` context are unusual).
874    if let Some(def) = macros.get(tok) {
875        if let Ok(n) = def.body.trim().parse::<i64>() {
876            return (n != 0, idx + 1);
877        }
878        return (true, idx + 1);
879    }
880    // Unknown identifier → false (spec C-PP convention).
881    (false, idx + 1)
882}
883
884fn parse_include(rest: &str) -> Option<Include> {
885    let rest = rest.trim();
886    if let Some(stripped) = rest.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
887        return Some(Include::Quoted(stripped.to_string()));
888    }
889    rest.strip_prefix('<')
890        .and_then(|s| s.strip_suffix('>'))
891        .map(|stripped| Include::System(stripped.to_string()))
892}
893
894fn parse_define(rest: &str) -> Option<Directive<'_>> {
895    let rest = rest.trim_end_matches('\n').trim();
896    if rest.is_empty() {
897        return None;
898    }
899    // Identifier part up to the first whitespace OR `(`. A directly
900    // following `(` without whitespace marks a function-like
901    // macro (spec §7.2.5 + ISO 14882 §16.3).
902    let name_end = rest
903        .find(|c: char| c.is_whitespace() || c == '(')
904        .unwrap_or(rest.len());
905    let name = &rest[..name_end];
906    if name.is_empty() {
907        return None;
908    }
909    let after_name = &rest[name_end..];
910    if let Some(after_paren) = after_name.strip_prefix('(') {
911        // function-like: `NAME(p1, p2, ...) body`
912        let close = after_paren.find(')')?;
913        let params_src = &after_paren[..close];
914        let body = after_paren[close + 1..].trim();
915        let params: Vec<String> = if params_src.trim().is_empty() {
916            Vec::new()
917        } else {
918            params_src
919                .split(',')
920                .map(|p| p.trim().to_string())
921                .collect()
922        };
923        return Some(Directive::Define(
924            name,
925            MacroDef::function_like(params, body),
926        ));
927    }
928    let body = after_name.trim();
929    Some(Directive::Define(name, MacroDef::object_like(body)))
930}
931
932/// `#pragma prefix "<prefix>"` — CORBA Part 1 §14.7.5.
933///
934/// Returns `None` if the pragma is not a prefix pragma or the
935/// string argument is missing/empty.
936fn parse_pragma_prefix(args: &str, file: &str, line: usize) -> Option<PragmaPrefix> {
937    let trimmed = args.trim();
938    let rest = trimmed.strip_prefix("prefix")?.trim_start();
939    let prefix = strip_optional_quotes(rest).trim().to_string();
940    if prefix.is_empty() {
941        return None;
942    }
943    Some(PragmaPrefix {
944        prefix,
945        file: file.to_string(),
946        line,
947    })
948}
949
950/// `#pragma dds_xtopics version="1.3"` — XTypes 1.3 §7.3.1.1.1.
951///
952/// Returns `None` if the pragma is not a dds_xtopics pragma.
953fn parse_pragma_dds_xtopics(args: &str, file: &str, line: usize) -> Option<PragmaDdsXtopics> {
954    let trimmed = args.trim();
955    let rest = trimmed.strip_prefix("dds_xtopics")?.trim_start();
956    // Accepts both `version="1.3"` and `version=1.3`.
957    let version = if rest.is_empty() {
958        String::new()
959    } else if let Some(v) = rest.strip_prefix("version") {
960        v.trim_start()
961            .strip_prefix('=')
962            .unwrap_or(v)
963            .trim()
964            .trim_matches('"')
965            .to_string()
966    } else {
967        rest.trim_matches('"').to_string()
968    };
969    Some(PragmaDdsXtopics {
970        version,
971        file: file.to_string(),
972        line,
973    })
974}
975
976/// `#pragma keylist <Type> <field>*` — Cyclone DDS convention.
977///
978/// Returns `None` if the pragma is not a keylist pragma.
979fn parse_pragma_keylist(args: &str, file: &str, line: usize) -> Option<PragmaKeylist> {
980    let trimmed = args.trim();
981    let rest = trimmed.strip_prefix("keylist")?.trim_start();
982    let mut parts = rest.split_whitespace();
983    let type_name = parts.next()?.to_string();
984    let keys: Vec<String> = parts.map(str::to_string).collect();
985    Some(PragmaKeylist {
986        type_name,
987        keys,
988        file: file.to_string(),
989        line,
990    })
991}
992
993/// Parses OpenSplice legacy pragmas (`DCPS_DATA_TYPE`, `DCPS_DATA_KEY`,
994/// `cats`, `genequality`).
995fn parse_opensplice_pragma(args: &str, file: &str, line: usize) -> Option<OpenSplicePragma> {
996    let trimmed = args.trim();
997    if let Some(rest) = trimmed.strip_prefix("DCPS_DATA_TYPE") {
998        let payload = rest.trim();
999        // Accepts both quoted ("Type") and unquoted (Type).
1000        let type_name = strip_optional_quotes(payload).to_string();
1001        if type_name.is_empty() {
1002            return None;
1003        }
1004        return Some(OpenSplicePragma::DataType {
1005            type_name,
1006            file: file.to_string(),
1007            line,
1008        });
1009    }
1010    if let Some(rest) = trimmed.strip_prefix("DCPS_DATA_KEY") {
1011        let payload = strip_optional_quotes(rest.trim());
1012        // Zwei akzeptierte Formen:
1013        //   "Type.field"            — Punkt-Trenner (Legacy)
1014        //   "Type field1 field2..."  — OpenDDS-Space-Form (1..n Felder)
1015        let (type_name, fields): (String, Vec<String>) = if let Some(dot) = payload.find('.') {
1016            let ty = payload[..dot].trim().to_string();
1017            let f = payload[dot + 1..].trim().to_string();
1018            (ty, vec![f])
1019        } else {
1020            let mut parts = payload.split_whitespace();
1021            let ty = parts.next()?.to_string();
1022            (ty, parts.map(str::to_string).collect())
1023        };
1024        if type_name.is_empty() || fields.iter().any(String::is_empty) || fields.is_empty() {
1025            return None;
1026        }
1027        return Some(OpenSplicePragma::DataKey {
1028            type_name,
1029            fields,
1030            file: file.to_string(),
1031            line,
1032        });
1033    }
1034    if let Some(rest) = trimmed.strip_prefix("cats") {
1035        let mut parts = rest.split_whitespace();
1036        let type_name = parts.next()?.to_string();
1037        let keys: Vec<String> = parts.map(str::to_string).collect();
1038        if keys.is_empty() {
1039            return None;
1040        }
1041        return Some(OpenSplicePragma::Cats {
1042            type_name,
1043            keys,
1044            file: file.to_string(),
1045            line,
1046        });
1047    }
1048    if trimmed == "genequality" {
1049        return Some(OpenSplicePragma::GenEquality {
1050            file: file.to_string(),
1051            line,
1052        });
1053    }
1054    None
1055}
1056
1057fn strip_optional_quotes(s: &str) -> &str {
1058    let s = s.trim();
1059    s.strip_prefix('"')
1060        .and_then(|t| t.strip_suffix('"'))
1061        .unwrap_or(s)
1062}
1063
1064/// Object-like macro substitution. Iterates over identifier tokens and
1065/// replaces macro names with their values. Simplified variant — no
1066/// re-expansion (no macro-in-macro), no function-like macros.
1067fn expand_macros(line: &str, macros: &HashMap<String, MacroDef>) -> String {
1068    expand_macros_rec(line, macros, 0)
1069}
1070
1071/// Maximum depth for recursive `#define` expansion. Protects against
1072/// pathological `#define A B` / `#define B A` cycles and against
1073/// indirect cycles over 2+ hops. The value covers typical
1074/// IDL const paths (≤ 8 hops) with ample reserve.
1075const MAX_MACRO_EXPANSION_DEPTH: usize = 32;
1076
1077/// zerodds-lint: recursion-depth 32
1078fn expand_macros_rec(line: &str, macros: &HashMap<String, MacroDef>, depth: usize) -> String {
1079    if macros.is_empty() || depth >= MAX_MACRO_EXPANSION_DEPTH {
1080        return line.to_string();
1081    }
1082    let mut out = String::with_capacity(line.len());
1083    let bytes = line.as_bytes();
1084    let mut i = 0;
1085    let mut expanded_any = false;
1086    while i < bytes.len() {
1087        let c = bytes[i];
1088        if c.is_ascii_alphabetic() || c == b'_' {
1089            // Identifier scannen.
1090            let start = i;
1091            while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') {
1092                i += 1;
1093            }
1094            let ident = &line[start..i];
1095            let Some(def) = macros.get(ident) else {
1096                out.push_str(ident);
1097                continue;
1098            };
1099            expanded_any = true;
1100            match &def.params {
1101                None => out.push_str(&def.body),
1102                Some(params) => {
1103                    // function-like: `(` expected directly (or after whitespace),
1104                    // otherwise pass the identifier through.
1105                    let after = skip_ascii_ws(bytes, i);
1106                    if after >= bytes.len() || bytes[after] != b'(' {
1107                        out.push_str(ident);
1108                        continue;
1109                    }
1110                    let Some((args, end)) = parse_call_args(line, after) else {
1111                        out.push_str(ident);
1112                        continue;
1113                    };
1114                    let expanded = expand_function_like(params, &args, &def.body);
1115                    out.push_str(&expanded);
1116                    i = end;
1117                }
1118            }
1119        } else {
1120            out.push(c as char);
1121            i += 1;
1122        }
1123    }
1124    // If something was expanded, the expansion itself may contain further
1125    // macro refs — re-expand recursively until the fixed point
1126    // is reached. `MAX_MACRO_EXPANSION_DEPTH` protects against
1127    // self-recursive `#define A A` pathologies.
1128    if expanded_any && out != line {
1129        return expand_macros_rec(&out, macros, depth + 1);
1130    }
1131    out
1132}
1133
1134fn skip_ascii_ws(bytes: &[u8], mut i: usize) -> usize {
1135    while i < bytes.len() && matches!(bytes[i], b' ' | b'\t') {
1136        i += 1;
1137    }
1138    i
1139}
1140
1141/// Parses `( arg1 , arg2 , ... )` from position `start` (points at `(`).
1142/// Returns the arguments and the index directly after `)`. Commas inside
1143/// parenthesis pairs are ignored (nested calls).
1144fn parse_call_args(line: &str, start: usize) -> Option<(Vec<String>, usize)> {
1145    let bytes = line.as_bytes();
1146    debug_assert_eq!(bytes.get(start), Some(&b'('));
1147    let mut i = start + 1;
1148    let mut depth: usize = 1;
1149    let mut args: Vec<String> = Vec::new();
1150    let mut cur = String::new();
1151    while i < bytes.len() {
1152        let c = bytes[i] as char;
1153        match c {
1154            '(' => {
1155                depth += 1;
1156                cur.push(c);
1157                i += 1;
1158            }
1159            ')' => {
1160                depth -= 1;
1161                if depth == 0 {
1162                    args.push(cur.trim().to_string());
1163                    return Some((args, i + 1));
1164                }
1165                cur.push(c);
1166                i += 1;
1167            }
1168            ',' if depth == 1 => {
1169                args.push(cur.trim().to_string());
1170                cur.clear();
1171                i += 1;
1172            }
1173            _ => {
1174                cur.push(c);
1175                i += 1;
1176            }
1177        }
1178    }
1179    None
1180}
1181
1182/// Substituiert Parameter im function-like-Macro-Body. Erkennt
1183/// `#param` (stringize, ISO 14882 §16.3.2) and `a##b` (token paste,
1184/// §16.3.3).
1185fn expand_function_like(params: &[String], args: &[String], body: &str) -> String {
1186    let arg_for = |name: &str| -> Option<&str> {
1187        params
1188            .iter()
1189            .position(|p| p == name)
1190            .and_then(|idx| args.get(idx).map(String::as_str))
1191    };
1192    // Tokenisiere Body grob: Identifier vs. Rest.
1193    let mut tokens: Vec<BodyTok> = Vec::new();
1194    let bytes = body.as_bytes();
1195    let mut i = 0;
1196    while i < bytes.len() {
1197        let c = bytes[i];
1198        if c.is_ascii_alphabetic() || c == b'_' {
1199            let start = i;
1200            while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') {
1201                i += 1;
1202            }
1203            tokens.push(BodyTok::Ident(body[start..i].to_string()));
1204        } else if c == b'#' && i + 1 < bytes.len() && bytes[i + 1] == b'#' {
1205            tokens.push(BodyTok::Paste);
1206            i += 2;
1207        } else if c == b'#' {
1208            tokens.push(BodyTok::Stringize);
1209            i += 1;
1210        } else {
1211            tokens.push(BodyTok::Other((c as char).to_string()));
1212            i += 1;
1213        }
1214    }
1215    // Token-paste pass: binds `<a> ## <b>` into `<a><b>`. Whitespace
1216    // directly around `##` is discarded. On a match the preceding
1217    // entry is popped from `after_paste` and concatenated with the right operand
1218    // (param-substituted).
1219    let mut after_paste: Vec<BodyTok> = Vec::with_capacity(tokens.len());
1220    let mut k = 0;
1221    while k < tokens.len() {
1222        if matches!(tokens[k], BodyTok::Paste) {
1223            // Skip whitespace tokens left/right of the `##`:
1224            // whitespace on the left is already in `after_paste` — so we
1225            // search for the last non-whitespace entry.
1226            let mut lhs: Option<BodyTok> = None;
1227            while let Some(last) = after_paste.last() {
1228                if let BodyTok::Other(s) = last {
1229                    if s.chars().all(char::is_whitespace) {
1230                        after_paste.pop();
1231                        continue;
1232                    }
1233                }
1234                lhs = after_paste.pop();
1235                break;
1236            }
1237            let rhs_idx = skip_body_ws(&tokens, k + 1);
1238            match (lhs, tokens.get(rhs_idx)) {
1239                (Some(lhs_tok), Some(rhs_tok)) => {
1240                    let lhs_text = render_tok(&lhs_tok, params, args);
1241                    let rhs_text = render_tok(rhs_tok, params, args);
1242                    after_paste.push(BodyTok::Ident(format!("{lhs_text}{rhs_text}")));
1243                    k = rhs_idx + 1;
1244                }
1245                _ => {
1246                    // Stand-alone `##` without operands — keep it as a literal.
1247                    after_paste.push(BodyTok::Paste);
1248                    k += 1;
1249                }
1250            }
1251        } else {
1252            after_paste.push(tokens[k].clone());
1253            k += 1;
1254        }
1255    }
1256    // Render pass: stringize and param substitution.
1257    let mut out = String::new();
1258    let mut j = 0;
1259    while j < after_paste.len() {
1260        match &after_paste[j] {
1261            BodyTok::Stringize => {
1262                let target_idx = skip_body_ws(&after_paste, j + 1);
1263                let arg_text = match after_paste.get(target_idx) {
1264                    Some(BodyTok::Ident(name)) => arg_for(name).unwrap_or(name).to_string(),
1265                    _ => String::new(),
1266                };
1267                out.push('"');
1268                for ch in arg_text.chars() {
1269                    if ch == '"' || ch == '\\' {
1270                        out.push('\\');
1271                    }
1272                    out.push(ch);
1273                }
1274                out.push('"');
1275                j = target_idx + 1;
1276            }
1277            BodyTok::Ident(name) => {
1278                if let Some(text) = arg_for(name) {
1279                    out.push_str(text);
1280                } else {
1281                    out.push_str(name);
1282                }
1283                j += 1;
1284            }
1285            BodyTok::Other(s) => {
1286                out.push_str(s);
1287                j += 1;
1288            }
1289            BodyTok::Paste => {
1290                // Stand-alone `##` without an LHS — emit as a literal
1291                // (should no longer occur after the paste pass).
1292                out.push_str("##");
1293                j += 1;
1294            }
1295        }
1296    }
1297    out
1298}
1299
1300#[derive(Clone, Debug)]
1301enum BodyTok {
1302    Ident(String),
1303    Other(String),
1304    Stringize,
1305    Paste,
1306}
1307
1308fn skip_body_ws(tokens: &[BodyTok], mut i: usize) -> usize {
1309    while let Some(BodyTok::Other(s)) = tokens.get(i) {
1310        if !s.chars().all(char::is_whitespace) {
1311            break;
1312        }
1313        i += 1;
1314    }
1315    i
1316}
1317
1318fn render_tok(tok: &BodyTok, params: &[String], args: &[String]) -> String {
1319    match tok {
1320        BodyTok::Ident(name) => params
1321            .iter()
1322            .position(|p| p == name)
1323            .and_then(|idx| args.get(idx).cloned())
1324            .unwrap_or_else(|| name.clone()),
1325        BodyTok::Other(s) => s.clone(),
1326        BodyTok::Stringize => "#".to_string(),
1327        BodyTok::Paste => "##".to_string(),
1328    }
1329}
1330
1331#[cfg(test)]
1332mod tests {
1333    #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
1334    use super::*;
1335
1336    fn run(src: &str) -> String {
1337        Preprocessor::new(MemoryResolver::new())
1338            .process("main.idl", src)
1339            .expect("ok")
1340            .expanded
1341    }
1342
1343    fn run_with(resolver: MemoryResolver, src: &str) -> String {
1344        Preprocessor::new(resolver)
1345            .process("main.idl", src)
1346            .expect("ok")
1347            .expanded
1348    }
1349
1350    #[test]
1351    fn passthrough_for_source_without_directives() {
1352        let out = run("struct Foo { long x; };\n");
1353        assert!(out.contains("struct Foo"));
1354    }
1355
1356    #[test]
1357    fn pragma_is_stripped() {
1358        let out = run("#pragma keylist Foo x\nstruct Foo { long x; };\n");
1359        assert!(!out.contains("#pragma"));
1360        assert!(out.contains("struct Foo"));
1361    }
1362
1363    #[test]
1364    fn define_object_like_substitutes_in_subsequent_lines() {
1365        let out = run("#define MAX 100\nconst long L = MAX;\n");
1366        assert!(out.contains("const long L = 100;"), "{out}");
1367        assert!(!out.contains("#define"));
1368    }
1369
1370    #[test]
1371    fn ifdef_keeps_block_when_macro_defined() {
1372        let out = run("#define WITH\n#ifdef WITH\nstruct A {};\n#endif\n");
1373        assert!(out.contains("struct A"), "{out}");
1374    }
1375
1376    #[test]
1377    fn ifdef_drops_block_when_macro_not_defined() {
1378        let out = run("#ifdef WITH\nstruct A {};\n#endif\n");
1379        assert!(!out.contains("struct A"), "{out}");
1380    }
1381
1382    #[test]
1383    fn ifndef_inverse_of_ifdef() {
1384        let out = run("#ifndef WITH\nstruct B {};\n#endif\n");
1385        assert!(out.contains("struct B"), "{out}");
1386    }
1387
1388    #[test]
1389    fn else_branch_taken_when_initial_false() {
1390        let out = run("#ifdef NOPE\nstruct A {};\n#else\nstruct B {};\n#endif\n");
1391        assert!(!out.contains("struct A"), "{out}");
1392        assert!(out.contains("struct B"), "{out}");
1393    }
1394
1395    #[test]
1396    fn nested_ifdef_works() {
1397        let out = run("#define X\n\
1398             #ifdef X\n\
1399                #ifdef Y\nstruct YY {};\n#else\nstruct XnotY {};\n#endif\n\
1400             #endif\n");
1401        assert!(out.contains("struct XnotY"), "{out}");
1402        assert!(!out.contains("struct YY"));
1403    }
1404
1405    #[test]
1406    fn undef_removes_macro() {
1407        let out = run("#define M\n#undef M\n#ifdef M\nA\n#endif\n");
1408        assert!(!out.contains('A'), "{out}");
1409    }
1410
1411    #[test]
1412    fn quoted_include_resolves() {
1413        let mut r = MemoryResolver::new();
1414        r.add("inc.idl", "struct Inc {};\n");
1415        let out = run_with(r, "#include \"inc.idl\"\nstruct Main {};\n");
1416        assert!(out.contains("struct Inc"), "{out}");
1417        assert!(out.contains("struct Main"), "{out}");
1418    }
1419
1420    #[test]
1421    fn system_include_resolves() {
1422        let mut r = MemoryResolver::new();
1423        r.add("sys.idl", "struct Sys {};\n");
1424        let out = run_with(r, "#include <sys.idl>\nstruct Main {};\n");
1425        assert!(out.contains("struct Sys"), "{out}");
1426    }
1427
1428    #[test]
1429    fn missing_include_is_error() {
1430        let res = Preprocessor::new(MemoryResolver::new())
1431            .process("main.idl", "#include \"missing.idl\"\n");
1432        assert!(matches!(res, Err(PreprocessError::IncludeNotFound(_))));
1433    }
1434
1435    #[test]
1436    fn include_cycle_is_detected() {
1437        let mut r = MemoryResolver::new();
1438        r.add("a.idl", "#include \"main.idl\"\n");
1439        let res = Preprocessor::new(r).process("main.idl", "#include \"a.idl\"\n");
1440        assert!(matches!(res, Err(PreprocessError::IncludeCycle { .. })));
1441    }
1442
1443    #[test]
1444    fn unmatched_endif_is_error() {
1445        let res = Preprocessor::new(MemoryResolver::new()).process("main.idl", "#endif\n");
1446        assert!(matches!(res, Err(PreprocessError::UnmatchedEndif { .. })));
1447    }
1448
1449    #[test]
1450    fn unclosed_conditional_is_error() {
1451        let res = Preprocessor::new(MemoryResolver::new()).process("main.idl", "#ifdef X\n");
1452        assert!(matches!(
1453            res,
1454            Err(PreprocessError::UnclosedConditional { .. })
1455        ));
1456    }
1457
1458    #[test]
1459    fn unmatched_else_is_error() {
1460        let res = Preprocessor::new(MemoryResolver::new()).process("main.idl", "#else\n");
1461        assert!(matches!(res, Err(PreprocessError::UnmatchedElse { .. })));
1462    }
1463
1464    #[test]
1465    fn macro_in_inactive_branch_does_not_take_effect() {
1466        let out = run("#ifdef NOPE\n#define M 99\n#endif\n#ifdef M\nseen\n#endif\n");
1467        assert!(!out.contains("seen"));
1468    }
1469
1470    #[test]
1471    fn source_map_records_segments() {
1472        let result = Preprocessor::new(MemoryResolver::new())
1473            .process("main.idl", "struct A {};\nstruct B {};\n")
1474            .expect("ok");
1475        // At least two segments for two lines.
1476        assert!(
1477            result.source_map.segment_count() >= 2,
1478            "got {} segments",
1479            result.source_map.segment_count()
1480        );
1481    }
1482
1483    #[test]
1484    fn expand_macros_skips_unknown_identifiers() {
1485        let macros = HashMap::new();
1486        let out = expand_macros("foo bar baz", &macros);
1487        assert_eq!(out, "foo bar baz");
1488    }
1489
1490    #[test]
1491    fn expand_macros_substitutes_only_full_idents() {
1492        let mut m = HashMap::new();
1493        m.insert("X".to_string(), MacroDef::object_like("100"));
1494        // `XY` contains `X` as a substring, but must not be replaced.
1495        let out = expand_macros("X XY", &m);
1496        assert_eq!(out, "100 XY");
1497    }
1498
1499    // -----------------------------------------------------------------
1500    // §7.3 stage 2 — #if/#elif/#warning/#line (B7)
1501    // -----------------------------------------------------------------
1502
1503    #[test]
1504    fn if_eval_defined_macro_keeps_block() {
1505        let src = "\
1506#define FOO 1
1507#if defined(FOO)
1508struct InFoo { long x; };
1509#endif
1510struct After { long y; };
1511";
1512        let out = run(src);
1513        assert!(out.contains("struct InFoo"), "got: {out}");
1514        assert!(out.contains("struct After"), "got: {out}");
1515    }
1516
1517    #[test]
1518    fn if_eval_undefined_macro_drops_block() {
1519        let src = "\
1520#if defined(FOO)
1521struct ShouldBeGone { long x; };
1522#endif
1523struct Visible { long y; };
1524";
1525        let out = run(src);
1526        assert!(!out.contains("ShouldBeGone"), "got: {out}");
1527        assert!(out.contains("struct Visible"));
1528    }
1529
1530    #[test]
1531    fn if_eval_numeric_zero_drops_block() {
1532        let src = "#if 0\nstruct X { long x; };\n#endif\nstruct Y {};\n";
1533        let out = run(src);
1534        assert!(!out.contains("struct X"), "got: {out}");
1535    }
1536
1537    #[test]
1538    fn if_eval_numeric_nonzero_keeps_block() {
1539        let src = "#if 1\nstruct X { long x; };\n#endif\n";
1540        let out = run(src);
1541        assert!(out.contains("struct X"), "got: {out}");
1542    }
1543
1544    #[test]
1545    fn if_eval_logical_or() {
1546        let src = "\
1547#define A 1
1548#if defined(A) || defined(B)
1549struct Match { long m; };
1550#endif
1551";
1552        let out = run(src);
1553        assert!(out.contains("struct Match"), "got: {out}");
1554    }
1555
1556    #[test]
1557    fn if_eval_logical_not() {
1558        let src = "#if !defined(NOT_DEFINED)\nstruct K {};\n#endif\n";
1559        let out = run(src);
1560        assert!(out.contains("struct K"), "got: {out}");
1561    }
1562
1563    #[test]
1564    fn if_eval_logical_and_both_defined_keeps_block() {
1565        // §7.2.5: `&&` in `#if`-Eval — beide Operanden true.
1566        let src = "\
1567#define A 1
1568#define B 1
1569#if defined(A) && defined(B)
1570struct Both {};
1571#endif
1572";
1573        let out = run(src);
1574        assert!(out.contains("struct Both"), "got: {out}");
1575    }
1576
1577    #[test]
1578    fn if_eval_logical_and_one_undefined_drops_block() {
1579        // §7.2.5: `&&` with one undefined operand → false.
1580        let src = "\
1581#define A 1
1582#if defined(A) && defined(NOT_DEFINED)
1583struct OnlyA {};
1584#endif
1585";
1586        let out = run(src);
1587        assert!(!out.contains("struct OnlyA"), "got: {out}");
1588    }
1589
1590    #[test]
1591    fn if_eval_logical_and_both_undefined_drops_block() {
1592        // §7.2.5: `&&` with both undefined → false.
1593        let src = "\
1594#if defined(NOT_A) && defined(NOT_B)
1595struct Neither {};
1596#endif
1597";
1598        let out = run(src);
1599        assert!(!out.contains("struct Neither"), "got: {out}");
1600    }
1601
1602    #[test]
1603    fn if_elif_else_branches() {
1604        let src = "\
1605#if defined(NOT_DEFINED)
1606struct One {};
1607#elif defined(MODE)
1608struct WithMode {};
1609#else
1610struct Default {};
1611#endif
1612";
1613        // MODE not defined → default branch.
1614        let out = run(src);
1615        assert!(out.contains("struct Default"), "got: {out}");
1616        assert!(!out.contains("struct One"));
1617        assert!(!out.contains("struct WithMode"));
1618    }
1619
1620    #[test]
1621    fn if_elif_picks_first_true_branch() {
1622        let src = "\
1623#define MODE 1
1624#if defined(NOT_DEFINED)
1625struct A {};
1626#elif defined(MODE)
1627struct B {};
1628#elif defined(ANOTHER)
1629struct C {};
1630#else
1631struct D {};
1632#endif
1633";
1634        let out = run(src);
1635        assert!(out.contains("struct B"), "got: {out}");
1636        assert!(!out.contains("struct A"));
1637        assert!(!out.contains("struct C"));
1638        assert!(!out.contains("struct D"));
1639    }
1640
1641    #[test]
1642    fn warning_directive_does_not_abort() {
1643        let src = "#warning this is a warning\nstruct OK {};\n";
1644        let out = run(src);
1645        assert!(out.contains("struct OK"), "got: {out}");
1646    }
1647
1648    #[test]
1649    fn line_directive_does_not_abort() {
1650        let src = "#line 42 \"original.idl\"\nstruct X {};\n";
1651        let out = run(src);
1652        assert!(out.contains("struct X"), "got: {out}");
1653    }
1654
1655    // -----------------------------------------------------------------
1656    // C1 — OpenSplice legacy pragmas (DCPS_DATA_TYPE/DATA_KEY/cats/
1657    // genequality). Migration use case for reference customers.
1658    // -----------------------------------------------------------------
1659
1660    #[test]
1661    fn opensplice_pragma_data_type_quoted() {
1662        let src = r#"#pragma DCPS_DATA_TYPE "Sensor"
1663struct Sensor { long id; };
1664"#;
1665        let res = Preprocessor::new(MemoryResolver::new())
1666            .process("main.idl", src)
1667            .expect("ok");
1668        assert_eq!(res.opensplice_pragmas.len(), 1);
1669        match &res.opensplice_pragmas[0] {
1670            OpenSplicePragma::DataType { type_name, .. } => {
1671                assert_eq!(type_name, "Sensor");
1672            }
1673            other => panic!("expected DataType, got {other:?}"),
1674        }
1675    }
1676
1677    #[test]
1678    fn opensplice_pragma_data_type_unquoted() {
1679        let src = "#pragma DCPS_DATA_TYPE Sensor\nstruct Sensor {};\n";
1680        let res = Preprocessor::new(MemoryResolver::new())
1681            .process("main.idl", src)
1682            .expect("ok");
1683        match &res.opensplice_pragmas[0] {
1684            OpenSplicePragma::DataType { type_name, .. } => {
1685                assert_eq!(type_name, "Sensor");
1686            }
1687            other => panic!("expected DataType, got {other:?}"),
1688        }
1689    }
1690
1691    #[test]
1692    fn opensplice_pragma_data_key() {
1693        let src = r#"#pragma DCPS_DATA_KEY "Sensor.id"
1694struct Sensor { long id; };
1695"#;
1696        let res = Preprocessor::new(MemoryResolver::new())
1697            .process("main.idl", src)
1698            .expect("ok");
1699        match &res.opensplice_pragmas[0] {
1700            OpenSplicePragma::DataKey {
1701                type_name, fields, ..
1702            } => {
1703                assert_eq!(type_name, "Sensor");
1704                assert_eq!(fields, &["id"]);
1705            }
1706            other => panic!("expected DataKey, got {other:?}"),
1707        }
1708    }
1709
1710    #[test]
1711    fn opensplice_pragma_data_key_space_form() {
1712        // OpenDDS space form with multiple fields.
1713        let src = "#pragma DCPS_DATA_KEY \"Sensor id region\"\n\
1714                   struct Sensor { long id; long region; };\n";
1715        let res = Preprocessor::new(MemoryResolver::new())
1716            .process("main.idl", src)
1717            .expect("ok");
1718        match &res.opensplice_pragmas[0] {
1719            OpenSplicePragma::DataKey {
1720                type_name, fields, ..
1721            } => {
1722                assert_eq!(type_name, "Sensor");
1723                assert_eq!(fields, &["id", "region"]);
1724            }
1725            other => panic!("expected DataKey, got {other:?}"),
1726        }
1727    }
1728
1729    #[test]
1730    fn opensplice_pragma_cats() {
1731        let src = "#pragma cats Sensor id sub_id\nstruct Sensor {};\n";
1732        let res = Preprocessor::new(MemoryResolver::new())
1733            .process("main.idl", src)
1734            .expect("ok");
1735        match &res.opensplice_pragmas[0] {
1736            OpenSplicePragma::Cats {
1737                type_name, keys, ..
1738            } => {
1739                assert_eq!(type_name, "Sensor");
1740                assert_eq!(keys, &vec!["id".to_string(), "sub_id".to_string()]);
1741            }
1742            other => panic!("expected Cats, got {other:?}"),
1743        }
1744    }
1745
1746    #[test]
1747    fn opensplice_pragma_genequality() {
1748        let src = "#pragma genequality\nstruct S {};\n";
1749        let res = Preprocessor::new(MemoryResolver::new())
1750            .process("main.idl", src)
1751            .expect("ok");
1752        assert!(matches!(
1753            res.opensplice_pragmas.first(),
1754            Some(OpenSplicePragma::GenEquality { .. })
1755        ));
1756    }
1757
1758    #[test]
1759    fn opensplice_legacy_full_topic_decl() {
1760        // Realistic OpenSplice legacy pattern: topic + key via
1761        // DCPS pragmas plus genequality for a codegen hint.
1762        let src = r#"#pragma DCPS_DATA_TYPE "Sensor"
1763#pragma DCPS_DATA_KEY "Sensor.id"
1764#pragma genequality
1765struct Sensor {
1766    long id;
1767    double value;
1768};
1769"#;
1770        let res = Preprocessor::new(MemoryResolver::new())
1771            .process("main.idl", src)
1772            .expect("ok");
1773        assert_eq!(res.opensplice_pragmas.len(), 3);
1774        assert!(res.expanded.contains("struct Sensor"));
1775    }
1776
1777    #[test]
1778    fn nested_if_in_active_branch() {
1779        let src = "\
1780#define OUTER 1
1781#if defined(OUTER)
1782#if defined(INNER)
1783struct ShouldBeGone {};
1784#else
1785struct InnerElse {};
1786#endif
1787#endif
1788";
1789        let out = run(src);
1790        assert!(out.contains("struct InnerElse"), "got: {out}");
1791        assert!(!out.contains("ShouldBeGone"), "got: {out}");
1792    }
1793
1794    // -----------------------------------------------------------------
1795    // §7.3 — whitespace before `#` (Phase 1.7)
1796    // -----------------------------------------------------------------
1797
1798    #[test]
1799    fn leading_whitespace_before_hash_accepted() {
1800        // Spec §7.3: "White space may appear before the #."
1801        let out = run("    #define X 1\nconst long Y = X;\n");
1802        assert!(out.contains("const long Y = 1;"), "got: {out}");
1803    }
1804
1805    // -----------------------------------------------------------------
1806    // §7.3 — Backslash-Newline-Continuation (Phase 1.8)
1807    // -----------------------------------------------------------------
1808
1809    #[test]
1810    fn line_continuation_in_define() {
1811        // Spec §7.3: backslash-newline is removed by splicing.
1812        let out = run("#define LONG_MACRO foo \\\nbar\nLONG_MACRO\n");
1813        // The macro body is `foo bar`; after substitution that appears in
1814        // the output.
1815        assert!(out.contains("foo bar"), "got: {out}");
1816    }
1817
1818    #[test]
1819    fn line_continuation_in_idl_line() {
1820        // Backslash-newline removed outside directives too.
1821        let out = run("const long\\\nX = 1;\n");
1822        // After splicing: `const longX = 1;`. Not semantically meaningful,
1823        // but the main point: no two lines anymore — no `\n` between
1824        // `long` and `X`.
1825        assert!(!out.contains("long\nX"), "got: {out}");
1826    }
1827
1828    #[test]
1829    fn line_continuation_with_crlf() {
1830        // Windows style: `\\\r\n` must also be recognized as a continuation.
1831        let out = run("#define M foo \\\r\nbar\nM\n");
1832        assert!(out.contains("foo bar"), "got: {out}");
1833    }
1834
1835    #[test]
1836    fn multi_line_continuation() {
1837        // Three lines joined with continuations.
1838        let out = run("#define M a \\\nb \\\nc\nM\n");
1839        assert!(out.contains("a b c"), "got: {out}");
1840    }
1841
1842    // -----------------------------------------------------------------
1843    // §7.3 — Backslash am File-Ende (Phase 1.9)
1844    // -----------------------------------------------------------------
1845
1846    #[test]
1847    fn trailing_backslash_at_file_end_is_error() {
1848        // Spec §7.3: "A backslash character may not be the last
1849        // character in a source file."
1850        let result = Preprocessor::new(MemoryResolver::new()).process("main.idl", "foo\\");
1851        assert!(
1852            matches!(result, Err(PreprocessError::TrailingBackslash { .. })),
1853            "got: {result:?}"
1854        );
1855    }
1856
1857    // -----------------------------------------------------------------
1858    // §7.2.5 — `#` Stringize + `##` Token-Paste in function-like Macros
1859    // -----------------------------------------------------------------
1860
1861    #[test]
1862    fn function_like_macro_substitutes_args() {
1863        // Precondition for stringize/token-paste: function-like
1864        // macros are expanded at all.
1865        let src = "#define ADD(a, b) a + b\nconst long L = ADD(1, 2);\n";
1866        let out = run(src);
1867        assert!(out.contains("1 + 2"), "got: {out}");
1868    }
1869
1870    #[test]
1871    fn stringize_param_in_function_macro() {
1872        // Spec §7.2.5 + ISO 14882 §16.3.2: `#param` im function-like-
1873        // The macro body turns the argument into a string literal.
1874        let src = "#define STR(x) #x\nconst string S = STR(hello);\n";
1875        let out = run(src);
1876        assert!(out.contains("\"hello\""), "got: {out}");
1877    }
1878
1879    #[test]
1880    fn stringize_escapes_quotes_and_backslashes() {
1881        // ISO 14882 §16.3.2: `\` and `"` in the argument are escaped in the
1882        // resulting string literal.
1883        let src = "#define STR(x) #x\nconst string S = STR(a\"b\\c);\n";
1884        let out = run(src);
1885        assert!(out.contains("\"a\\\"b\\\\c\""), "got: {out}");
1886    }
1887
1888    #[test]
1889    fn token_paste_concatenates_idents() {
1890        // Spec §7.2.5 + ISO 14882 §16.3.3: `a##b` konkateniert zu `ab`.
1891        let src = "#define CAT(a, b) a##b\nconst long CAT(foo, bar) = 0;\n";
1892        let out = run(src);
1893        assert!(out.contains("foobar"), "got: {out}");
1894    }
1895
1896    #[test]
1897    fn token_paste_with_macro_args_produces_single_ident() {
1898        // Token-paste must erase whitespace between the operands.
1899        let src = "#define CAT(a, b) a ## b\nconst long CAT(x, y) = 0;\n";
1900        let out = run(src);
1901        assert!(out.contains("xy"), "got: {out}");
1902    }
1903
1904    // ---- §7.3.1.1.1 dds_xtopics-Pragma ----
1905
1906    fn process(src: &str) -> ProcessedSource {
1907        Preprocessor::new(MemoryResolver::new())
1908            .process("main.idl", src)
1909            .expect("ok")
1910    }
1911
1912    #[test]
1913    fn pragma_dds_xtopics_version_match() {
1914        let out = process("#pragma dds_xtopics version=\"1.3\"\nstruct S { long x; };\n");
1915        assert_eq!(out.pragma_dds_xtopics.len(), 1);
1916        assert_eq!(out.pragma_dds_xtopics[0].version, "1.3");
1917    }
1918
1919    #[test]
1920    fn pragma_dds_xtopics_version_mismatch_warns() {
1921        // Two dds_xtopics pragmas with different versions — both are
1922        // collected; the spec validator (separate pass) detects
1923        // mismatches.
1924        let out = process(
1925            "#pragma dds_xtopics version=\"1.0\"\n\
1926             #pragma dds_xtopics version=\"1.3\"\n\
1927             struct S { long x; };\n",
1928        );
1929        assert_eq!(out.pragma_dds_xtopics.len(), 2);
1930        let versions: Vec<&str> = out
1931            .pragma_dds_xtopics
1932            .iter()
1933            .map(|p| p.version.as_str())
1934            .collect();
1935        assert!(versions.contains(&"1.0"));
1936        assert!(versions.contains(&"1.3"));
1937    }
1938
1939    #[test]
1940    fn pragma_dds_xtopics_nested_pragmas_handled() {
1941        // dds_xtopics + keylist + prefix in the same file — all three
1942        // pragmas are collected separately, without conflict.
1943        let out = process(
1944            "#pragma prefix \"acme.com\"\n\
1945             #pragma dds_xtopics version=\"1.3\"\n\
1946             #pragma keylist Topic key_field\n\
1947             struct Topic { long key_field; };\n",
1948        );
1949        assert_eq!(out.pragma_dds_xtopics.len(), 1);
1950        assert_eq!(out.pragma_dds_xtopics[0].version, "1.3");
1951        assert_eq!(out.pragma_prefixes.len(), 1);
1952        assert_eq!(out.pragma_keylists.len(), 1);
1953    }
1954
1955    #[test]
1956    fn pragma_dds_xtopics_without_version_value_is_empty() {
1957        // `#pragma dds_xtopics` without version= — permitted (marker-only),
1958        // the version field is empty.
1959        let out = process("#pragma dds_xtopics\nstruct S { long x; };\n");
1960        assert_eq!(out.pragma_dds_xtopics.len(), 1);
1961        assert_eq!(out.pragma_dds_xtopics[0].version, "");
1962    }
1963
1964    // ---- §7.3.1.3 Const-Eval: nested #define-Refs ----
1965
1966    #[test]
1967    fn nested_define_two_hops() {
1968        // #define A 100; #define B A; const long x = B;
1969        // Expected: B → A → 100.
1970        let out = run("#define A 100\n#define B A\nconst long x = B;\n");
1971        assert!(out.contains("const long x = 100;"), "{out}");
1972    }
1973
1974    #[test]
1975    fn nested_define_three_hops() {
1976        let out = run("#define A 7\n\
1977             #define B A\n\
1978             #define C B\n\
1979             const long x = C;\n");
1980        assert!(out.contains("const long x = 7;"), "{out}");
1981    }
1982
1983    #[test]
1984    fn nested_define_with_arithmetic_expression() {
1985        let out = run("#define UNIT 8\n\
1986             #define BUF (UNIT * 4)\n\
1987             const long x = BUF;\n");
1988        // BUF becomes (UNIT * 4) → (8 * 4); caller eval does the computation.
1989        assert!(out.contains("(8 * 4)"), "{out}");
1990    }
1991
1992    #[test]
1993    fn nested_define_self_recursive_terminates() {
1994        // #define A A — pathological; expand_macros must NOT run in an
1995        // infinite loop. The output must contain "A" (the self-
1996        // reference does not resolve).
1997        let out = run("#define A A\nconst long x = A;\n");
1998        assert!(out.contains("const long x = A;"), "{out}");
1999    }
2000
2001    #[test]
2002    fn nested_define_mutually_recursive_terminates() {
2003        // #define A B; #define B A; the expand cap MUST terminate.
2004        let out = run("#define A B\n#define B A\nconst long x = A;\n");
2005        // The concrete result is implementation-defined; important: no hang.
2006        assert!(out.contains("const long x ="));
2007    }
2008
2009    #[test]
2010    fn pragma_dds_xtopics_unquoted_version_accepted() {
2011        let out = process("#pragma dds_xtopics version=1.3\nstruct S { long x; };\n");
2012        assert_eq!(out.pragma_dds_xtopics.len(), 1);
2013        assert_eq!(out.pragma_dds_xtopics[0].version, "1.3");
2014    }
2015}