calyx_frontend/
source_info.rs

1use itertools::Itertools;
2use std::{collections::HashMap, fmt::Display, num::NonZero, path::PathBuf};
3use thiserror::Error;
4
5type Word = u32;
6
7/// An identifier representing a given file path
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
9pub struct FileId(Word);
10
11impl Display for FileId {
12    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
13        self.0.fmt(f)
14    }
15}
16
17impl FileId {
18    pub fn new(id: Word) -> Self {
19        Self(id)
20    }
21}
22
23impl From<Word> for FileId {
24    fn from(value: Word) -> Self {
25        Self(value)
26    }
27}
28
29/// An identifier representing a location in the Calyx source code
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
31pub struct PositionId(Word);
32
33impl PositionId {
34    pub fn new(id: Word) -> Self {
35        Self(id)
36    }
37
38    pub fn value(&self) -> Word {
39        self.0
40    }
41}
42
43impl From<Word> for PositionId {
44    fn from(value: Word) -> Self {
45        Self(value)
46    }
47}
48
49impl Display for PositionId {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        self.0.fmt(f)
52    }
53}
54
55/// A newtype wrapping a line number
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub struct LineNum(NonZero<Word>);
58
59impl LineNum {
60    pub fn new(line: Word) -> Self {
61        Self(NonZero::new(line).expect("Line number must be non-zero"))
62    }
63    pub fn as_usize(&self) -> usize {
64        self.0.get() as usize
65    }
66}
67
68impl Display for LineNum {
69    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70        self.0.fmt(f)
71    }
72}
73
74#[derive(Error)]
75#[error("Line number cannot be zero")]
76pub struct LineNumCreationError;
77
78impl std::fmt::Debug for LineNumCreationError {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        std::fmt::Display::fmt(self, f)
81    }
82}
83
84impl TryFrom<Word> for LineNum {
85    type Error = LineNumCreationError;
86
87    fn try_from(value: Word) -> Result<Self, Self::Error> {
88        if value != 0 {
89            Ok(Self(NonZero::new(value).unwrap()))
90        } else {
91            Err(LineNumCreationError)
92        }
93    }
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Default)]
97pub struct SourceInfoTable {
98    /// map file ids to the file path, note that this does not contain file content
99    file_map: HashMap<FileId, PathBuf>,
100    /// maps position ids to their source locations.
101    position_map: HashMap<PositionId, SourceLocation>,
102}
103
104impl SourceInfoTable {
105    const HEADER: &str = "sourceinfo";
106
107    /// Looks up the path of the file with the given id.
108    ///
109    /// # Panics
110    /// Panics if the file id does not exist in the file map
111    pub fn lookup_file_path(&self, file: FileId) -> &PathBuf {
112        &self.file_map[&file]
113    }
114
115    /// Looks up the source location of the position with the given id.
116    ///
117    /// # Panics
118    /// Panics if the position id does not exist in the position map
119    pub fn lookup_position(&self, pos: PositionId) -> &SourceLocation {
120        &self.position_map[&pos]
121    }
122
123    /// Looks up the source location of the position with the given id. If no
124    /// such position exists, returns `None`
125    pub fn get_position(&self, pos: PositionId) -> Option<&SourceLocation> {
126        self.position_map.get(&pos)
127    }
128
129    /// Iterate over the stored file map, returning a tuple of references to the
130    /// file id and the path
131    pub fn iter_file_map(&self) -> impl Iterator<Item = (&FileId, &PathBuf)> {
132        self.file_map.iter()
133    }
134
135    /// Iterate over the paths of all files in the file map
136    pub fn iter_file_paths(&self) -> impl Iterator<Item = &PathBuf> {
137        self.file_map.values()
138    }
139
140    /// Iterate over all file ids in the file map
141    pub fn iter_file_ids(&self) -> impl Iterator<Item = FileId> + '_ {
142        self.file_map.keys().copied()
143    }
144
145    /// Iterate over the stored position map, returning a tuple of references to
146    /// the position id and the source location
147    pub fn iter_position_map(
148        &self,
149    ) -> impl Iterator<Item = (&PositionId, &SourceLocation)> {
150        self.position_map.iter()
151    }
152
153    /// Iterate over all position ids in the position map
154    pub fn iter_positions(&self) -> impl Iterator<Item = PositionId> + '_ {
155        self.position_map.keys().copied()
156    }
157
158    /// Iterate over the source locations in the position map
159    pub fn iter_source_locations(
160        &self,
161    ) -> impl Iterator<Item = &SourceLocation> {
162        self.position_map.values()
163    }
164
165    /// Adds a file to the file map with the given id
166    pub fn add_file(&mut self, file: FileId, path: PathBuf) {
167        self.file_map.insert(file, path);
168    }
169
170    /// Adds a file to the file map and generates a new file id
171    /// for it. If you want to add a file with a specific id, use
172    /// [`SourceInfoTable::add_file`]
173    pub fn push_file(&mut self, path: PathBuf) -> FileId {
174        // find the largest file id in the map
175        let max = self.iter_file_ids().max().unwrap_or(0.into());
176        let new = FileId(max.0 + 1);
177
178        self.add_file(new, path);
179        new
180    }
181    pub fn add_position(
182        &mut self,
183        pos: PositionId,
184        file: FileId,
185        line: LineNum,
186    ) {
187        self.position_map
188            .insert(pos, SourceLocation::new(file, line));
189    }
190
191    /// Adds a position to the position map and generates a new position id
192    /// for it. If you want to add a position with a specific id, use
193    /// [`SourceInfoTable::add_position`]
194    pub fn push_position(&mut self, file: FileId, line: LineNum) -> PositionId {
195        // find the largest position id in the map
196        let max = self.iter_positions().max().unwrap_or(0.into());
197        let new = PositionId(max.0 + 1);
198
199        self.add_position(new, file, line);
200        new
201    }
202
203    /// Creates a new empty source info table
204    pub fn new_empty() -> Self {
205        Self {
206            file_map: HashMap::new(),
207            position_map: HashMap::new(),
208        }
209    }
210
211    pub fn new<F, P>(files: F, positions: P) -> SourceInfoResult<Self>
212    where
213        F: IntoIterator<Item = (FileId, PathBuf)>,
214        P: IntoIterator<Item = (PositionId, FileId, LineNum)>,
215    {
216        let files = files.into_iter();
217        let positions = positions.into_iter();
218
219        let mut file_map = HashMap::with_capacity(
220            files.size_hint().1.unwrap_or(files.size_hint().0),
221        );
222        let mut position_map = HashMap::with_capacity(
223            positions.size_hint().1.unwrap_or(positions.size_hint().0),
224        );
225
226        for (file, path) in files {
227            if let Some(first_path) = file_map.insert(file, path) {
228                let inserted_path = &file_map[&file];
229                if &first_path != inserted_path {
230                    return Err(SourceInfoTableError::DuplicateFiles {
231                        id1: file,
232                        path1: first_path,
233                        path2: inserted_path.clone(),
234                    });
235                }
236            }
237        }
238
239        for (pos, file, line) in positions {
240            let source = SourceLocation::new(file, line);
241            if let Some(first_pos) = position_map.insert(pos, source) {
242                let inserted_position = &position_map[&pos];
243                if inserted_position != &first_pos {
244                    return Err(SourceInfoTableError::DuplicatePositions {
245                        pos,
246                        s1: first_pos,
247                        s2: position_map[&pos].clone(),
248                    });
249                }
250            }
251        }
252
253        Ok(SourceInfoTable {
254            file_map,
255            position_map,
256        })
257    }
258
259    pub fn serialize<W: std::io::Write>(
260        &self,
261        mut f: W,
262    ) -> Result<(), std::io::Error> {
263        writeln!(f, "{} #{{", Self::HEADER)?;
264
265        // write file table
266        writeln!(f, "FILES")?;
267        for (file, path) in self.file_map.iter().sorted_by_key(|(k, _)| **k) {
268            writeln!(f, "  {file}: {}", path.display())?;
269        }
270
271        // write the position table
272        writeln!(f, "POSITIONS")?;
273        for (position, SourceLocation { line, file }) in
274            self.position_map.iter().sorted_by_key(|(k, _)| **k)
275        {
276            writeln!(f, "  {position}: {file} {line}")?;
277        }
278
279        writeln!(f, "}}#")
280    }
281}
282
283#[derive(Debug, Clone, PartialEq, Eq)]
284pub struct SourceLocation {
285    pub file: FileId,
286    pub line: LineNum,
287}
288
289impl SourceLocation {
290    pub fn new(file: FileId, line: LineNum) -> Self {
291        Self { line, file }
292    }
293}
294#[derive(Error)]
295pub enum SourceInfoTableError {
296    #[error("Duplicate positions found in the metadata table. Position {pos} is defined multiple times:
297    1. file {}, line {}
298    2. file {}, line {}\n", s1.file, s1.line, s2.file, s2.line)]
299    DuplicatePositions {
300        pos: PositionId,
301        s1: SourceLocation,
302        s2: SourceLocation,
303    },
304
305    #[error("Duplicate files found in the metadata table. File id {id1} is defined multiple times:
306         1. {path1}
307         2. {path2}\n")]
308    DuplicateFiles {
309        id1: FileId,
310        path1: PathBuf,
311        path2: PathBuf,
312    },
313}
314
315impl std::fmt::Debug for SourceInfoTableError {
316    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
317        std::fmt::Display::fmt(&self, f)
318    }
319}
320
321pub type SourceInfoResult<T> = Result<T, SourceInfoTableError>;
322
323#[cfg(test)]
324mod tests {
325    use std::path::PathBuf;
326
327    use crate::{
328        parser::CalyxParser,
329        source_info::{FileId, LineNum, PositionId, SourceInfoTableError},
330    };
331
332    use super::SourceInfoTable;
333
334    #[test]
335    fn test_parse_metadata() {
336        let input_str = r#"sourceinfo #{
337    FILES
338        0: test.calyx
339        1: test2.calyx
340        2: test3.calyx
341    POSITIONS
342        0: 0 5
343        1: 0 1
344        2: 0 2
345}#"#;
346
347        let metadata = CalyxParser::parse_source_info_table(input_str)
348            .unwrap()
349            .unwrap();
350        let file = metadata.lookup_file_path(1.into());
351        assert_eq!(file, &PathBuf::from("test2.calyx"));
352
353        let pos = metadata.lookup_position(1.into());
354        assert_eq!(pos.file, 0.into());
355        assert_eq!(pos.line, LineNum::new(1));
356    }
357
358    #[test]
359    fn test_duplicate_file_parse() {
360        let input_str = r#"sourceinfo #{
361            FILES
362                0: test.calyx
363                0: test2.calyx
364                2: test3.calyx
365            POSITIONS
366                0: 0 5
367                1: 0 1
368                2: 0 2
369        }#"#;
370        let metadata = CalyxParser::parse_source_info_table(input_str).unwrap();
371
372        assert!(metadata.is_err());
373        let err = metadata.unwrap_err();
374        assert!(matches!(&err, SourceInfoTableError::DuplicateFiles { .. }));
375        if let SourceInfoTableError::DuplicateFiles { id1, .. } = &err {
376            assert_eq!(id1, &FileId::new(0))
377        } else {
378            unreachable!()
379        }
380    }
381
382    #[test]
383    fn test_duplicate_position_parse() {
384        let input_str = r#"sourceinfo #{
385            FILES
386                0: test.calyx
387                1: test2.calyx
388                2: test3.calyx
389            POSITIONS
390                0: 0 5
391                0: 0 1
392                2: 0 2
393        }#"#;
394        let metadata = CalyxParser::parse_source_info_table(input_str).unwrap();
395
396        assert!(metadata.is_err());
397        let err = metadata.unwrap_err();
398        assert!(matches!(
399            &err,
400            SourceInfoTableError::DuplicatePositions { .. }
401        ));
402        if let SourceInfoTableError::DuplicatePositions { pos, .. } = err {
403            assert_eq!(pos, PositionId::new(0))
404        } else {
405            unreachable!()
406        }
407    }
408
409    #[test]
410    fn test_serialize() {
411        let mut metadata = SourceInfoTable::new_empty();
412        metadata.add_file(0.into(), "test.calyx".into());
413        metadata.add_file(1.into(), "test2.calyx".into());
414        metadata.add_file(2.into(), "test3.calyx".into());
415
416        metadata.add_position(0.into(), 0.into(), LineNum::new(1));
417        metadata.add_position(1.into(), 1.into(), LineNum::new(2));
418        metadata.add_position(150.into(), 2.into(), LineNum::new(148));
419
420        let mut serialized_str = vec![];
421        metadata.serialize(&mut serialized_str).unwrap();
422        let serialized_str = String::from_utf8(serialized_str).unwrap();
423
424        let parsed_metadata =
425            CalyxParser::parse_source_info_table(&serialized_str)
426                .unwrap()
427                .unwrap();
428
429        assert_eq!(metadata, parsed_metadata)
430    }
431}