calyx_frontend/
source_info.rs

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