Skip to main content

zerodds_idl/preprocessor/
source_map.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Source-Map: mappt expandierte Output-Positionen auf
4//! `(Datei, Original-Offset)` (T6.3).
5//!
6//! Nach Preprocessor-Lauf zeigt der Lexer auf Bytes des `expanded`-
7//! Strings. Diagnostiken muessen aber auf die Original-Quelldatei
8//! zeigen (eine Zeile aus `inc.idl` ist im expanded-Output an einer
9//! voellig anderen Position).
10//!
11//! [`SourceMap`] sammelt Segmente: jedes Segment ist ein Output-
12//! Bereich `[output_start, output_start + length)`, der aus einer
13//! Datei `file_id` ab `original_offset` stammt. `lookup(output_pos)`
14//! findet das passende Segment und rechnet die ursprungsgetreue
15//! Position aus.
16
17/// Stable Identifier fuer eine Datei (lokal innerhalb einer
18/// SourceMap).
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub struct FileId(pub u32);
21
22/// Originale Quell-Position nach Lookup.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct SourceLocation {
25    pub file_id: FileId,
26    pub byte_offset: usize,
27}
28
29/// Source-Map.
30#[derive(Debug, Clone, Default)]
31pub struct SourceMap {
32    files: Vec<String>,
33    segments: Vec<Segment>,
34}
35
36#[derive(Debug, Clone, Copy)]
37struct Segment {
38    output_start: usize,
39    length: usize,
40    file_id: FileId,
41    original_offset: usize,
42}
43
44impl SourceMap {
45    /// Konstruiert eine leere SourceMap.
46    #[must_use]
47    pub fn new() -> Self {
48        Self::default()
49    }
50
51    /// Registriert eine Datei und liefert ihre stabile [`FileId`].
52    pub fn add_file(&mut self, name: &str) -> FileId {
53        if let Some(idx) = self.files.iter().position(|f| f == name) {
54            return FileId(idx as u32);
55        }
56        let id = FileId(self.files.len() as u32);
57        self.files.push(name.to_string());
58        id
59    }
60
61    /// Schreibt ein Segment ein. `output_start` und `length` beziehen
62    /// sich auf den expanded-String; `original_offset` zeigt auf die
63    /// erste Byte-Position in der `file_id`-Source.
64    pub fn record_segment(
65        &mut self,
66        output_start: usize,
67        length: usize,
68        file_id: FileId,
69        original_offset: usize,
70    ) {
71        if length == 0 {
72            return;
73        }
74        self.segments.push(Segment {
75            output_start,
76            length,
77            file_id,
78            original_offset,
79        });
80    }
81
82    /// Loest eine expanded-Position auf die Original-(Datei,Offset).
83    /// Liefert `None`, wenn die Position nicht in einem registrierten
84    /// Segment liegt (z.B. zwischen zwei Segmenten oder hinter dem
85    /// letzten).
86    #[must_use]
87    pub fn lookup(&self, output_pos: usize) -> Option<SourceLocation> {
88        for s in &self.segments {
89            if output_pos >= s.output_start && output_pos < s.output_start + s.length {
90                let delta = output_pos - s.output_start;
91                return Some(SourceLocation {
92                    file_id: s.file_id,
93                    byte_offset: s.original_offset + delta,
94                });
95            }
96        }
97        None
98    }
99
100    /// Datei-Name fuer eine [`FileId`].
101    #[must_use]
102    pub fn file_name(&self, id: FileId) -> Option<&str> {
103        self.files.get(id.0 as usize).map(String::as_str)
104    }
105
106    /// Anzahl registrierter Segmente.
107    #[must_use]
108    pub fn segment_count(&self) -> usize {
109        self.segments.len()
110    }
111
112    /// Anzahl registrierter Dateien.
113    #[must_use]
114    pub fn file_count(&self) -> usize {
115        self.files.len()
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    #![allow(clippy::expect_used)]
122    use super::*;
123
124    #[test]
125    fn add_file_returns_stable_id_for_same_name() {
126        let mut m = SourceMap::new();
127        let a = m.add_file("foo.idl");
128        let b = m.add_file("foo.idl");
129        assert_eq!(a, b);
130    }
131
132    #[test]
133    fn add_file_returns_distinct_ids_for_different_names() {
134        let mut m = SourceMap::new();
135        let a = m.add_file("a.idl");
136        let b = m.add_file("b.idl");
137        assert_ne!(a, b);
138    }
139
140    #[test]
141    fn lookup_returns_none_for_empty_map() {
142        let m = SourceMap::new();
143        assert!(m.lookup(0).is_none());
144    }
145
146    #[test]
147    fn lookup_finds_position_in_recorded_segment() {
148        let mut m = SourceMap::new();
149        let id = m.add_file("a.idl");
150        m.record_segment(10, 5, id, 100);
151        let loc = m.lookup(12).expect("in segment");
152        assert_eq!(loc.file_id, id);
153        assert_eq!(loc.byte_offset, 102);
154    }
155
156    #[test]
157    fn lookup_beyond_last_segment_is_none() {
158        let mut m = SourceMap::new();
159        let id = m.add_file("a.idl");
160        m.record_segment(0, 5, id, 0);
161        assert!(m.lookup(100).is_none());
162    }
163
164    #[test]
165    fn record_segment_with_zero_length_is_noop() {
166        let mut m = SourceMap::new();
167        let id = m.add_file("a.idl");
168        m.record_segment(0, 0, id, 0);
169        assert_eq!(m.segment_count(), 0);
170    }
171
172    #[test]
173    fn file_name_resolves_back() {
174        let mut m = SourceMap::new();
175        let id = m.add_file("foo.idl");
176        assert_eq!(m.file_name(id), Some("foo.idl"));
177    }
178
179    #[test]
180    fn file_count_tracks_unique_files() {
181        let mut m = SourceMap::new();
182        m.add_file("a");
183        m.add_file("b");
184        m.add_file("a"); // duplicate
185        assert_eq!(m.file_count(), 2);
186    }
187
188    #[test]
189    fn lookup_works_across_multiple_segments() {
190        let mut m = SourceMap::new();
191        let a = m.add_file("a");
192        let b = m.add_file("b");
193        m.record_segment(0, 10, a, 0);
194        m.record_segment(10, 20, b, 50);
195        let l1 = m.lookup(5).expect("in a");
196        assert_eq!(l1.file_id, a);
197        assert_eq!(l1.byte_offset, 5);
198        let l2 = m.lookup(15).expect("in b");
199        assert_eq!(l2.file_id, b);
200        assert_eq!(l2.byte_offset, 55);
201    }
202}