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 fuer OMG IDL 4.2.
4//!
5//! IDL erbt vom C-Preprocessor — und Vendor-IDL-Files (RTI, OpenSplice)
6//! nutzen `#include`, `#define`, `#ifdef`, `#pragma` regelmaessig.
7//! Damit der Parser solche Files konsumieren kann, sitzt der
8//! Preprocessor VOR dem Lexer und expandiert die Directives zu reinem
9//! IDL-Source.
10//!
11//! # Scope
12//!
13//! - **`#include "rel/path.idl"`** und **`#include <abs/path.idl>`**:
14//!   text-basierte Inklusion via [`Resolver`]-Trait
15//! - **`#define MACRO value`** (object-like) und
16//!   **`#define NAME(p1, p2) body`** (function-like) inkl.
17//!   `#`-Stringize und `##`-Token-Paste (Spec §7.2.5 + ISO 14882
18//!   §16.3.2/§16.3.3)
19//! - **`#ifdef`** / **`#ifndef`** / **`#if`** / **`#elif`** /
20//!   **`#else`** / **`#endif`**: konditionelle Kompilation mit
21//!   Expression-Eval (`defined`, `&&`, `||`, `!`, numerische Literale)
22//! - **`#pragma <args>`**: stripped (nicht im Output) — Vendor-Pragmas
23//!   wie RTI's `#pragma keylist` werden als spezielle AST-Nodes erfasst
24//! - **`#undef`**: Macro entfernen
25//!
26//! Nicht supported:
27//! - Recursive Macro-Expansion (eine Pass)
28//! - Variadic-Macros (`__VA_ARGS__`)
29//! - `#error`, `#warning`, `#line` (geparst, aber nicht funktional)
30//! - Volle C-PP-Arithmetik in `#if` (Vergleiche, Bitops, Ternary)
31//!
32//! # Source-Map
33//!
34//! [`SourceMap`] mappt jede Position im expandierten Output auf
35//! `(file_id, byte_offset_im_original)`. Damit Diagnostiken nach
36//! Parsing auf die richtige Original-Datei und -Zeile zeigen.
37//!
38//! # Beispiel
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-Kommentare nicht überall vollständig
59
60mod source_map;
61
62pub use source_map::{FileId, SourceLocation, SourceMap};
63
64use std::collections::HashMap;
65
66/// Trait fuer Include-File-Resolution.
67///
68/// Erlaubt File-IO (`FsResolver`), In-Memory-Tests (`MemoryResolver`)
69/// oder benutzerdefinierte Strategien.
70pub trait Resolver {
71    /// Loest einen `#include "path"` (relativ) oder `#include <path>`
72    /// (system) zu Source-Text auf.
73    ///
74    /// # Errors
75    /// Implementierungs-spezifisch. Sollte einen sprechenden Fehler
76    /// liefern, der den gesuchten Pfad enthaelt.
77    fn resolve(&self, requesting_file: &str, include: &Include) -> Result<String, ResolveError>;
78}
79
80/// Beschreibt einen Include-Request.
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub enum Include {
83    /// `#include "name"` — relative/lokale Suche.
84    Quoted(String),
85    /// `#include <name>` — System-Pfad-Suche.
86    System(String),
87}
88
89impl Include {
90    /// Pfad-Komponente unabhaengig vom 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-Fehler. Fehlende Datei, IO-Fehler, etc.
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct ResolveError {
102    /// Gesuchter Pfad (so wie er im `#include` stand).
103    pub requested: String,
104    /// Sprechende Beschreibung.
105    pub message: String,
106}
107
108/// In-Memory-Resolver fuer Tests und CLI-Tools, die Source ohne
109/// Filesystem-Zugriff verwalten.
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    /// Registriert eine Datei.
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/// Gesammelt vom Preprocessor; vom Konsumenten (Spec-Validator) gegen
142/// `typeprefix`-Decls auf Repository-ID-Konflikt geprueft (IDL 4.2
143/// §7.4.6.4.1.3).
144#[derive(Debug, Clone, PartialEq, Eq)]
145pub struct PragmaPrefix {
146    /// Prefix-String (ohne umgebende Anfuehrungszeichen).
147    pub prefix: String,
148    /// Quelldatei.
149    pub file: String,
150    /// Zeile (1-basiert).
151    pub line: usize,
152}
153
154/// `#pragma keylist Foo a b c` — Cyclone/OpenSplice-Konvention.
155///
156/// Gesammelt vom Preprocessor; vom Konsumenten (idlc, AST-Builder)
157/// kann ueber [`ProcessedSource::pragma_keylists`] gelesen werden.
158#[derive(Debug, Clone, PartialEq, Eq)]
159pub struct PragmaKeylist {
160    /// Topic-Type-Name.
161    pub type_name: String,
162    /// Key-Member-Namen.
163    pub keys: Vec<String>,
164    /// Quelldatei.
165    pub file: String,
166    /// Zeile (1-basiert).
167    pub line: usize,
168}
169
170/// OpenSplice-Legacy-spezifische Pragmas (`#pragma DCPS_DATA_TYPE`,
171/// `#pragma DCPS_DATA_KEY`, `#pragma cats`, `#pragma genequality`).
172///
173/// In OpenSplice-Versionen 5.x/6.x waren diese Pragmas die primaere
174/// Methode, IDL-Types als DDS-Topics zu markieren — vor der OMG-IDL-4.2-
175/// Annotation `@key`. Migration-Use-Cases muessen sie auf moderne
176/// `@key`/`@topic`-Annotations mappen.
177#[derive(Debug, Clone, PartialEq, Eq)]
178pub enum OpenSplicePragma {
179    /// `#pragma DCPS_DATA_TYPE "<TypeName>"` — markiert Type als
180    /// DDS-Topic-Type.
181    DataType {
182        type_name: String,
183        file: String,
184        line: usize,
185    },
186    /// `#pragma DCPS_DATA_KEY "<TypeName>.<field>"` — markiert
187    /// Member als Key.
188    DataKey {
189        type_name: String,
190        field: String,
191        file: String,
192        line: usize,
193    },
194    /// `#pragma cats <TypeName> <field>` — catenated keys
195    /// (alternative key-Markierung).
196    Cats {
197        type_name: String,
198        keys: Vec<String>,
199        file: String,
200        line: usize,
201    },
202    /// `#pragma genequality` — codegen-Flag fuer Gleichheits-Operator
203    /// in C++/Java-Bindings.
204    GenEquality { file: String, line: usize },
205}
206
207/// `#pragma dds_xtopics version="1.3"` (XTypes 1.3 §7.3.1.1.1) —
208/// erlaubt einer IDL-Datei zu markieren, gegen welche XTypes-Spec-
209/// Version sie geschrieben wurde. Der Compiler-Frontend kann dann
210/// vendor-Erweiterungen mit/ohne Version-Match validieren.
211#[derive(Debug, Clone, PartialEq, Eq)]
212pub struct PragmaDdsXtopics {
213    /// Version-String (z.B. `"1.3"`). Leer wenn nicht angegeben.
214    pub version: String,
215    /// Quelldatei.
216    pub file: String,
217    /// Zeile.
218    pub line: usize,
219}
220
221/// Resultat eines Preprocessor-Laufs.
222#[derive(Debug, Clone, Default)]
223pub struct ProcessedSource {
224    /// Expandierter IDL-Source, fertig fuer den Lexer.
225    pub expanded: String,
226    /// Mapping von Output-Position zu Original-(Datei,Position).
227    pub source_map: SourceMap,
228    /// Gesammelte `#pragma keylist`-Direktiven.
229    pub pragma_keylists: Vec<PragmaKeylist>,
230    /// Gesammelte OpenSplice-Legacy-Pragmas (`DCPS_DATA_TYPE`,
231    /// `DCPS_DATA_KEY`, `cats`, `genequality`).
232    pub opensplice_pragmas: Vec<OpenSplicePragma>,
233    /// Gesammelte `#pragma prefix "<prefix>"`-Direktiven (CORBA Part 1
234    /// §14.7.5). Vom Spec-Validator fuer §7.4.6.4.1.3 Repository-ID-
235    /// Konflikt-Detection genutzt.
236    pub pragma_prefixes: Vec<PragmaPrefix>,
237    /// Gesammelte `#pragma dds_xtopics version="..."`-Direktiven
238    /// (XTypes 1.3 §7.3.1.1.1). Mehrfach-Pragmas pro File sind erlaubt;
239    /// der Validator prueft Versions-Konsistenz.
240    pub pragma_dds_xtopics: Vec<PragmaDdsXtopics>,
241}
242
243/// Top-Level-Preprocessor-Fehler.
244#[derive(Debug, Clone, PartialEq, Eq)]
245pub enum PreprocessError {
246    /// `#include` konnte nicht aufgeloest werden.
247    IncludeNotFound(ResolveError),
248    /// `#include` schon im Expansion-Stack — Zyklus erkannt.
249    IncludeCycle {
250        /// Datei, die zyklisch eingebunden werden sollte.
251        file: String,
252    },
253    /// `#endif` ohne passendes `#ifdef`/`#ifndef`.
254    UnmatchedEndif {
255        /// Quell-Datei der unmatched-Direktive.
256        file: String,
257        /// 1-indexierte Zeile.
258        line: usize,
259    },
260    /// `#else` ohne passendes `#ifdef`/`#ifndef`.
261    UnmatchedElse { file: String, line: usize },
262    /// `#ifdef`/`#ifndef` ohne abschliessendes `#endif`.
263    UnclosedConditional { file: String, line: usize },
264    /// Direktive mit fehlerhafter Syntax (z.B. `#define` ohne Name).
265    SyntaxError {
266        file: String,
267        line: usize,
268        message: String,
269    },
270    /// `#error <message>` — explizit angefragter Build-Stopp.
271    ErrorDirective {
272        /// Quelldatei.
273        file: String,
274        /// Zeile.
275        line: usize,
276        /// Inhalt der Direktive.
277        message: String,
278    },
279    /// Backslash als letztes Zeichen im Source-File — Spec §7.3:
280    /// "A backslash character may not be the last character in a source
281    /// file."
282    TrailingBackslash {
283        /// Quelldatei.
284        file: String,
285    },
286}
287
288impl core::fmt::Display for PreprocessError {
289    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
290        match self {
291            Self::IncludeNotFound(e) => write!(f, "include not found: {}", e.requested),
292            Self::IncludeCycle { file } => write!(f, "include cycle: {file}"),
293            Self::UnmatchedEndif { file, line } => {
294                write!(f, "unmatched #endif at {file}:{line}")
295            }
296            Self::UnmatchedElse { file, line } => {
297                write!(f, "unmatched #else at {file}:{line}")
298            }
299            Self::UnclosedConditional { file, line } => {
300                write!(f, "unclosed conditional starting at {file}:{line}")
301            }
302            Self::SyntaxError {
303                file,
304                line,
305                message,
306            } => {
307                write!(f, "preprocessor syntax error at {file}:{line}: {message}")
308            }
309            Self::ErrorDirective {
310                file,
311                line,
312                message,
313            } => {
314                write!(f, "#error at {file}:{line}: {message}")
315            }
316            Self::TrailingBackslash { file } => {
317                write!(f, "trailing backslash at end of source file: {file}")
318            }
319        }
320    }
321}
322
323impl std::error::Error for PreprocessError {}
324
325/// Top-Level-Preprocessor.
326///
327/// Konstruktion via [`Preprocessor::new`] mit einem [`Resolver`].
328/// Anschliessend `process(file_name, source)` aufrufen.
329pub struct Preprocessor<R: Resolver> {
330    resolver: R,
331}
332
333impl<R: Resolver> Preprocessor<R> {
334    pub fn new(resolver: R) -> Self {
335        Self { resolver }
336    }
337
338    /// Verarbeitet einen Source-String und expandiert alle Direktiven.
339    ///
340    /// `file_name` wird in Diagnostiken angezeigt und ist die "current
341    /// file" fuer relative `#include`-Aufloesung.
342    ///
343    /// # Errors
344    /// Siehe [`PreprocessError`].
345    pub fn process(
346        &self,
347        file_name: &str,
348        source: &str,
349    ) -> Result<ProcessedSource, PreprocessError> {
350        let mut state = State::new();
351        let root_id = state.source_map.add_file(file_name);
352        let mut output = String::new();
353        // Backslash-Newline-Continuation (Spec §7.3, ISO 14882 5.2):
354        // Ein `\` direkt vor `\n` wird durch Token-Splice ersetzt — d.h.
355        // beide Zeichen werden entfernt, die nachfolgende Zeile setzt
356        // syntaktisch fort. Wir tun das vor der Zeilen-Iteration, damit
357        // multi-line `#define X foo \\\n bar` korrekt erkannt werden.
358        let spliced = splice_backslash_newlines(source);
359        // Spec §7.3: "A backslash character may not be the last character
360        // in a source file." Wenn nach dem Splicing noch ein Backslash am
361        // File-Ende uebrig ist, war er nicht von einem `\n` gefolgt.
362        if spliced.ends_with('\\') {
363            return Err(PreprocessError::TrailingBackslash {
364                file: file_name.to_string(),
365            });
366        }
367        self.expand_into(file_name, &spliced, root_id, &mut state, &mut output, 0)?;
368        Ok(ProcessedSource {
369            expanded: output,
370            source_map: state.source_map,
371            pragma_keylists: state.pragma_keylists,
372            opensplice_pragmas: state.opensplice_pragmas,
373            pragma_prefixes: state.pragma_prefixes,
374            pragma_dds_xtopics: state.pragma_dds_xtopics,
375        })
376    }
377
378    fn expand_into(
379        &self,
380        file_name: &str,
381        source: &str,
382        file_id: FileId,
383        state: &mut State,
384        output: &mut String,
385        depth: usize,
386    ) -> Result<(), PreprocessError> {
387        if state.include_stack.iter().any(|f| f == file_name) {
388            return Err(PreprocessError::IncludeCycle {
389                file: file_name.to_string(),
390            });
391        }
392        state.include_stack.push(file_name.to_string());
393
394        let mut conditional_stack: Vec<ConditionalFrame> = Vec::new();
395        let mut byte_offset = 0usize;
396
397        for (line_idx, line) in source.split_inclusive('\n').enumerate() {
398            let line_no = line_idx + 1;
399            let trimmed = line.trim_start();
400
401            // Conditional-Skipping: wenn aktuell in einem inactive
402            // Frame, alles ausser #else/#endif/#ifdef/#ifndef ueberspringen.
403            let active = conditional_stack.iter().all(|f| f.active);
404
405            if let Some(directive) = parse_directive(trimmed) {
406                match directive {
407                    Directive::Ifdef(name) => {
408                        let parent_active = active;
409                        let cond = parent_active && state.macros.contains_key(name);
410                        conditional_stack.push(ConditionalFrame {
411                            active: cond,
412                            else_seen: false,
413                            parent_active,
414                            taken: cond,
415                        });
416                    }
417                    Directive::Ifndef(name) => {
418                        let parent_active = active;
419                        let cond = parent_active && !state.macros.contains_key(name);
420                        conditional_stack.push(ConditionalFrame {
421                            active: cond,
422                            else_seen: false,
423                            parent_active,
424                            taken: cond,
425                        });
426                    }
427                    Directive::Else => {
428                        let frame = conditional_stack.last_mut().ok_or_else(|| {
429                            PreprocessError::UnmatchedElse {
430                                file: file_name.to_string(),
431                                line: line_no,
432                            }
433                        })?;
434                        if frame.else_seen {
435                            return Err(PreprocessError::SyntaxError {
436                                file: file_name.to_string(),
437                                line: line_no,
438                                message: "duplicate #else".to_string(),
439                            });
440                        }
441                        frame.else_seen = true;
442                        // Else aktiviert sich nur wenn parent aktiv UND
443                        // bisher KEINE Branch genommen wurde.
444                        frame.active = frame.parent_active && !frame.taken;
445                        if frame.active {
446                            frame.taken = true;
447                        }
448                    }
449                    Directive::Endif => {
450                        if conditional_stack.pop().is_none() {
451                            return Err(PreprocessError::UnmatchedEndif {
452                                file: file_name.to_string(),
453                                line: line_no,
454                            });
455                        }
456                    }
457                    Directive::If(expr) => {
458                        let parent_active = active;
459                        let cond = parent_active && eval_if_expr(expr, &state.macros);
460                        conditional_stack.push(ConditionalFrame {
461                            parent_active,
462                            active: cond,
463                            else_seen: false,
464                            taken: cond,
465                        });
466                    }
467                    Directive::Elif(expr) => {
468                        let Some(frame) = conditional_stack.last_mut() else {
469                            return Err(PreprocessError::UnmatchedEndif {
470                                file: file_name.to_string(),
471                                line: line_no,
472                            });
473                        };
474                        if frame.else_seen {
475                            return Err(PreprocessError::SyntaxError {
476                                file: file_name.to_string(),
477                                line: line_no,
478                                message: "#elif after #else".to_string(),
479                            });
480                        }
481                        // Erst aktiv wenn parent aktiv UND noch keine
482                        // taken Branch UND expr=true.
483                        let cond = frame.parent_active
484                            && !frame.taken
485                            && eval_if_expr(expr, &state.macros);
486                        frame.active = cond;
487                        if cond {
488                            frame.taken = true;
489                        }
490                    }
491                    _ if !active => {
492                        // In inaktivem Block — andere Direktiven nicht
493                        // ausfuehren.
494                    }
495                    Directive::Define(name, def) => {
496                        state.macros.insert(name.to_string(), def);
497                    }
498                    Directive::Undef(name) => {
499                        state.macros.remove(name);
500                    }
501                    Directive::Include(inc) => {
502                        if depth > MAX_INCLUDE_DEPTH {
503                            return Err(PreprocessError::SyntaxError {
504                                file: file_name.to_string(),
505                                line: line_no,
506                                message: format!("include depth exceeded {MAX_INCLUDE_DEPTH}"),
507                            });
508                        }
509                        let inc_path = inc.path().to_string();
510                        // Cycle-Detection vor Resolve, damit Zyklen
511                        // unabhaengig vom Resolver erkannt werden.
512                        if state.include_stack.iter().any(|f| f == &inc_path) {
513                            return Err(PreprocessError::IncludeCycle { file: inc_path });
514                        }
515                        let included = self
516                            .resolver
517                            .resolve(file_name, &inc)
518                            .map_err(PreprocessError::IncludeNotFound)?;
519                        let inc_id = state.source_map.add_file(&inc_path);
520                        self.expand_into(&inc_path, &included, inc_id, state, output, depth + 1)?;
521                    }
522                    Directive::Pragma(args) => {
523                        if let Some(keylist) = parse_pragma_keylist(args, file_name, line_no) {
524                            state.pragma_keylists.push(keylist);
525                        } else if let Some(osp) = parse_opensplice_pragma(args, file_name, line_no)
526                        {
527                            state.opensplice_pragmas.push(osp);
528                        } else if let Some(pp) = parse_pragma_prefix(args, file_name, line_no) {
529                            state.pragma_prefixes.push(pp);
530                        } else if let Some(xt) = parse_pragma_dds_xtopics(args, file_name, line_no)
531                        {
532                            state.pragma_dds_xtopics.push(xt);
533                        }
534                        // Andere Pragmas: gestrippt.
535                    }
536                    Directive::Error(msg) => {
537                        return Err(PreprocessError::ErrorDirective {
538                            file: file_name.to_string(),
539                            line: line_no,
540                            message: msg.trim().to_string(),
541                        });
542                    }
543                    Directive::Warning(_msg) => {
544                        // `#warning` ist Diagnose ohne Abort.
545                        // gestripped; UI-Layer kann Warnungen via Hook
546                        // rendern (kommt mit Diagnostic-Reporter).
547                    }
548                    Directive::Line(_args) => {
549                        // `#line N "file"` — Position-Override.
550                        // SourceMap-Integration steht aus.
551                    }
552                }
553            } else if active {
554                // Normale Source-Zeile: Macros expandieren und ans
555                // Output anhaengen.
556                let expanded = expand_macros(line, &state.macros);
557                state
558                    .source_map
559                    .record_segment(output.len(), expanded.len(), file_id, byte_offset);
560                output.push_str(&expanded);
561            }
562
563            byte_offset += line.len();
564        }
565
566        if let Some(frame) = conditional_stack.first() {
567            let _ = frame;
568            return Err(PreprocessError::UnclosedConditional {
569                file: file_name.to_string(),
570                line: 0,
571            });
572        }
573
574        state.include_stack.pop();
575        Ok(())
576    }
577}
578
579const MAX_INCLUDE_DEPTH: usize = 64;
580
581struct State {
582    macros: HashMap<String, MacroDef>,
583    include_stack: Vec<String>,
584    source_map: SourceMap,
585    pragma_keylists: Vec<PragmaKeylist>,
586    opensplice_pragmas: Vec<OpenSplicePragma>,
587    pragma_prefixes: Vec<PragmaPrefix>,
588    pragma_dds_xtopics: Vec<PragmaDdsXtopics>,
589}
590
591impl State {
592    fn new() -> Self {
593        Self {
594            macros: HashMap::new(),
595            include_stack: Vec::new(),
596            source_map: SourceMap::new(),
597            pragma_keylists: Vec::new(),
598            opensplice_pragmas: Vec::new(),
599            pragma_prefixes: Vec::new(),
600            pragma_dds_xtopics: Vec::new(),
601        }
602    }
603}
604
605/// Definition eines `#define`-Macros — entweder object-like oder
606/// function-like (mit Parameter-Liste).
607#[derive(Clone, Debug, PartialEq, Eq)]
608struct MacroDef {
609    /// `Some(params)` fuer function-like Macros (`#define NAME(p1, p2) body`),
610    /// `None` fuer object-like (`#define NAME body`).
611    params: Option<Vec<String>>,
612    /// Macro-Body (unexpandiert).
613    body: String,
614}
615
616impl MacroDef {
617    fn object_like(body: &str) -> Self {
618        Self {
619            params: None,
620            body: body.to_string(),
621        }
622    }
623
624    fn function_like(params: Vec<String>, body: &str) -> Self {
625        Self {
626            params: Some(params),
627            body: body.to_string(),
628        }
629    }
630}
631
632/// Splice backslash-newline pairs (Token-Splicing) gemaess §7.3 / ISO 14882.
633fn splice_backslash_newlines(src: &str) -> String {
634    // Wir arbeiten Byte-orientiert; UTF-8 kompatibel weil `\` und `\n` ASCII sind.
635    let bytes = src.as_bytes();
636    let mut out = Vec::with_capacity(bytes.len());
637    let mut i = 0;
638    while i < bytes.len() {
639        if bytes[i] == b'\\' && i + 1 < bytes.len() && bytes[i + 1] == b'\n' {
640            // Skip both bytes.
641            i += 2;
642            continue;
643        }
644        if bytes[i] == b'\\'
645            && i + 2 < bytes.len()
646            && bytes[i + 1] == b'\r'
647            && bytes[i + 2] == b'\n'
648        {
649            i += 3;
650            continue;
651        }
652        out.push(bytes[i]);
653        i += 1;
654    }
655    // Sicher: nur ASCII-Bytes wurden entfernt → bleibt valides UTF-8.
656    String::from_utf8(out).unwrap_or_default()
657}
658
659struct ConditionalFrame {
660    /// `true`, wenn die aktuelle Branch aktiv ist (Tokens werden emittiert).
661    active: bool,
662    /// `true`, wenn `#else` schon gesehen wurde (zweiter `#else` Fehler).
663    else_seen: bool,
664    /// War der Parent-Frame aktiv? Wenn nein, ist diese Branch ohnehin
665    /// nicht aktiv — wichtig fuer korrektes #else-Toggling in nested.
666    parent_active: bool,
667    /// `true` sobald irgendeine Branch (`#if`/`#elif`) als aktiv gewaehlt
668    /// wurde. Folgende `#elif`/`#else` werden ignoriert. (#if/#elif-Eval)
669    taken: bool,
670}
671
672/// Erkennbare Preprocessor-Direktiven.
673#[derive(Debug, PartialEq, Eq)]
674enum Directive<'a> {
675    Include(Include),
676    Define(&'a str, MacroDef),
677    Undef(&'a str),
678    Ifdef(&'a str),
679    Ifndef(&'a str),
680    /// `#if <const-expr>` — vereinfachte Expression-Eval:
681    /// `defined(MACRO)`, `0`/`1`, sowie `&&`/`||`/`!`.
682    /// (Spec-Stufe-2; gated via `preprocessor_full` Feature.)
683    If(&'a str),
684    /// `#elif <const-expr>` — Variante von `#if`.
685    Elif(&'a str),
686    Else,
687    Endif,
688    Pragma(&'a str),
689    /// `#error <message>` — bricht den Build ab.
690    Error(&'a str),
691    /// `#warning <message>` — Diagnose ohne Abort
692    /// (gated via `preprocessor_warning_line` Feature).
693    Warning(&'a str),
694    /// `#line <linenum> ["filename"]` — Source-Position-Override
695    /// (gated via `preprocessor_warning_line` Feature; /// gestripped, SourceMap-Update folgt bei Bedarf).
696    Line(&'a str),
697}
698
699fn parse_directive(line: &str) -> Option<Directive<'_>> {
700    let stripped = line.strip_prefix('#')?.trim_start();
701    let (head, rest) = match stripped.find(|c: char| c.is_whitespace()) {
702        Some(idx) => (&stripped[..idx], stripped[idx..].trim()),
703        None => (stripped.trim_end(), ""),
704    };
705    match head {
706        "include" => parse_include(rest).map(Directive::Include),
707        "define" => parse_define(rest),
708        "undef" => Some(Directive::Undef(rest)),
709        "ifdef" => Some(Directive::Ifdef(rest)),
710        "ifndef" => Some(Directive::Ifndef(rest)),
711        "if" => Some(Directive::If(rest)),
712        "elif" => Some(Directive::Elif(rest)),
713        "else" => Some(Directive::Else),
714        "endif" => Some(Directive::Endif),
715        "pragma" => Some(Directive::Pragma(rest)),
716        "error" => Some(Directive::Error(rest)),
717        "warning" => Some(Directive::Warning(rest)),
718        "line" => Some(Directive::Line(rest)),
719        _ => None,
720    }
721}
722
723/// Vereinfachte `#if`-Expression-Evaluation (Spec §7.3.2 + ISO 14882
724/// constant-expression Subset).
725///
726/// Unterstuetzt:
727/// - Numerische Literale: `0` (false), alles andere (true).
728/// - `defined(MACRO)` und `defined MACRO` — true wenn Macro definiert.
729/// - Boolean-Operatoren `&&`/`||`/`!` (links-nach-rechts, kein Precedence).
730/// - Macro-Identifiers werden als nicht-definiert (false) interpretiert,
731///   ausser sie sind explizit als `defined(...)` gewrapped.
732///
733/// Volle C-Preprocessor-Expression-Eval (Arithmetik, Vergleiche,
734/// Bitops, Ternary) ist nicht implementiert.
735fn eval_if_expr(expr: &str, macros: &HashMap<String, MacroDef>) -> bool {
736    let trimmed = expr.trim();
737    if trimmed.is_empty() {
738        return false;
739    }
740    // Tokenize einfach.
741    let normalized = normalize_if_tokens(trimmed);
742    eval_if_tokens(&normalized, macros)
743}
744
745fn normalize_if_tokens(expr: &str) -> Vec<String> {
746    let mut out = Vec::new();
747    let mut chars = expr.chars().peekable();
748    while let Some(c) = chars.next() {
749        match c {
750            ' ' | '\t' => {}
751            '(' | ')' | '!' => out.push(c.to_string()),
752            '&' if chars.peek() == Some(&'&') => {
753                chars.next();
754                out.push("&&".into());
755            }
756            '|' if chars.peek() == Some(&'|') => {
757                chars.next();
758                out.push("||".into());
759            }
760            c if c.is_ascii_alphabetic() || c == '_' => {
761                let mut buf = String::from(c);
762                while let Some(&n) = chars.peek() {
763                    if n.is_ascii_alphanumeric() || n == '_' {
764                        buf.push(n);
765                        chars.next();
766                    } else {
767                        break;
768                    }
769                }
770                out.push(buf);
771            }
772            c if c.is_ascii_digit() => {
773                let mut buf = String::from(c);
774                while let Some(&n) = chars.peek() {
775                    if n.is_ascii_digit() {
776                        buf.push(n);
777                        chars.next();
778                    } else {
779                        break;
780                    }
781                }
782                out.push(buf);
783            }
784            _ => {} // unbekannte Zeichen ignorieren (defensive)
785        }
786    }
787    out
788}
789
790/// zerodds-lint: recursion-depth 64 (If-Expr; bounded by IDL nesting)
791fn eval_if_tokens(tokens: &[String], macros: &HashMap<String, MacroDef>) -> bool {
792    let (val, _) = eval_or(tokens, 0, macros);
793    val
794}
795
796fn eval_or(tokens: &[String], idx: usize, macros: &HashMap<String, MacroDef>) -> (bool, usize) {
797    let (mut left, mut i) = eval_and(tokens, idx, macros);
798    while tokens.get(i).map(String::as_str) == Some("||") {
799        let (right, ni) = eval_and(tokens, i + 1, macros);
800        left = left || right;
801        i = ni;
802    }
803    (left, i)
804}
805
806fn eval_and(tokens: &[String], idx: usize, macros: &HashMap<String, MacroDef>) -> (bool, usize) {
807    let (mut left, mut i) = eval_not(tokens, idx, macros);
808    while tokens.get(i).map(String::as_str) == Some("&&") {
809        let (right, ni) = eval_not(tokens, i + 1, macros);
810        left = left && right;
811        i = ni;
812    }
813    (left, i)
814}
815
816/// zerodds-lint: recursion-depth 16 (logical-not chain; bounded by IDL macro nesting)
817fn eval_not(tokens: &[String], idx: usize, macros: &HashMap<String, MacroDef>) -> (bool, usize) {
818    if tokens.get(idx).map(String::as_str) == Some("!") {
819        let (v, ni) = eval_not(tokens, idx + 1, macros);
820        return (!v, ni);
821    }
822    eval_atom(tokens, idx, macros)
823}
824
825fn eval_atom(tokens: &[String], idx: usize, macros: &HashMap<String, MacroDef>) -> (bool, usize) {
826    let Some(tok) = tokens.get(idx) else {
827        return (false, idx);
828    };
829    if tok == "(" {
830        let (v, ni) = eval_or(tokens, idx + 1, macros);
831        let after = if tokens.get(ni).map(String::as_str) == Some(")") {
832            ni + 1
833        } else {
834            ni
835        };
836        return (v, after);
837    }
838    if tok == "defined" {
839        // `defined(MACRO)` oder `defined MACRO`.
840        let (next_idx, ident) = if tokens.get(idx + 1).map(String::as_str) == Some("(") {
841            (
842                idx + 3,
843                tokens.get(idx + 2).map(String::as_str).unwrap_or(""),
844            )
845        } else {
846            (
847                idx + 2,
848                tokens.get(idx + 1).map(String::as_str).unwrap_or(""),
849            )
850        };
851        let v = macros.contains_key(ident);
852        let after = if tokens.get(idx + 1).map(String::as_str) == Some("(") {
853            // Skip closing ')'.
854            if tokens.get(next_idx).map(String::as_str) == Some(")") {
855                next_idx + 1
856            } else {
857                next_idx
858            }
859        } else {
860            next_idx
861        };
862        return (v, after);
863    }
864    // Numerisches Literal: `0` = false, sonst true.
865    if let Ok(n) = tok.parse::<i64>() {
866        return (n != 0, idx + 1);
867    }
868    // Identifier ohne `defined()` — als macro-value-Lookup behandeln;
869    // wenn macro-body parsbar als int → entsprechend; sonst true (Macro
870    // existiert). Function-like Macros werden hier als true behandelt
871    // (Aufruf-Parameter im `#if`-Kontext sind unueblich).
872    if let Some(def) = macros.get(tok) {
873        if let Ok(n) = def.body.trim().parse::<i64>() {
874            return (n != 0, idx + 1);
875        }
876        return (true, idx + 1);
877    }
878    // Unbekannter Identifier → false (Spec C-PP-Konvention).
879    (false, idx + 1)
880}
881
882fn parse_include(rest: &str) -> Option<Include> {
883    let rest = rest.trim();
884    if let Some(stripped) = rest.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
885        return Some(Include::Quoted(stripped.to_string()));
886    }
887    rest.strip_prefix('<')
888        .and_then(|s| s.strip_suffix('>'))
889        .map(|stripped| Include::System(stripped.to_string()))
890}
891
892fn parse_define(rest: &str) -> Option<Directive<'_>> {
893    let rest = rest.trim_end_matches('\n').trim();
894    if rest.is_empty() {
895        return None;
896    }
897    // Identifier-Teil bis zum ersten Whitespace ODER `(`. Ein direkt
898    // anschliessendes `(` ohne Whitespace markiert ein function-like
899    // Macro (Spec §7.2.5 + ISO 14882 §16.3).
900    let name_end = rest
901        .find(|c: char| c.is_whitespace() || c == '(')
902        .unwrap_or(rest.len());
903    let name = &rest[..name_end];
904    if name.is_empty() {
905        return None;
906    }
907    let after_name = &rest[name_end..];
908    if let Some(after_paren) = after_name.strip_prefix('(') {
909        // function-like: `NAME(p1, p2, ...) body`
910        let close = after_paren.find(')')?;
911        let params_src = &after_paren[..close];
912        let body = after_paren[close + 1..].trim();
913        let params: Vec<String> = if params_src.trim().is_empty() {
914            Vec::new()
915        } else {
916            params_src
917                .split(',')
918                .map(|p| p.trim().to_string())
919                .collect()
920        };
921        return Some(Directive::Define(
922            name,
923            MacroDef::function_like(params, body),
924        ));
925    }
926    let body = after_name.trim();
927    Some(Directive::Define(name, MacroDef::object_like(body)))
928}
929
930/// `#pragma prefix "<prefix>"` — CORBA Part 1 §14.7.5.
931///
932/// Liefert `None`, wenn die Pragma kein prefix-Pragma ist oder das
933/// String-Argument fehlt/leer ist.
934fn parse_pragma_prefix(args: &str, file: &str, line: usize) -> Option<PragmaPrefix> {
935    let trimmed = args.trim();
936    let rest = trimmed.strip_prefix("prefix")?.trim_start();
937    let prefix = strip_optional_quotes(rest).trim().to_string();
938    if prefix.is_empty() {
939        return None;
940    }
941    Some(PragmaPrefix {
942        prefix,
943        file: file.to_string(),
944        line,
945    })
946}
947
948/// `#pragma dds_xtopics version="1.3"` — XTypes 1.3 §7.3.1.1.1.
949///
950/// Liefert `None`, wenn die Pragma keine dds_xtopics-Pragma ist.
951fn parse_pragma_dds_xtopics(args: &str, file: &str, line: usize) -> Option<PragmaDdsXtopics> {
952    let trimmed = args.trim();
953    let rest = trimmed.strip_prefix("dds_xtopics")?.trim_start();
954    // Akzeptiert sowohl `version="1.3"` als auch `version=1.3`.
955    let version = if rest.is_empty() {
956        String::new()
957    } else if let Some(v) = rest.strip_prefix("version") {
958        v.trim_start()
959            .strip_prefix('=')
960            .unwrap_or(v)
961            .trim()
962            .trim_matches('"')
963            .to_string()
964    } else {
965        rest.trim_matches('"').to_string()
966    };
967    Some(PragmaDdsXtopics {
968        version,
969        file: file.to_string(),
970        line,
971    })
972}
973
974/// `#pragma keylist <Type> <field>*` — Cyclone-DDS-Konvention.
975///
976/// Liefert `None`, wenn die Pragma kein keylist-Pragma ist.
977fn parse_pragma_keylist(args: &str, file: &str, line: usize) -> Option<PragmaKeylist> {
978    let trimmed = args.trim();
979    let rest = trimmed.strip_prefix("keylist")?.trim_start();
980    let mut parts = rest.split_whitespace();
981    let type_name = parts.next()?.to_string();
982    let keys: Vec<String> = parts.map(str::to_string).collect();
983    Some(PragmaKeylist {
984        type_name,
985        keys,
986        file: file.to_string(),
987        line,
988    })
989}
990
991/// Parst OpenSplice-Legacy-Pragmas (`DCPS_DATA_TYPE`, `DCPS_DATA_KEY`,
992/// `cats`, `genequality`).
993fn parse_opensplice_pragma(args: &str, file: &str, line: usize) -> Option<OpenSplicePragma> {
994    let trimmed = args.trim();
995    if let Some(rest) = trimmed.strip_prefix("DCPS_DATA_TYPE") {
996        let payload = rest.trim();
997        // Akzeptiert sowohl quoted ("Type") als auch unquoted (Type).
998        let type_name = strip_optional_quotes(payload).to_string();
999        if type_name.is_empty() {
1000            return None;
1001        }
1002        return Some(OpenSplicePragma::DataType {
1003            type_name,
1004            file: file.to_string(),
1005            line,
1006        });
1007    }
1008    if let Some(rest) = trimmed.strip_prefix("DCPS_DATA_KEY") {
1009        let payload = strip_optional_quotes(rest.trim());
1010        let dot = payload.find('.')?;
1011        let type_name = payload[..dot].trim().to_string();
1012        let field = payload[dot + 1..].trim().to_string();
1013        if type_name.is_empty() || field.is_empty() {
1014            return None;
1015        }
1016        return Some(OpenSplicePragma::DataKey {
1017            type_name,
1018            field,
1019            file: file.to_string(),
1020            line,
1021        });
1022    }
1023    if let Some(rest) = trimmed.strip_prefix("cats") {
1024        let mut parts = rest.split_whitespace();
1025        let type_name = parts.next()?.to_string();
1026        let keys: Vec<String> = parts.map(str::to_string).collect();
1027        if keys.is_empty() {
1028            return None;
1029        }
1030        return Some(OpenSplicePragma::Cats {
1031            type_name,
1032            keys,
1033            file: file.to_string(),
1034            line,
1035        });
1036    }
1037    if trimmed == "genequality" {
1038        return Some(OpenSplicePragma::GenEquality {
1039            file: file.to_string(),
1040            line,
1041        });
1042    }
1043    None
1044}
1045
1046fn strip_optional_quotes(s: &str) -> &str {
1047    let s = s.trim();
1048    s.strip_prefix('"')
1049        .and_then(|t| t.strip_suffix('"'))
1050        .unwrap_or(s)
1051}
1052
1053/// Object-like Macro-Substitution. Iteriert ueber Identifier-Tokens und
1054/// ersetzt Macro-Namen durch ihre Werte. Vereinfachte Variante — keine
1055/// Re-Expansion (keine Macro-in-Macro), keine function-like Macros.
1056fn expand_macros(line: &str, macros: &HashMap<String, MacroDef>) -> String {
1057    expand_macros_rec(line, macros, 0)
1058}
1059
1060/// Maximal-Tiefe fuer rekursive `#define`-Expansion. Schuetzt vor
1061/// pathologischen `#define A B` / `#define B A`-Cycles und vor
1062/// indirekten Zyklen ueber 2+-Hops. Wert deckt typische
1063/// IDL-Const-Pfade (≤ 8 Hops) mit reichlich Reserve ab.
1064const MAX_MACRO_EXPANSION_DEPTH: usize = 32;
1065
1066/// zerodds-lint: recursion-depth 32
1067fn expand_macros_rec(line: &str, macros: &HashMap<String, MacroDef>, depth: usize) -> String {
1068    if macros.is_empty() || depth >= MAX_MACRO_EXPANSION_DEPTH {
1069        return line.to_string();
1070    }
1071    let mut out = String::with_capacity(line.len());
1072    let bytes = line.as_bytes();
1073    let mut i = 0;
1074    let mut expanded_any = false;
1075    while i < bytes.len() {
1076        let c = bytes[i];
1077        if c.is_ascii_alphabetic() || c == b'_' {
1078            // Identifier scannen.
1079            let start = i;
1080            while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') {
1081                i += 1;
1082            }
1083            let ident = &line[start..i];
1084            let Some(def) = macros.get(ident) else {
1085                out.push_str(ident);
1086                continue;
1087            };
1088            expanded_any = true;
1089            match &def.params {
1090                None => out.push_str(&def.body),
1091                Some(params) => {
1092                    // function-like: `(` direkt (oder nach Whitespace)
1093                    // erwartet, sonst Identifier durchreichen.
1094                    let after = skip_ascii_ws(bytes, i);
1095                    if after >= bytes.len() || bytes[after] != b'(' {
1096                        out.push_str(ident);
1097                        continue;
1098                    }
1099                    let Some((args, end)) = parse_call_args(line, after) else {
1100                        out.push_str(ident);
1101                        continue;
1102                    };
1103                    let expanded = expand_function_like(params, &args, &def.body);
1104                    out.push_str(&expanded);
1105                    i = end;
1106                }
1107            }
1108        } else {
1109            out.push(c as char);
1110            i += 1;
1111        }
1112    }
1113    // Wenn etwas expandiert wurde, kann die Expansion selbst weitere
1114    // Macro-Refs enthalten — rekursiv nach-expandieren bis Fixed-Point
1115    // erreicht ist. `MAX_MACRO_EXPANSION_DEPTH` schuetzt vor
1116    // selbst-rekursiven `#define A A`-Pathologien.
1117    if expanded_any && out != line {
1118        return expand_macros_rec(&out, macros, depth + 1);
1119    }
1120    out
1121}
1122
1123fn skip_ascii_ws(bytes: &[u8], mut i: usize) -> usize {
1124    while i < bytes.len() && matches!(bytes[i], b' ' | b'\t') {
1125        i += 1;
1126    }
1127    i
1128}
1129
1130/// Parst `( arg1 , arg2 , ... )` ab Position `start` (zeigt auf `(`).
1131/// Liefert die Argumente und den Index direkt nach `)`. Kommas innerhalb
1132/// von Klammer-Paaren werden ignoriert (verschachtelte Calls).
1133fn parse_call_args(line: &str, start: usize) -> Option<(Vec<String>, usize)> {
1134    let bytes = line.as_bytes();
1135    debug_assert_eq!(bytes.get(start), Some(&b'('));
1136    let mut i = start + 1;
1137    let mut depth: usize = 1;
1138    let mut args: Vec<String> = Vec::new();
1139    let mut cur = String::new();
1140    while i < bytes.len() {
1141        let c = bytes[i] as char;
1142        match c {
1143            '(' => {
1144                depth += 1;
1145                cur.push(c);
1146                i += 1;
1147            }
1148            ')' => {
1149                depth -= 1;
1150                if depth == 0 {
1151                    args.push(cur.trim().to_string());
1152                    return Some((args, i + 1));
1153                }
1154                cur.push(c);
1155                i += 1;
1156            }
1157            ',' if depth == 1 => {
1158                args.push(cur.trim().to_string());
1159                cur.clear();
1160                i += 1;
1161            }
1162            _ => {
1163                cur.push(c);
1164                i += 1;
1165            }
1166        }
1167    }
1168    None
1169}
1170
1171/// Substituiert Parameter im function-like-Macro-Body. Erkennt
1172/// `#param` (Stringize, ISO 14882 §16.3.2) und `a##b` (Token-Paste,
1173/// §16.3.3).
1174fn expand_function_like(params: &[String], args: &[String], body: &str) -> String {
1175    let arg_for = |name: &str| -> Option<&str> {
1176        params
1177            .iter()
1178            .position(|p| p == name)
1179            .and_then(|idx| args.get(idx).map(String::as_str))
1180    };
1181    // Tokenisiere Body grob: Identifier vs. Rest.
1182    let mut tokens: Vec<BodyTok> = Vec::new();
1183    let bytes = body.as_bytes();
1184    let mut i = 0;
1185    while i < bytes.len() {
1186        let c = bytes[i];
1187        if c.is_ascii_alphabetic() || c == b'_' {
1188            let start = i;
1189            while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') {
1190                i += 1;
1191            }
1192            tokens.push(BodyTok::Ident(body[start..i].to_string()));
1193        } else if c == b'#' && i + 1 < bytes.len() && bytes[i + 1] == b'#' {
1194            tokens.push(BodyTok::Paste);
1195            i += 2;
1196        } else if c == b'#' {
1197            tokens.push(BodyTok::Stringize);
1198            i += 1;
1199        } else {
1200            tokens.push(BodyTok::Other((c as char).to_string()));
1201            i += 1;
1202        }
1203    }
1204    // Token-Paste-Pass: bindet `<a> ## <b>` zu `<a><b>`. Whitespace
1205    // direkt um `##` wird verworfen. Bei Treffer wird der vorhergehende
1206    // Eintrag aus `after_paste` gepoppt und mit dem rechten Operanden
1207    // (param-substituiert) konkateniert.
1208    let mut after_paste: Vec<BodyTok> = Vec::with_capacity(tokens.len());
1209    let mut k = 0;
1210    while k < tokens.len() {
1211        if matches!(tokens[k], BodyTok::Paste) {
1212            // Whitespace-Tokens links/rechts vom `##` ueberspringen:
1213            // Whitespace links liegt schon in `after_paste` — darum wird
1214            // der letzte Non-Whitespace-Eintrag gesucht.
1215            let mut lhs: Option<BodyTok> = None;
1216            while let Some(last) = after_paste.last() {
1217                if let BodyTok::Other(s) = last {
1218                    if s.chars().all(char::is_whitespace) {
1219                        after_paste.pop();
1220                        continue;
1221                    }
1222                }
1223                lhs = after_paste.pop();
1224                break;
1225            }
1226            let rhs_idx = skip_body_ws(&tokens, k + 1);
1227            match (lhs, tokens.get(rhs_idx)) {
1228                (Some(lhs_tok), Some(rhs_tok)) => {
1229                    let lhs_text = render_tok(&lhs_tok, params, args);
1230                    let rhs_text = render_tok(rhs_tok, params, args);
1231                    after_paste.push(BodyTok::Ident(format!("{lhs_text}{rhs_text}")));
1232                    k = rhs_idx + 1;
1233                }
1234                _ => {
1235                    // Stand-alone `##` ohne Operanden — als Literal behalten.
1236                    after_paste.push(BodyTok::Paste);
1237                    k += 1;
1238                }
1239            }
1240        } else {
1241            after_paste.push(tokens[k].clone());
1242            k += 1;
1243        }
1244    }
1245    // Render-Pass: Stringize und Param-Substitution.
1246    let mut out = String::new();
1247    let mut j = 0;
1248    while j < after_paste.len() {
1249        match &after_paste[j] {
1250            BodyTok::Stringize => {
1251                let target_idx = skip_body_ws(&after_paste, j + 1);
1252                let arg_text = match after_paste.get(target_idx) {
1253                    Some(BodyTok::Ident(name)) => arg_for(name).unwrap_or(name).to_string(),
1254                    _ => String::new(),
1255                };
1256                out.push('"');
1257                for ch in arg_text.chars() {
1258                    if ch == '"' || ch == '\\' {
1259                        out.push('\\');
1260                    }
1261                    out.push(ch);
1262                }
1263                out.push('"');
1264                j = target_idx + 1;
1265            }
1266            BodyTok::Ident(name) => {
1267                if let Some(text) = arg_for(name) {
1268                    out.push_str(text);
1269                } else {
1270                    out.push_str(name);
1271                }
1272                j += 1;
1273            }
1274            BodyTok::Other(s) => {
1275                out.push_str(s);
1276                j += 1;
1277            }
1278            BodyTok::Paste => {
1279                // Stand-alone `##` ohne LHS — als Literal ausgeben
1280                // (sollte nach dem Paste-Pass nicht mehr vorkommen).
1281                out.push_str("##");
1282                j += 1;
1283            }
1284        }
1285    }
1286    out
1287}
1288
1289#[derive(Clone, Debug)]
1290enum BodyTok {
1291    Ident(String),
1292    Other(String),
1293    Stringize,
1294    Paste,
1295}
1296
1297fn skip_body_ws(tokens: &[BodyTok], mut i: usize) -> usize {
1298    while let Some(BodyTok::Other(s)) = tokens.get(i) {
1299        if !s.chars().all(char::is_whitespace) {
1300            break;
1301        }
1302        i += 1;
1303    }
1304    i
1305}
1306
1307fn render_tok(tok: &BodyTok, params: &[String], args: &[String]) -> String {
1308    match tok {
1309        BodyTok::Ident(name) => params
1310            .iter()
1311            .position(|p| p == name)
1312            .and_then(|idx| args.get(idx).cloned())
1313            .unwrap_or_else(|| name.clone()),
1314        BodyTok::Other(s) => s.clone(),
1315        BodyTok::Stringize => "#".to_string(),
1316        BodyTok::Paste => "##".to_string(),
1317    }
1318}
1319
1320#[cfg(test)]
1321mod tests {
1322    #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
1323    use super::*;
1324
1325    fn run(src: &str) -> String {
1326        Preprocessor::new(MemoryResolver::new())
1327            .process("main.idl", src)
1328            .expect("ok")
1329            .expanded
1330    }
1331
1332    fn run_with(resolver: MemoryResolver, src: &str) -> String {
1333        Preprocessor::new(resolver)
1334            .process("main.idl", src)
1335            .expect("ok")
1336            .expanded
1337    }
1338
1339    #[test]
1340    fn passthrough_for_source_without_directives() {
1341        let out = run("struct Foo { long x; };\n");
1342        assert!(out.contains("struct Foo"));
1343    }
1344
1345    #[test]
1346    fn pragma_is_stripped() {
1347        let out = run("#pragma keylist Foo x\nstruct Foo { long x; };\n");
1348        assert!(!out.contains("#pragma"));
1349        assert!(out.contains("struct Foo"));
1350    }
1351
1352    #[test]
1353    fn define_object_like_substitutes_in_subsequent_lines() {
1354        let out = run("#define MAX 100\nconst long L = MAX;\n");
1355        assert!(out.contains("const long L = 100;"), "{out}");
1356        assert!(!out.contains("#define"));
1357    }
1358
1359    #[test]
1360    fn ifdef_keeps_block_when_macro_defined() {
1361        let out = run("#define WITH\n#ifdef WITH\nstruct A {};\n#endif\n");
1362        assert!(out.contains("struct A"), "{out}");
1363    }
1364
1365    #[test]
1366    fn ifdef_drops_block_when_macro_not_defined() {
1367        let out = run("#ifdef WITH\nstruct A {};\n#endif\n");
1368        assert!(!out.contains("struct A"), "{out}");
1369    }
1370
1371    #[test]
1372    fn ifndef_inverse_of_ifdef() {
1373        let out = run("#ifndef WITH\nstruct B {};\n#endif\n");
1374        assert!(out.contains("struct B"), "{out}");
1375    }
1376
1377    #[test]
1378    fn else_branch_taken_when_initial_false() {
1379        let out = run("#ifdef NOPE\nstruct A {};\n#else\nstruct B {};\n#endif\n");
1380        assert!(!out.contains("struct A"), "{out}");
1381        assert!(out.contains("struct B"), "{out}");
1382    }
1383
1384    #[test]
1385    fn nested_ifdef_works() {
1386        let out = run("#define X\n\
1387             #ifdef X\n\
1388                #ifdef Y\nstruct YY {};\n#else\nstruct XnotY {};\n#endif\n\
1389             #endif\n");
1390        assert!(out.contains("struct XnotY"), "{out}");
1391        assert!(!out.contains("struct YY"));
1392    }
1393
1394    #[test]
1395    fn undef_removes_macro() {
1396        let out = run("#define M\n#undef M\n#ifdef M\nA\n#endif\n");
1397        assert!(!out.contains('A'), "{out}");
1398    }
1399
1400    #[test]
1401    fn quoted_include_resolves() {
1402        let mut r = MemoryResolver::new();
1403        r.add("inc.idl", "struct Inc {};\n");
1404        let out = run_with(r, "#include \"inc.idl\"\nstruct Main {};\n");
1405        assert!(out.contains("struct Inc"), "{out}");
1406        assert!(out.contains("struct Main"), "{out}");
1407    }
1408
1409    #[test]
1410    fn system_include_resolves() {
1411        let mut r = MemoryResolver::new();
1412        r.add("sys.idl", "struct Sys {};\n");
1413        let out = run_with(r, "#include <sys.idl>\nstruct Main {};\n");
1414        assert!(out.contains("struct Sys"), "{out}");
1415    }
1416
1417    #[test]
1418    fn missing_include_is_error() {
1419        let res = Preprocessor::new(MemoryResolver::new())
1420            .process("main.idl", "#include \"missing.idl\"\n");
1421        assert!(matches!(res, Err(PreprocessError::IncludeNotFound(_))));
1422    }
1423
1424    #[test]
1425    fn include_cycle_is_detected() {
1426        let mut r = MemoryResolver::new();
1427        r.add("a.idl", "#include \"main.idl\"\n");
1428        let res = Preprocessor::new(r).process("main.idl", "#include \"a.idl\"\n");
1429        assert!(matches!(res, Err(PreprocessError::IncludeCycle { .. })));
1430    }
1431
1432    #[test]
1433    fn unmatched_endif_is_error() {
1434        let res = Preprocessor::new(MemoryResolver::new()).process("main.idl", "#endif\n");
1435        assert!(matches!(res, Err(PreprocessError::UnmatchedEndif { .. })));
1436    }
1437
1438    #[test]
1439    fn unclosed_conditional_is_error() {
1440        let res = Preprocessor::new(MemoryResolver::new()).process("main.idl", "#ifdef X\n");
1441        assert!(matches!(
1442            res,
1443            Err(PreprocessError::UnclosedConditional { .. })
1444        ));
1445    }
1446
1447    #[test]
1448    fn unmatched_else_is_error() {
1449        let res = Preprocessor::new(MemoryResolver::new()).process("main.idl", "#else\n");
1450        assert!(matches!(res, Err(PreprocessError::UnmatchedElse { .. })));
1451    }
1452
1453    #[test]
1454    fn macro_in_inactive_branch_does_not_take_effect() {
1455        let out = run("#ifdef NOPE\n#define M 99\n#endif\n#ifdef M\nseen\n#endif\n");
1456        assert!(!out.contains("seen"));
1457    }
1458
1459    #[test]
1460    fn source_map_records_segments() {
1461        let result = Preprocessor::new(MemoryResolver::new())
1462            .process("main.idl", "struct A {};\nstruct B {};\n")
1463            .expect("ok");
1464        // Mind. zwei Segments fuer zwei Zeilen.
1465        assert!(
1466            result.source_map.segment_count() >= 2,
1467            "got {} segments",
1468            result.source_map.segment_count()
1469        );
1470    }
1471
1472    #[test]
1473    fn expand_macros_skips_unknown_identifiers() {
1474        let macros = HashMap::new();
1475        let out = expand_macros("foo bar baz", &macros);
1476        assert_eq!(out, "foo bar baz");
1477    }
1478
1479    #[test]
1480    fn expand_macros_substitutes_only_full_idents() {
1481        let mut m = HashMap::new();
1482        m.insert("X".to_string(), MacroDef::object_like("100"));
1483        // `XY` enthaelt `X` als Substring, darf aber nicht ersetzt werden.
1484        let out = expand_macros("X XY", &m);
1485        assert_eq!(out, "100 XY");
1486    }
1487
1488    // -----------------------------------------------------------------
1489    // §7.3 Stufe 2 — #if/#elif/#warning/#line (B7)
1490    // -----------------------------------------------------------------
1491
1492    #[test]
1493    fn if_eval_defined_macro_keeps_block() {
1494        let src = "\
1495#define FOO 1
1496#if defined(FOO)
1497struct InFoo { long x; };
1498#endif
1499struct After { long y; };
1500";
1501        let out = run(src);
1502        assert!(out.contains("struct InFoo"), "got: {out}");
1503        assert!(out.contains("struct After"), "got: {out}");
1504    }
1505
1506    #[test]
1507    fn if_eval_undefined_macro_drops_block() {
1508        let src = "\
1509#if defined(FOO)
1510struct ShouldBeGone { long x; };
1511#endif
1512struct Visible { long y; };
1513";
1514        let out = run(src);
1515        assert!(!out.contains("ShouldBeGone"), "got: {out}");
1516        assert!(out.contains("struct Visible"));
1517    }
1518
1519    #[test]
1520    fn if_eval_numeric_zero_drops_block() {
1521        let src = "#if 0\nstruct X { long x; };\n#endif\nstruct Y {};\n";
1522        let out = run(src);
1523        assert!(!out.contains("struct X"), "got: {out}");
1524    }
1525
1526    #[test]
1527    fn if_eval_numeric_nonzero_keeps_block() {
1528        let src = "#if 1\nstruct X { long x; };\n#endif\n";
1529        let out = run(src);
1530        assert!(out.contains("struct X"), "got: {out}");
1531    }
1532
1533    #[test]
1534    fn if_eval_logical_or() {
1535        let src = "\
1536#define A 1
1537#if defined(A) || defined(B)
1538struct Match { long m; };
1539#endif
1540";
1541        let out = run(src);
1542        assert!(out.contains("struct Match"), "got: {out}");
1543    }
1544
1545    #[test]
1546    fn if_eval_logical_not() {
1547        let src = "#if !defined(NOT_DEFINED)\nstruct K {};\n#endif\n";
1548        let out = run(src);
1549        assert!(out.contains("struct K"), "got: {out}");
1550    }
1551
1552    #[test]
1553    fn if_eval_logical_and_both_defined_keeps_block() {
1554        // §7.2.5: `&&` in `#if`-Eval — beide Operanden true.
1555        let src = "\
1556#define A 1
1557#define B 1
1558#if defined(A) && defined(B)
1559struct Both {};
1560#endif
1561";
1562        let out = run(src);
1563        assert!(out.contains("struct Both"), "got: {out}");
1564    }
1565
1566    #[test]
1567    fn if_eval_logical_and_one_undefined_drops_block() {
1568        // §7.2.5: `&&` mit einem undefined Operand → false.
1569        let src = "\
1570#define A 1
1571#if defined(A) && defined(NOT_DEFINED)
1572struct OnlyA {};
1573#endif
1574";
1575        let out = run(src);
1576        assert!(!out.contains("struct OnlyA"), "got: {out}");
1577    }
1578
1579    #[test]
1580    fn if_eval_logical_and_both_undefined_drops_block() {
1581        // §7.2.5: `&&` mit beiden undefined → false.
1582        let src = "\
1583#if defined(NOT_A) && defined(NOT_B)
1584struct Neither {};
1585#endif
1586";
1587        let out = run(src);
1588        assert!(!out.contains("struct Neither"), "got: {out}");
1589    }
1590
1591    #[test]
1592    fn if_elif_else_branches() {
1593        let src = "\
1594#if defined(NOT_DEFINED)
1595struct One {};
1596#elif defined(MODE)
1597struct WithMode {};
1598#else
1599struct Default {};
1600#endif
1601";
1602        // MODE nicht definiert → Default-Branch.
1603        let out = run(src);
1604        assert!(out.contains("struct Default"), "got: {out}");
1605        assert!(!out.contains("struct One"));
1606        assert!(!out.contains("struct WithMode"));
1607    }
1608
1609    #[test]
1610    fn if_elif_picks_first_true_branch() {
1611        let src = "\
1612#define MODE 1
1613#if defined(NOT_DEFINED)
1614struct A {};
1615#elif defined(MODE)
1616struct B {};
1617#elif defined(ANOTHER)
1618struct C {};
1619#else
1620struct D {};
1621#endif
1622";
1623        let out = run(src);
1624        assert!(out.contains("struct B"), "got: {out}");
1625        assert!(!out.contains("struct A"));
1626        assert!(!out.contains("struct C"));
1627        assert!(!out.contains("struct D"));
1628    }
1629
1630    #[test]
1631    fn warning_directive_does_not_abort() {
1632        let src = "#warning this is a warning\nstruct OK {};\n";
1633        let out = run(src);
1634        assert!(out.contains("struct OK"), "got: {out}");
1635    }
1636
1637    #[test]
1638    fn line_directive_does_not_abort() {
1639        let src = "#line 42 \"original.idl\"\nstruct X {};\n";
1640        let out = run(src);
1641        assert!(out.contains("struct X"), "got: {out}");
1642    }
1643
1644    // -----------------------------------------------------------------
1645    // C1 — OpenSplice-Legacy Pragmas (DCPS_DATA_TYPE/DATA_KEY/cats/
1646    // genequality). Migration-Use-Case fuer Referenz-Kunden.
1647    // -----------------------------------------------------------------
1648
1649    #[test]
1650    fn opensplice_pragma_data_type_quoted() {
1651        let src = r#"#pragma DCPS_DATA_TYPE "Sensor"
1652struct Sensor { long id; };
1653"#;
1654        let res = Preprocessor::new(MemoryResolver::new())
1655            .process("main.idl", src)
1656            .expect("ok");
1657        assert_eq!(res.opensplice_pragmas.len(), 1);
1658        match &res.opensplice_pragmas[0] {
1659            OpenSplicePragma::DataType { type_name, .. } => {
1660                assert_eq!(type_name, "Sensor");
1661            }
1662            other => panic!("expected DataType, got {other:?}"),
1663        }
1664    }
1665
1666    #[test]
1667    fn opensplice_pragma_data_type_unquoted() {
1668        let src = "#pragma DCPS_DATA_TYPE Sensor\nstruct Sensor {};\n";
1669        let res = Preprocessor::new(MemoryResolver::new())
1670            .process("main.idl", src)
1671            .expect("ok");
1672        match &res.opensplice_pragmas[0] {
1673            OpenSplicePragma::DataType { type_name, .. } => {
1674                assert_eq!(type_name, "Sensor");
1675            }
1676            other => panic!("expected DataType, got {other:?}"),
1677        }
1678    }
1679
1680    #[test]
1681    fn opensplice_pragma_data_key() {
1682        let src = r#"#pragma DCPS_DATA_KEY "Sensor.id"
1683struct Sensor { long id; };
1684"#;
1685        let res = Preprocessor::new(MemoryResolver::new())
1686            .process("main.idl", src)
1687            .expect("ok");
1688        match &res.opensplice_pragmas[0] {
1689            OpenSplicePragma::DataKey {
1690                type_name, field, ..
1691            } => {
1692                assert_eq!(type_name, "Sensor");
1693                assert_eq!(field, "id");
1694            }
1695            other => panic!("expected DataKey, got {other:?}"),
1696        }
1697    }
1698
1699    #[test]
1700    fn opensplice_pragma_cats() {
1701        let src = "#pragma cats Sensor id sub_id\nstruct Sensor {};\n";
1702        let res = Preprocessor::new(MemoryResolver::new())
1703            .process("main.idl", src)
1704            .expect("ok");
1705        match &res.opensplice_pragmas[0] {
1706            OpenSplicePragma::Cats {
1707                type_name, keys, ..
1708            } => {
1709                assert_eq!(type_name, "Sensor");
1710                assert_eq!(keys, &vec!["id".to_string(), "sub_id".to_string()]);
1711            }
1712            other => panic!("expected Cats, got {other:?}"),
1713        }
1714    }
1715
1716    #[test]
1717    fn opensplice_pragma_genequality() {
1718        let src = "#pragma genequality\nstruct S {};\n";
1719        let res = Preprocessor::new(MemoryResolver::new())
1720            .process("main.idl", src)
1721            .expect("ok");
1722        assert!(matches!(
1723            res.opensplice_pragmas.first(),
1724            Some(OpenSplicePragma::GenEquality { .. })
1725        ));
1726    }
1727
1728    #[test]
1729    fn opensplice_legacy_full_topic_decl() {
1730        // Realistisches OpenSplice-Legacy-Pattern: Topic + Key via
1731        // DCPS-Pragmas plus genequality fuer Codegen-Hint.
1732        let src = r#"#pragma DCPS_DATA_TYPE "Sensor"
1733#pragma DCPS_DATA_KEY "Sensor.id"
1734#pragma genequality
1735struct Sensor {
1736    long id;
1737    double value;
1738};
1739"#;
1740        let res = Preprocessor::new(MemoryResolver::new())
1741            .process("main.idl", src)
1742            .expect("ok");
1743        assert_eq!(res.opensplice_pragmas.len(), 3);
1744        assert!(res.expanded.contains("struct Sensor"));
1745    }
1746
1747    #[test]
1748    fn nested_if_in_active_branch() {
1749        let src = "\
1750#define OUTER 1
1751#if defined(OUTER)
1752#if defined(INNER)
1753struct ShouldBeGone {};
1754#else
1755struct InnerElse {};
1756#endif
1757#endif
1758";
1759        let out = run(src);
1760        assert!(out.contains("struct InnerElse"), "got: {out}");
1761        assert!(!out.contains("ShouldBeGone"), "got: {out}");
1762    }
1763
1764    // -----------------------------------------------------------------
1765    // §7.3 — Whitespace vor `#` (Phase 1.7)
1766    // -----------------------------------------------------------------
1767
1768    #[test]
1769    fn leading_whitespace_before_hash_accepted() {
1770        // Spec §7.3: "White space may appear before the #."
1771        let out = run("    #define X 1\nconst long Y = X;\n");
1772        assert!(out.contains("const long Y = 1;"), "got: {out}");
1773    }
1774
1775    // -----------------------------------------------------------------
1776    // §7.3 — Backslash-Newline-Continuation (Phase 1.8)
1777    // -----------------------------------------------------------------
1778
1779    #[test]
1780    fn line_continuation_in_define() {
1781        // Spec §7.3: backslash-newline wird durch Splicing entfernt.
1782        let out = run("#define LONG_MACRO foo \\\nbar\nLONG_MACRO\n");
1783        // Macro-Body ist `foo bar`, nach Substitution erscheint das im
1784        // Output.
1785        assert!(out.contains("foo bar"), "got: {out}");
1786    }
1787
1788    #[test]
1789    fn line_continuation_in_idl_line() {
1790        // Backslash-Newline auch außerhalb Direktiven entfernt.
1791        let out = run("const long\\\nX = 1;\n");
1792        // Nach Splicing: `const longX = 1;`. Nicht semantisch sinnvoll
1793        // aber Hauptsache: keine zwei Zeilen mehr — kein `\n` zwischen
1794        // `long` und `X`.
1795        assert!(!out.contains("long\nX"), "got: {out}");
1796    }
1797
1798    #[test]
1799    fn line_continuation_with_crlf() {
1800        // Windows-Style: `\\\r\n` muss ebenfalls als Continuation erkannt
1801        // werden.
1802        let out = run("#define M foo \\\r\nbar\nM\n");
1803        assert!(out.contains("foo bar"), "got: {out}");
1804    }
1805
1806    #[test]
1807    fn multi_line_continuation() {
1808        // Drei Zeilen mit Continuation verkettet.
1809        let out = run("#define M a \\\nb \\\nc\nM\n");
1810        assert!(out.contains("a b c"), "got: {out}");
1811    }
1812
1813    // -----------------------------------------------------------------
1814    // §7.3 — Backslash am File-Ende (Phase 1.9)
1815    // -----------------------------------------------------------------
1816
1817    #[test]
1818    fn trailing_backslash_at_file_end_is_error() {
1819        // Spec §7.3: "A backslash character may not be the last
1820        // character in a source file."
1821        let result = Preprocessor::new(MemoryResolver::new()).process("main.idl", "foo\\");
1822        assert!(
1823            matches!(result, Err(PreprocessError::TrailingBackslash { .. })),
1824            "got: {result:?}"
1825        );
1826    }
1827
1828    // -----------------------------------------------------------------
1829    // §7.2.5 — `#` Stringize + `##` Token-Paste in function-like Macros
1830    // -----------------------------------------------------------------
1831
1832    #[test]
1833    fn function_like_macro_substitutes_args() {
1834        // Voraussetzung fuer Stringize/Token-Paste: function-like
1835        // Macros werden ueberhaupt expandiert.
1836        let src = "#define ADD(a, b) a + b\nconst long L = ADD(1, 2);\n";
1837        let out = run(src);
1838        assert!(out.contains("1 + 2"), "got: {out}");
1839    }
1840
1841    #[test]
1842    fn stringize_param_in_function_macro() {
1843        // Spec §7.2.5 + ISO 14882 §16.3.2: `#param` im function-like-
1844        // Macro-Body wandelt das Argument in einen String-Literal um.
1845        let src = "#define STR(x) #x\nconst string S = STR(hello);\n";
1846        let out = run(src);
1847        assert!(out.contains("\"hello\""), "got: {out}");
1848    }
1849
1850    #[test]
1851    fn stringize_escapes_quotes_and_backslashes() {
1852        // ISO 14882 §16.3.2: `\` und `"` im Argument werden im
1853        // resultierenden String-Literal escaped.
1854        let src = "#define STR(x) #x\nconst string S = STR(a\"b\\c);\n";
1855        let out = run(src);
1856        assert!(out.contains("\"a\\\"b\\\\c\""), "got: {out}");
1857    }
1858
1859    #[test]
1860    fn token_paste_concatenates_idents() {
1861        // Spec §7.2.5 + ISO 14882 §16.3.3: `a##b` konkateniert zu `ab`.
1862        let src = "#define CAT(a, b) a##b\nconst long CAT(foo, bar) = 0;\n";
1863        let out = run(src);
1864        assert!(out.contains("foobar"), "got: {out}");
1865    }
1866
1867    #[test]
1868    fn token_paste_with_macro_args_produces_single_ident() {
1869        // Token-Paste muss Whitespace zwischen den Operanden tilgen.
1870        let src = "#define CAT(a, b) a ## b\nconst long CAT(x, y) = 0;\n";
1871        let out = run(src);
1872        assert!(out.contains("xy"), "got: {out}");
1873    }
1874
1875    // ---- §7.3.1.1.1 dds_xtopics-Pragma ----
1876
1877    fn process(src: &str) -> ProcessedSource {
1878        Preprocessor::new(MemoryResolver::new())
1879            .process("main.idl", src)
1880            .expect("ok")
1881    }
1882
1883    #[test]
1884    fn pragma_dds_xtopics_version_match() {
1885        let out = process("#pragma dds_xtopics version=\"1.3\"\nstruct S { long x; };\n");
1886        assert_eq!(out.pragma_dds_xtopics.len(), 1);
1887        assert_eq!(out.pragma_dds_xtopics[0].version, "1.3");
1888    }
1889
1890    #[test]
1891    fn pragma_dds_xtopics_version_mismatch_warns() {
1892        // Zwei dds_xtopics-Pragmas mit verschiedenen Versionen — beide werden
1893        // gesammelt; der Spec-Validator (separater Pass) erkennt
1894        // Mismatches.
1895        let out = process(
1896            "#pragma dds_xtopics version=\"1.0\"\n\
1897             #pragma dds_xtopics version=\"1.3\"\n\
1898             struct S { long x; };\n",
1899        );
1900        assert_eq!(out.pragma_dds_xtopics.len(), 2);
1901        let versions: Vec<&str> = out
1902            .pragma_dds_xtopics
1903            .iter()
1904            .map(|p| p.version.as_str())
1905            .collect();
1906        assert!(versions.contains(&"1.0"));
1907        assert!(versions.contains(&"1.3"));
1908    }
1909
1910    #[test]
1911    fn pragma_dds_xtopics_nested_pragmas_handled() {
1912        // dds_xtopics + keylist + prefix in derselben Datei — alle drei
1913        // Pragmas werden separat gesammelt, ohne Konflikt.
1914        let out = process(
1915            "#pragma prefix \"acme.com\"\n\
1916             #pragma dds_xtopics version=\"1.3\"\n\
1917             #pragma keylist Topic key_field\n\
1918             struct Topic { long key_field; };\n",
1919        );
1920        assert_eq!(out.pragma_dds_xtopics.len(), 1);
1921        assert_eq!(out.pragma_dds_xtopics[0].version, "1.3");
1922        assert_eq!(out.pragma_prefixes.len(), 1);
1923        assert_eq!(out.pragma_keylists.len(), 1);
1924    }
1925
1926    #[test]
1927    fn pragma_dds_xtopics_without_version_value_is_empty() {
1928        // `#pragma dds_xtopics` ohne version= — zulaessig (Marker-only),
1929        // version-Feld ist leer.
1930        let out = process("#pragma dds_xtopics\nstruct S { long x; };\n");
1931        assert_eq!(out.pragma_dds_xtopics.len(), 1);
1932        assert_eq!(out.pragma_dds_xtopics[0].version, "");
1933    }
1934
1935    // ---- §7.3.1.3 Const-Eval: nested #define-Refs ----
1936
1937    #[test]
1938    fn nested_define_two_hops() {
1939        // #define A 100; #define B A; const long x = B;
1940        // Erwartet: B → A → 100.
1941        let out = run("#define A 100\n#define B A\nconst long x = B;\n");
1942        assert!(out.contains("const long x = 100;"), "{out}");
1943    }
1944
1945    #[test]
1946    fn nested_define_three_hops() {
1947        let out = run("#define A 7\n\
1948             #define B A\n\
1949             #define C B\n\
1950             const long x = C;\n");
1951        assert!(out.contains("const long x = 7;"), "{out}");
1952    }
1953
1954    #[test]
1955    fn nested_define_with_arithmetic_expression() {
1956        let out = run("#define UNIT 8\n\
1957             #define BUF (UNIT * 4)\n\
1958             const long x = BUF;\n");
1959        // BUF wird zu (UNIT * 4) → (8 * 4); Caller-Eval macht das Ausrechnen.
1960        assert!(out.contains("(8 * 4)"), "{out}");
1961    }
1962
1963    #[test]
1964    fn nested_define_self_recursive_terminates() {
1965        // #define A A — pathologisch; expand_macros darf NICHT in
1966        // Endlosschleife laufen. Output muss "A" enthalten (Selbst-
1967        // Reference loest sich nicht auf).
1968        let out = run("#define A A\nconst long x = A;\n");
1969        assert!(out.contains("const long x = A;"), "{out}");
1970    }
1971
1972    #[test]
1973    fn nested_define_mutually_recursive_terminates() {
1974        // #define A B; #define B A; expand-Cap MUSS terminieren.
1975        let out = run("#define A B\n#define B A\nconst long x = A;\n");
1976        // Konkretes Resultat ist implementation-defined; wichtig: kein Hang.
1977        assert!(out.contains("const long x ="));
1978    }
1979
1980    #[test]
1981    fn pragma_dds_xtopics_unquoted_version_accepted() {
1982        let out = process("#pragma dds_xtopics version=1.3\nstruct S { long x; };\n");
1983        assert_eq!(out.pragma_dds_xtopics.len(), 1);
1984        assert_eq!(out.pragma_dds_xtopics[0].version, "1.3");
1985    }
1986}