calyx_frontend/
source_info.rs

1use itertools::Itertools;
2use std::{
3    collections::HashMap,
4    fmt::Display,
5    fs::File,
6    io::Read,
7    num::{NonZero, TryFromIntError},
8    path::PathBuf,
9};
10use thiserror::Error;
11
12type Word = u32;
13
14/// An identifier representing a given file path
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
16pub struct FileId(Word);
17
18impl Display for FileId {
19    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20        self.0.fmt(f)
21    }
22}
23
24impl FileId {
25    pub fn new(id: Word) -> Self {
26        Self(id)
27    }
28}
29
30impl From<Word> for FileId {
31    fn from(value: Word) -> Self {
32        Self(value)
33    }
34}
35
36/// An identifier representing a location in the Calyx source code
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
38pub struct PositionId(Word);
39
40impl PositionId {
41    pub fn new(id: Word) -> Self {
42        Self(id)
43    }
44
45    pub fn value(&self) -> Word {
46        self.0
47    }
48}
49
50impl From<Word> for PositionId {
51    fn from(value: Word) -> Self {
52        Self(value)
53    }
54}
55
56impl TryFrom<u64> for PositionId {
57    type Error = TryFromIntError;
58
59    fn try_from(value: u64) -> Result<Self, Self::Error> {
60        let v: u32 = value.try_into()?;
61        Ok(Self(v))
62    }
63}
64
65impl Display for PositionId {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        self.0.fmt(f)
68    }
69}
70
71/// A newtype wrapping a line number
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub struct LineNum(NonZero<Word>);
74
75impl LineNum {
76    pub fn new(line: Word) -> Self {
77        Self(NonZero::new(line).expect("Line number must be non-zero"))
78    }
79    pub fn as_usize(&self) -> usize {
80        self.0.get() as usize
81    }
82}
83
84impl Display for LineNum {
85    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86        self.0.fmt(f)
87    }
88}
89
90#[derive(Error)]
91#[error("Line number cannot be zero")]
92pub struct LineNumCreationError;
93
94impl std::fmt::Debug for LineNumCreationError {
95    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        std::fmt::Display::fmt(self, f)
97    }
98}
99
100impl TryFrom<Word> for LineNum {
101    type Error = LineNumCreationError;
102
103    fn try_from(value: Word) -> Result<Self, Self::Error> {
104        if value != 0 {
105            Ok(Self(NonZero::new(value).unwrap()))
106        } else {
107            Err(LineNumCreationError)
108        }
109    }
110}
111
112/// An ID in the source map labelling some memory location
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
114pub struct MemoryLocationId(Word);
115
116impl TryFrom<u64> for MemoryLocationId {
117    type Error = TryFromIntError;
118
119    fn try_from(value: u64) -> Result<Self, Self::Error> {
120        let v: u32 = value.try_into()?;
121        Ok(Self(v))
122    }
123}
124
125impl From<Word> for MemoryLocationId {
126    fn from(value: Word) -> Self {
127        Self(value)
128    }
129}
130
131impl Display for MemoryLocationId {
132    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> core::fmt::Result {
133        <Word as Display>::fmt(&self.0, f)
134    }
135}
136
137#[derive(Debug, Clone, PartialEq, Eq)]
138pub struct MemoryLocation {
139    pub cell: String,
140    pub address: Vec<usize>,
141}
142
143/// An ID in the source map labelling a set of mappings from variable names to memory locations
144#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
145pub struct VariableAssignmentId(Word);
146
147impl Display for VariableAssignmentId {
148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> core::fmt::Result {
149        <Word as Display>::fmt(&self.0, f)
150    }
151}
152impl TryFrom<u64> for VariableAssignmentId {
153    type Error = TryFromIntError;
154
155    fn try_from(value: u64) -> Result<Self, Self::Error> {
156        let v: u32 = value.try_into()?;
157        Ok(Self(v))
158    }
159}
160#[derive(Debug, Clone, PartialEq, Eq, Default)]
161pub struct SourceInfoTable {
162    /// map file ids to the file path, note that this does not contain file content
163    file_map: HashMap<FileId, PathBuf>,
164    /// maps position ids to their source locations.
165    position_map: HashMap<PositionId, SourceLocation>,
166    /// assigns ids to locations in memories and registers
167    mem_location_map: HashMap<MemoryLocationId, MemoryLocation>,
168    /// assigns ids to collections of variable -> location mappings
169    variable_assignment_map:
170        HashMap<VariableAssignmentId, HashMap<String, MemoryLocationId>>,
171    /// collects the mapping from positions representing a point in the control
172    /// program to the set of variable assignments for that position
173    position_state_map: HashMap<PositionId, VariableAssignmentId>,
174}
175
176impl SourceInfoTable {
177    const HEADER: &str = "sourceinfo";
178
179    /// Looks up the path of the file with the given id.
180    ///
181    /// # Panics
182    /// Panics if the file id does not exist in the file map
183    pub fn lookup_file_path(&self, file: FileId) -> &PathBuf {
184        &self.file_map[&file]
185    }
186
187    /// Looks up the source location of the position with the given id.
188    ///
189    /// # Panics
190    /// Panics if the position id does not exist in the position map
191    pub fn lookup_position(&self, pos: PositionId) -> &SourceLocation {
192        &self.position_map[&pos]
193    }
194
195    /// Looks up the source location of the position with the given id. If no
196    /// such position exists, returns `None`
197    pub fn get_position(&self, pos: PositionId) -> Option<&SourceLocation> {
198        self.position_map.get(&pos)
199    }
200
201    /// Iterate over the stored file map, returning a tuple of references to the
202    /// file id and the path
203    pub fn iter_file_map(&self) -> impl Iterator<Item = (&FileId, &PathBuf)> {
204        self.file_map.iter()
205    }
206
207    /// Iterate over the paths of all files in the file map
208    pub fn iter_file_paths(&self) -> impl Iterator<Item = &PathBuf> {
209        self.file_map.values()
210    }
211
212    /// Iterate over all file ids in the file map
213    pub fn iter_file_ids(&self) -> impl Iterator<Item = FileId> + '_ {
214        self.file_map.keys().copied()
215    }
216
217    /// Iterate over the stored position map, returning a tuple of references to
218    /// the position id and the source location
219    pub fn iter_position_map(
220        &self,
221    ) -> impl Iterator<Item = (&PositionId, &SourceLocation)> {
222        self.position_map.iter()
223    }
224
225    /// Iterate over all position ids in the position map
226    pub fn iter_positions(&self) -> impl Iterator<Item = PositionId> + '_ {
227        self.position_map.keys().copied()
228    }
229
230    /// Iterate over the source locations in the position map
231    pub fn iter_source_locations(
232        &self,
233    ) -> impl Iterator<Item = &SourceLocation> {
234        self.position_map.values()
235    }
236
237    /// Adds a file to the file map with the given id
238    pub fn add_file(&mut self, file: FileId, path: PathBuf) {
239        self.file_map.insert(file, path);
240    }
241
242    /// Adds a file to the file map and generates a new file id
243    /// for it. If you want to add a file with a specific id, use
244    /// [`SourceInfoTable::add_file`]
245    pub fn push_file(&mut self, path: PathBuf) -> FileId {
246        // find the largest file id in the map
247        let max = self.iter_file_ids().max().unwrap_or(0.into());
248        let new = FileId(max.0 + 1);
249
250        self.add_file(new, path);
251        new
252    }
253    pub fn add_position(
254        &mut self,
255        pos: PositionId,
256        file: FileId,
257        line: LineNum,
258    ) {
259        self.position_map
260            .insert(pos, SourceLocation::new(file, line));
261    }
262
263    /// Adds a position to the position map and generates a new position id
264    /// for it. If you want to add a position with a specific id, use
265    /// [`SourceInfoTable::add_position`]
266    pub fn push_position(&mut self, file: FileId, line: LineNum) -> PositionId {
267        // find the largest position id in the map
268        let max = self.iter_positions().max().unwrap_or(0.into());
269        let new = PositionId(max.0 + 1);
270
271        self.add_position(new, file, line);
272        new
273    }
274
275    pub fn add_location(&mut self, id: MemoryLocationId, info: MemoryLocation) {
276        self.mem_location_map.insert(id, info);
277    }
278
279    /// Creates a new empty source info table
280    pub fn new_empty() -> Self {
281        Self {
282            file_map: HashMap::new(),
283            position_map: HashMap::new(),
284            mem_location_map: HashMap::new(),
285            variable_assignment_map: HashMap::new(),
286            position_state_map: HashMap::new(),
287        }
288    }
289
290    /// A wrapper function to construct a source a source map containing only
291    /// files and positions. If an empty map is needed use [SourceInfoTable::new_empty]
292    pub fn new_minimal(
293        files: impl IntoIterator<Item = (FileId, PathBuf)>,
294        positions: impl IntoIterator<Item = (PositionId, FileId, LineNum)>,
295    ) -> SourceInfoResult<Self> {
296        // the compiler needs some concrete types here even though the input is
297        // all empty
298        let loc: Vec<(MemoryLocationId, MemoryLocation)> = vec![];
299        let states: Vec<(PositionId, VariableAssignmentId)> = vec![];
300        let variable_assigns: Vec<(
301            VariableAssignmentId,
302            Vec<(String, MemoryLocationId)>,
303        )> = vec![];
304
305        Self::new(files, positions, loc, variable_assigns, states)
306    }
307
308    // this is awful
309    pub fn new(
310        files: impl IntoIterator<Item = (FileId, PathBuf)>,
311        positions: impl IntoIterator<Item = (PositionId, FileId, LineNum)>,
312        locations: impl IntoIterator<Item = (MemoryLocationId, MemoryLocation)>,
313        variable_assigns: impl IntoIterator<
314            Item = (
315                VariableAssignmentId,
316                impl IntoIterator<Item = (String, MemoryLocationId)>,
317            ),
318        >,
319        states: impl IntoIterator<Item = (PositionId, VariableAssignmentId)>,
320    ) -> SourceInfoResult<Self> {
321        let files = files.into_iter();
322        let positions = positions.into_iter();
323        let locations = locations.into_iter();
324        let vars = variable_assigns.into_iter();
325        let states = states.into_iter();
326
327        let mut file_map = HashMap::with_capacity(
328            files.size_hint().1.unwrap_or(files.size_hint().0),
329        );
330        let mut position_map = HashMap::with_capacity(
331            positions.size_hint().1.unwrap_or(positions.size_hint().0),
332        );
333
334        let mut memory_location_map: HashMap<MemoryLocationId, MemoryLocation> =
335            HashMap::new();
336
337        let mut variable_map = HashMap::new();
338        let mut state_map = HashMap::new();
339
340        for (file, path) in files {
341            if let Some(first_path) = file_map.insert(file, path) {
342                let inserted_path = &file_map[&file];
343                if &first_path != inserted_path {
344                    return Err(SourceInfoTableError::DuplicateFiles {
345                        id1: file,
346                        path1: first_path,
347                        path2: inserted_path.clone(),
348                    });
349                }
350            }
351        }
352
353        for (pos, file, line) in positions {
354            let source = SourceLocation::new(file, line);
355            if let Some(first_pos) = position_map.insert(pos, source) {
356                let inserted_position = &position_map[&pos];
357                if inserted_position != &first_pos {
358                    return Err(SourceInfoTableError::DuplicatePositions {
359                        pos,
360                        s1: first_pos,
361                        s2: position_map[&pos].clone(),
362                    });
363                }
364            }
365        }
366
367        for (id, loc) in locations {
368            if memory_location_map.insert(id, loc).is_some() {
369                // duplictate entry error
370                return Err(SourceInfoTableError::DuplicateMemoryIdentifiers {
371                    id,
372                });
373            }
374        }
375
376        for (assign_label, assigns) in vars {
377            let mut mapping = HashMap::new();
378            for (name, location) in assigns {
379                // this is to avoid copying the string in all cases since we
380                // would only need it when emitting the error. Clippy doesn't
381                // like this for good reasons and while I suspect it may be
382                // possible using the entry api, I think this is clearer so I'm
383                // just suppressing the warning and writing this very long
384                // comment about it instead.
385                #[allow(clippy::map_entry)]
386                if !memory_location_map.contains_key(&location) {
387                    // unknown memory location
388                    return Err(SourceInfoTableError::UnknownMemoryId {
389                        id: location,
390                    });
391                } else if mapping.contains_key(&name) {
392                    // duplicate entries
393                    return Err(
394                        SourceInfoTableError::DuplicateVariableAssignments {
395                            id: assign_label,
396                            var: name,
397                        },
398                    );
399                } else {
400                    mapping.insert(name, location);
401                }
402            }
403            if variable_map.insert(assign_label, mapping).is_some() {
404                // duplicate entries
405                return Err(SourceInfoTableError::DuplicateVariableMappings {
406                    id: assign_label,
407                });
408            };
409        }
410
411        for (pos_id, var_id) in states {
412            if !variable_map.contains_key(&var_id) {
413                // unknown var
414                return Err(SourceInfoTableError::UnknownVariableMapping {
415                    id: var_id,
416                });
417            }
418            if state_map.insert(pos_id, var_id).is_some() {
419                // duplicate
420                return Err(SourceInfoTableError::DuplicatePosStateMappings {
421                    id: pos_id,
422                });
423            }
424        }
425
426        Ok(SourceInfoTable {
427            file_map,
428            position_map,
429            mem_location_map: memory_location_map,
430            variable_assignment_map: variable_map,
431            position_state_map: state_map,
432        })
433    }
434
435    pub fn serialize<W: std::io::Write>(
436        &self,
437        mut f: W,
438    ) -> Result<(), std::io::Error> {
439        writeln!(f, "{} #{{", Self::HEADER)?;
440
441        // write file table
442        writeln!(f, "FILES")?;
443        for (file, path) in self.file_map.iter().sorted_by_key(|(k, _)| **k) {
444            writeln!(f, "  {file}: {}", path.display())?;
445        }
446
447        // write the position table
448        writeln!(f, "POSITIONS")?;
449        for (position, SourceLocation { line, file }) in
450            self.position_map.iter().sorted_by_key(|(k, _)| **k)
451        {
452            writeln!(f, "  {position}: {file} {line}")?;
453        }
454
455        writeln!(f, "}}#")
456    }
457
458    /// Attempt to lookup the line that a given position points to. Returns an error in
459    /// cases when the position does not exist, the file is unavailable, or the file
460    /// does not contain the indicated line.
461    pub fn get_position_string(
462        &self,
463        pos: PositionId,
464    ) -> Result<String, SourceLookupError> {
465        let Some(src_loc) = self.get_position(pos) else {
466            return Err(SourceLookupError::MissingPosition(pos));
467        };
468        // this will panic if the file doesn't exist but that would imply the table has
469        // incorrect information in it
470        let file_path = self.lookup_file_path(src_loc.file);
471
472        let Ok(mut file) = File::open(file_path) else {
473            return Err(SourceLookupError::MissingFile(file_path));
474        };
475
476        let mut file_contents = String::new();
477
478        match file.read_to_string(&mut file_contents) {
479            Ok(_) => {}
480            Err(_) => {
481                return Err(SourceLookupError::MissingFile(file_path));
482            }
483        }
484
485        let Some(line) = file_contents.lines().nth(src_loc.line.as_usize() - 1)
486        else {
487            return Err(SourceLookupError::MissingLine {
488                file: file_path,
489                line: src_loc.line.as_usize(),
490            });
491        };
492
493        Ok(String::from(line))
494    }
495}
496
497#[derive(Debug, Clone, PartialEq, Eq)]
498pub struct SourceLocation {
499    pub file: FileId,
500    pub line: LineNum,
501}
502
503impl SourceLocation {
504    pub fn new(file: FileId, line: LineNum) -> Self {
505        Self { line, file }
506    }
507}
508#[derive(Error)]
509pub enum SourceInfoTableError {
510    #[error("Duplicate positions found in the metadata table. Position {pos} is defined multiple times:
511    1. file {}, line {}
512    2. file {}, line {}\n", s1.file, s1.line, s2.file, s2.line)]
513    DuplicatePositions {
514        pos: PositionId,
515        s1: SourceLocation,
516        s2: SourceLocation,
517    },
518
519    #[error("Duplicate files found in the metadata table. File id {id1} is defined multiple times:
520         1. {path1}
521         2. {path2}\n")]
522    DuplicateFiles {
523        id1: FileId,
524        path1: PathBuf,
525        path2: PathBuf,
526    },
527
528    #[error("Duplicate definitions for memory location {id}")]
529    DuplicateMemoryIdentifiers { id: MemoryLocationId },
530
531    #[error("Memory location {id} is referenced but never defined")]
532    UnknownMemoryId { id: MemoryLocationId },
533
534    #[error("Variable mapping {id} is referenced but never defined")]
535    UnknownVariableMapping { id: VariableAssignmentId },
536
537    #[error("Duplicate definitions for variable mapping {id}")]
538    DuplicateVariableMappings { id: VariableAssignmentId },
539
540    #[error(
541        "Duplicate definitions for variable mapping associated with position {id}"
542    )]
543    DuplicatePosStateMappings { id: PositionId },
544
545    #[error(
546        "In variable mapping {id} the variable '{var}' has multiple definitions"
547    )]
548    DuplicateVariableAssignments {
549        id: VariableAssignmentId,
550        var: String,
551    },
552}
553
554/// Any error that can emerge while attempting to pull the actual line of text that a
555/// source line points to
556#[derive(Error, Debug)]
557pub enum SourceLookupError<'a> {
558    #[error("unable to open file {0}")]
559    MissingFile(&'a PathBuf),
560    #[error("file {file} does not have a line {line}")]
561    MissingLine { file: &'a PathBuf, line: usize },
562    #[error("position id {0} does not exist")]
563    MissingPosition(PositionId),
564}
565
566impl std::fmt::Debug for SourceInfoTableError {
567    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
568        std::fmt::Display::fmt(&self, f)
569    }
570}
571
572pub type SourceInfoResult<T> = Result<T, SourceInfoTableError>;
573
574#[cfg(test)]
575mod tests {
576    use std::path::PathBuf;
577
578    use crate::{
579        parser::CalyxParser,
580        source_info::{
581            FileId, LineNum, MemoryLocationId, PositionId,
582            SourceInfoTableError, VariableAssignmentId,
583        },
584    };
585
586    use super::SourceInfoTable;
587
588    #[test]
589    fn test_parse_metadata() {
590        let input_str = r#"sourceinfo #{
591    FILES
592        0: test.calyx
593        1: test2.calyx
594        2: test3.calyx
595    POSITIONS
596        0: 0 5
597        1: 0 1
598        2: 0 2
599    MEMORY_LOCATIONS
600        0: main.reg1
601        1: main.reg2
602        2: main.mem1 [1,4]
603    VARIABLE_ASSIGNMENTS
604        0: {
605            x: 0
606            y: 1
607            z: 2
608        }
609        1: {
610            q: 0
611        }
612    POSITION_STATE_MAP
613        0: 0
614        2: 1
615}#"#;
616
617        let metadata = CalyxParser::parse_source_info_table(input_str)
618            .unwrap()
619            .unwrap();
620        let file = metadata.lookup_file_path(1.into());
621        assert_eq!(file, &PathBuf::from("test2.calyx"));
622
623        let pos = metadata.lookup_position(1.into());
624        assert_eq!(pos.file, 0.into());
625        assert_eq!(pos.line, LineNum::new(1));
626    }
627
628    #[test]
629    fn test_undefined_mem_loc() {
630        let input_str = r#"sourceinfo #{
631    FILES
632        0: test.calyx
633    POSITIONS
634        0: 0 5
635        1: 0 1
636        2: 0 2
637    MEMORY_LOCATIONS
638        0: main.reg1
639        2: main.mem1 [1,4]
640    VARIABLE_ASSIGNMENTS
641        0: {
642            x: 0
643            y: 1
644            z: 2
645        }
646        1: {
647            q: 0
648        }
649    POSITION_STATE_MAP
650        0: 0
651        2: 1
652}#"#;
653        let metadata = CalyxParser::parse_source_info_table(input_str).unwrap();
654        assert!(metadata.is_err());
655        let err = metadata.unwrap_err();
656        assert!(matches!(
657            &err,
658            SourceInfoTableError::UnknownMemoryId {
659                id: MemoryLocationId(1)
660            }
661        ));
662    }
663
664    #[test]
665    fn test_undefined_variable() {
666        let input_str = r#"sourceinfo #{
667    FILES
668        0: test.calyx
669    POSITIONS
670        0: 0 5
671        1: 0 1
672        2: 0 2
673    MEMORY_LOCATIONS
674        0: main.reg1
675        1: main.reg2
676        2: main.mem1 [1,4]
677    VARIABLE_ASSIGNMENTS
678        0: {
679            x: 0
680            y: 1
681            z: 2
682        }
683        1: {
684            q: 0
685        }
686    POSITION_STATE_MAP
687        0: 0
688        2: 2
689}#"#;
690        let metadata = CalyxParser::parse_source_info_table(input_str).unwrap();
691        assert!(metadata.is_err());
692        let err = metadata.unwrap_err();
693
694        assert!(matches!(
695            &err,
696            SourceInfoTableError::UnknownVariableMapping {
697                id: VariableAssignmentId(2)
698            }
699        ));
700    }
701
702    #[test]
703    fn test_duplicate_variable_maps() {
704        let input_str = r#"sourceinfo #{
705    FILES
706        0: test.calyx
707    POSITIONS
708        0: 0 5
709        1: 0 1
710        2: 0 2
711    MEMORY_LOCATIONS
712        0: main.reg1
713        1: main.reg2
714        2: main.mem1 [1,4]
715    VARIABLE_ASSIGNMENTS
716        0: {
717            x: 0
718            y: 1
719            z: 2
720        }
721        1: {
722            q: 0
723        }
724        1: {
725            a: 0
726        }
727    POSITION_STATE_MAP
728        0: 0
729        2: 1
730}#"#;
731        let metadata = CalyxParser::parse_source_info_table(input_str).unwrap();
732        assert!(metadata.is_err());
733        let err = metadata.unwrap_err();
734        assert!(matches!(
735            &err,
736            SourceInfoTableError::DuplicateVariableMappings {
737                id: VariableAssignmentId(1)
738            }
739        ));
740    }
741
742    #[test]
743    fn test_duplicate_variable_assignment() {
744        let input_str = r#"sourceinfo #{
745    FILES
746        0: test.calyx
747    POSITIONS
748        0: 0 5
749        1: 0 1
750        2: 0 2
751    MEMORY_LOCATIONS
752        0: main.reg1
753        1: main.reg2
754        2: main.mem1 [1,4]
755    VARIABLE_ASSIGNMENTS
756        0: {
757            x: 0
758            y: 1
759            z: 2
760        }
761        1: {
762            q: 0
763            q: 1
764        }
765    POSITION_STATE_MAP
766        0: 0
767        2: 1
768}#"#;
769        let metadata = CalyxParser::parse_source_info_table(input_str).unwrap();
770        assert!(metadata.is_err());
771        let err = metadata.unwrap_err();
772        assert!(matches!(
773            &err,
774            SourceInfoTableError::DuplicateVariableAssignments {
775                id: VariableAssignmentId(1),
776                var
777            } if var == "q"
778        ));
779    }
780
781    #[test]
782    fn test_duplicate_mem_def() {
783        let input_str = r#"sourceinfo #{
784    FILES
785        0: test.calyx
786    POSITIONS
787        0: 0 5
788        1: 0 1
789        2: 0 2
790    MEMORY_LOCATIONS
791        0: main.reg1
792        1: main.reg2
793        1: main.mem1 [1,4]
794    VARIABLE_ASSIGNMENTS
795        0: {
796            x: 0
797            y: 1
798            z: 2
799        }
800        1: {
801            q: 0
802        }
803    POSITION_STATE_MAP
804        0: 0
805        2: 1
806}#"#;
807        let metadata = CalyxParser::parse_source_info_table(input_str).unwrap();
808        assert!(metadata.is_err());
809        let err = metadata.unwrap_err();
810        assert!(matches!(
811            &err,
812            SourceInfoTableError::DuplicateMemoryIdentifiers {
813                id: MemoryLocationId(1)
814            }
815        ));
816    }
817
818    #[test]
819    fn test_duplicate_pos_state() {
820        let input_str = r#"sourceinfo #{
821    FILES
822        0: test.calyx
823    POSITIONS
824        0: 0 5
825        1: 0 1
826        2: 0 2
827    MEMORY_LOCATIONS
828        0: main.reg1
829        1: main.reg2
830        2: main.mem1 [1,4]
831    VARIABLE_ASSIGNMENTS
832        0: {
833            x: 0
834            y: 1
835            z: 2
836        }
837        1: {
838            q: 0
839        }
840    POSITION_STATE_MAP
841        0: 0
842        0: 1
843}#"#;
844        let metadata = CalyxParser::parse_source_info_table(input_str).unwrap();
845        assert!(metadata.is_err());
846        let err = metadata.unwrap_err();
847        assert!(matches!(
848            &err,
849            SourceInfoTableError::DuplicatePosStateMappings {
850                id: PositionId(0)
851            }
852        ));
853    }
854
855    #[test]
856    fn test_duplicate_file_parse() {
857        let input_str = r#"sourceinfo #{
858            FILES
859                0: test.calyx
860                0: test2.calyx
861                2: test3.calyx
862            POSITIONS
863                0: 0 5
864                1: 0 1
865                2: 0 2
866        }#"#;
867        let metadata = CalyxParser::parse_source_info_table(input_str).unwrap();
868
869        assert!(metadata.is_err());
870        let err = metadata.unwrap_err();
871        assert!(matches!(&err, SourceInfoTableError::DuplicateFiles { .. }));
872        if let SourceInfoTableError::DuplicateFiles { id1, .. } = &err {
873            assert_eq!(id1, &FileId::new(0))
874        } else {
875            unreachable!()
876        }
877    }
878
879    #[test]
880    fn test_duplicate_position_parse() {
881        let input_str = r#"sourceinfo #{
882            FILES
883                0: test.calyx
884                1: test2.calyx
885                2: test3.calyx
886            POSITIONS
887                0: 0 5
888                0: 0 1
889                2: 0 2
890        }#"#;
891        let metadata = CalyxParser::parse_source_info_table(input_str).unwrap();
892
893        assert!(metadata.is_err());
894        let err = metadata.unwrap_err();
895        assert!(matches!(
896            &err,
897            SourceInfoTableError::DuplicatePositions { .. }
898        ));
899        if let SourceInfoTableError::DuplicatePositions { pos, .. } = err {
900            assert_eq!(pos, PositionId::new(0))
901        } else {
902            unreachable!()
903        }
904    }
905
906    #[test]
907    fn test_serialize() {
908        let mut metadata = SourceInfoTable::new_empty();
909        metadata.add_file(0.into(), "test.calyx".into());
910        metadata.add_file(1.into(), "test2.calyx".into());
911        metadata.add_file(2.into(), "test3.calyx".into());
912
913        metadata.add_position(0.into(), 0.into(), LineNum::new(1));
914        metadata.add_position(1.into(), 1.into(), LineNum::new(2));
915        metadata.add_position(150.into(), 2.into(), LineNum::new(148));
916
917        let mut serialized_str = vec![];
918        metadata.serialize(&mut serialized_str).unwrap();
919        let serialized_str = String::from_utf8(serialized_str).unwrap();
920
921        let parsed_metadata =
922            CalyxParser::parse_source_info_table(&serialized_str)
923                .unwrap()
924                .unwrap();
925
926        assert_eq!(metadata, parsed_metadata)
927    }
928}