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    /// Attempts to look up the variable mapping associated with a given
280    /// position, if such a mapping exists
281    pub fn get_variable_mapping(
282        &self,
283        pos: PositionId,
284    ) -> Option<&HashMap<String, MemoryLocationId>> {
285        self.position_state_map
286            .get(&pos)
287            .and_then(|x| self.variable_assignment_map.get(x))
288    }
289
290    pub fn get_memory_location(
291        &self,
292        loc: &MemoryLocationId,
293    ) -> &MemoryLocation {
294        &self.mem_location_map[loc]
295    }
296
297    /// Creates a new empty source info table
298    pub fn new_empty() -> Self {
299        Self {
300            file_map: HashMap::new(),
301            position_map: HashMap::new(),
302            mem_location_map: HashMap::new(),
303            variable_assignment_map: HashMap::new(),
304            position_state_map: HashMap::new(),
305        }
306    }
307
308    /// A wrapper function to construct a source a source map containing only
309    /// files and positions. If an empty map is needed use [SourceInfoTable::new_empty]
310    pub fn new_minimal(
311        files: impl IntoIterator<Item = (FileId, PathBuf)>,
312        positions: impl IntoIterator<Item = (PositionId, FileId, LineNum)>,
313    ) -> SourceInfoResult<Self> {
314        // the compiler needs some concrete types here even though the input is
315        // all empty
316        let loc: Vec<(MemoryLocationId, MemoryLocation)> = vec![];
317        let states: Vec<(PositionId, VariableAssignmentId)> = vec![];
318        let variable_assigns: Vec<(
319            VariableAssignmentId,
320            Vec<(String, MemoryLocationId)>,
321        )> = vec![];
322
323        Self::new(files, positions, loc, variable_assigns, states)
324    }
325
326    // this is awful
327    pub fn new(
328        files: impl IntoIterator<Item = (FileId, PathBuf)>,
329        positions: impl IntoIterator<Item = (PositionId, FileId, LineNum)>,
330        locations: impl IntoIterator<Item = (MemoryLocationId, MemoryLocation)>,
331        variable_assigns: impl IntoIterator<
332            Item = (
333                VariableAssignmentId,
334                impl IntoIterator<Item = (String, MemoryLocationId)>,
335            ),
336        >,
337        states: impl IntoIterator<Item = (PositionId, VariableAssignmentId)>,
338    ) -> SourceInfoResult<Self> {
339        let files = files.into_iter();
340        let positions = positions.into_iter();
341        let locations = locations.into_iter();
342        let vars = variable_assigns.into_iter();
343        let states = states.into_iter();
344
345        let mut file_map = HashMap::with_capacity(
346            files.size_hint().1.unwrap_or(files.size_hint().0),
347        );
348        let mut position_map = HashMap::with_capacity(
349            positions.size_hint().1.unwrap_or(positions.size_hint().0),
350        );
351
352        let mut memory_location_map: HashMap<MemoryLocationId, MemoryLocation> =
353            HashMap::new();
354
355        let mut variable_map = HashMap::new();
356        let mut state_map = HashMap::new();
357
358        for (file, path) in files {
359            if let Some(first_path) = file_map.insert(file, path) {
360                let inserted_path = &file_map[&file];
361                if &first_path != inserted_path {
362                    return Err(SourceInfoTableError::DuplicateFiles {
363                        id1: file,
364                        path1: first_path,
365                        path2: inserted_path.clone(),
366                    });
367                }
368            }
369        }
370
371        for (pos, file, line) in positions {
372            let source = SourceLocation::new(file, line);
373            if let Some(first_pos) = position_map.insert(pos, source) {
374                let inserted_position = &position_map[&pos];
375                if inserted_position != &first_pos {
376                    return Err(SourceInfoTableError::DuplicatePositions {
377                        pos,
378                        s1: first_pos,
379                        s2: position_map[&pos].clone(),
380                    });
381                }
382            }
383        }
384
385        for (id, loc) in locations {
386            if memory_location_map.insert(id, loc).is_some() {
387                // duplictate entry error
388                return Err(SourceInfoTableError::DuplicateMemoryIdentifiers {
389                    id,
390                });
391            }
392        }
393
394        for (assign_label, assigns) in vars {
395            let mut mapping = HashMap::new();
396            for (name, location) in assigns {
397                // this is to avoid copying the string in all cases since we
398                // would only need it when emitting the error. Clippy doesn't
399                // like this for good reasons and while I suspect it may be
400                // possible using the entry api, I think this is clearer so I'm
401                // just suppressing the warning and writing this very long
402                // comment about it instead.
403                #[allow(clippy::map_entry)]
404                if !memory_location_map.contains_key(&location) {
405                    // unknown memory location
406                    return Err(SourceInfoTableError::UnknownMemoryId {
407                        id: location,
408                    });
409                } else if mapping.contains_key(&name) {
410                    // duplicate entries
411                    return Err(
412                        SourceInfoTableError::DuplicateVariableAssignments {
413                            id: assign_label,
414                            var: name,
415                        },
416                    );
417                } else {
418                    mapping.insert(name, location);
419                }
420            }
421            if variable_map.insert(assign_label, mapping).is_some() {
422                // duplicate entries
423                return Err(SourceInfoTableError::DuplicateVariableMappings {
424                    id: assign_label,
425                });
426            };
427        }
428
429        for (pos_id, var_id) in states {
430            if !variable_map.contains_key(&var_id) {
431                // unknown var
432                return Err(SourceInfoTableError::UnknownVariableMapping {
433                    id: var_id,
434                });
435            }
436            if state_map.insert(pos_id, var_id).is_some() {
437                // duplicate
438                return Err(SourceInfoTableError::DuplicatePosStateMappings {
439                    id: pos_id,
440                });
441            }
442        }
443
444        Ok(SourceInfoTable {
445            file_map,
446            position_map,
447            mem_location_map: memory_location_map,
448            variable_assignment_map: variable_map,
449            position_state_map: state_map,
450        })
451    }
452
453    pub fn serialize<W: std::io::Write>(
454        &self,
455        mut f: W,
456    ) -> Result<(), std::io::Error> {
457        writeln!(f, "{} #{{", Self::HEADER)?;
458
459        // write file table
460        writeln!(f, "FILES")?;
461        for (file, path) in self.file_map.iter().sorted_by_key(|(k, _)| **k) {
462            writeln!(f, "  {file}: {}", path.display())?;
463        }
464
465        // write the position table
466        writeln!(f, "POSITIONS")?;
467        for (position, SourceLocation { line, file }) in
468            self.position_map.iter().sorted_by_key(|(k, _)| **k)
469        {
470            writeln!(f, "  {position}: {file} {line}")?;
471        }
472        if !(self.mem_location_map.is_empty()
473            && self.variable_assignment_map.is_empty()
474            && self.position_state_map.is_empty())
475        {
476            // write the position table
477            writeln!(f, "MEMORY_LOCATIONS")?;
478            for (loc, MemoryLocation { cell, address }) in
479                self.mem_location_map.iter().sorted_by_key(|(k, _)| **k)
480            {
481                write!(f, "  {loc}: {cell}")?;
482                if !address.is_empty() {
483                    write!(f, "[{}]", address.iter().join(","))?;
484                }
485                writeln!(f)?;
486            }
487
488            writeln!(f, "VARIABLE_ASSIGNMENTS")?;
489            for (id, map) in self
490                .variable_assignment_map
491                .iter()
492                .sorted_by_key(|(k, _)| **k)
493            {
494                writeln!(f, "  {id}: {{")?;
495                for (var, loc) in map.iter().sorted() {
496                    writeln!(f, "    {var}: {loc}")?;
497                }
498                writeln!(f, "  }}")?;
499            }
500
501            writeln!(f, "POSITION_STATE_MAP")?;
502            for (pos, var) in
503                self.position_state_map.iter().sorted_by_key(|(k, _)| **k)
504            {
505                writeln!(f, "  {pos}: {var}")?;
506            }
507        }
508
509        writeln!(f, "}}#")
510    }
511
512    /// Attempt to lookup the line that a given position points to. Returns an error in
513    /// cases when the position does not exist, the file is unavailable, or the file
514    /// does not contain the indicated line.
515    pub fn get_position_string(
516        &self,
517        pos: PositionId,
518    ) -> Result<String, SourceLookupError> {
519        let Some(src_loc) = self.get_position(pos) else {
520            return Err(SourceLookupError::MissingPosition(pos));
521        };
522        // this will panic if the file doesn't exist but that would imply the table has
523        // incorrect information in it
524        let file_path = self.lookup_file_path(src_loc.file);
525
526        let Ok(mut file) = File::open(file_path) else {
527            return Err(SourceLookupError::MissingFile(file_path));
528        };
529
530        let mut file_contents = String::new();
531
532        match file.read_to_string(&mut file_contents) {
533            Ok(_) => {}
534            Err(_) => {
535                return Err(SourceLookupError::MissingFile(file_path));
536            }
537        }
538
539        let Some(line) = file_contents.lines().nth(src_loc.line.as_usize() - 1)
540        else {
541            return Err(SourceLookupError::MissingLine {
542                file: file_path,
543                line: src_loc.line.as_usize(),
544            });
545        };
546
547        Ok(String::from(line))
548    }
549}
550
551#[derive(Debug, Clone, PartialEq, Eq)]
552pub struct SourceLocation {
553    pub file: FileId,
554    pub line: LineNum,
555}
556
557impl SourceLocation {
558    pub fn new(file: FileId, line: LineNum) -> Self {
559        Self { line, file }
560    }
561}
562#[derive(Error)]
563pub enum SourceInfoTableError {
564    #[error("Duplicate positions found in the metadata table. Position {pos} is defined multiple times:
565    1. file {}, line {}
566    2. file {}, line {}\n", s1.file, s1.line, s2.file, s2.line)]
567    DuplicatePositions {
568        pos: PositionId,
569        s1: SourceLocation,
570        s2: SourceLocation,
571    },
572
573    #[error("Duplicate files found in the metadata table. File id {id1} is defined multiple times:
574         1. {path1}
575         2. {path2}\n")]
576    DuplicateFiles {
577        id1: FileId,
578        path1: PathBuf,
579        path2: PathBuf,
580    },
581
582    #[error("Duplicate definitions for memory location {id}")]
583    DuplicateMemoryIdentifiers { id: MemoryLocationId },
584
585    #[error("Memory location {id} is referenced but never defined")]
586    UnknownMemoryId { id: MemoryLocationId },
587
588    #[error("Variable mapping {id} is referenced but never defined")]
589    UnknownVariableMapping { id: VariableAssignmentId },
590
591    #[error("Duplicate definitions for variable mapping {id}")]
592    DuplicateVariableMappings { id: VariableAssignmentId },
593
594    #[error(
595        "Duplicate definitions for variable mapping associated with position {id}"
596    )]
597    DuplicatePosStateMappings { id: PositionId },
598
599    #[error(
600        "In variable mapping {id} the variable '{var}' has multiple definitions"
601    )]
602    DuplicateVariableAssignments {
603        id: VariableAssignmentId,
604        var: String,
605    },
606}
607
608/// Any error that can emerge while attempting to pull the actual line of text that a
609/// source line points to
610#[derive(Error, Debug)]
611pub enum SourceLookupError<'a> {
612    #[error("unable to open file {0}")]
613    MissingFile(&'a PathBuf),
614    #[error("file {file} does not have a line {line}")]
615    MissingLine { file: &'a PathBuf, line: usize },
616    #[error("position id {0} does not exist")]
617    MissingPosition(PositionId),
618}
619
620impl std::fmt::Debug for SourceInfoTableError {
621    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
622        std::fmt::Display::fmt(&self, f)
623    }
624}
625
626pub type SourceInfoResult<T> = Result<T, SourceInfoTableError>;
627
628#[cfg(test)]
629mod tests {
630    use std::path::PathBuf;
631
632    use crate::{
633        parser::CalyxParser,
634        source_info::{
635            FileId, LineNum, MemoryLocationId, PositionId,
636            SourceInfoTableError, VariableAssignmentId,
637        },
638    };
639
640    use super::SourceInfoTable;
641
642    #[test]
643    fn test_parse_metadata() {
644        let input_str = r#"sourceinfo #{
645    FILES
646        0: test.calyx
647        1: test2.calyx
648        2: test3.calyx
649    POSITIONS
650        0: 0 5
651        1: 0 1
652        2: 0 2
653    MEMORY_LOCATIONS
654        0: main.reg1
655        1: main.reg2
656        2: main.mem1 [1,4]
657    VARIABLE_ASSIGNMENTS
658        0: {
659            x: 0
660            y: 1
661            z: 2
662        }
663        1: {
664            q: 0
665        }
666    POSITION_STATE_MAP
667        0: 0
668        2: 1
669}#"#;
670
671        let metadata = CalyxParser::parse_source_info_table(input_str)
672            .unwrap()
673            .unwrap();
674        let file = metadata.lookup_file_path(1.into());
675        assert_eq!(file, &PathBuf::from("test2.calyx"));
676
677        let pos = metadata.lookup_position(1.into());
678        assert_eq!(pos.file, 0.into());
679        assert_eq!(pos.line, LineNum::new(1));
680    }
681
682    #[test]
683    fn test_undefined_mem_loc() {
684        let input_str = r#"sourceinfo #{
685    FILES
686        0: test.calyx
687    POSITIONS
688        0: 0 5
689        1: 0 1
690        2: 0 2
691    MEMORY_LOCATIONS
692        0: main.reg1
693        2: main.mem1 [1,4]
694    VARIABLE_ASSIGNMENTS
695        0: {
696            x: 0
697            y: 1
698            z: 2
699        }
700        1: {
701            q: 0
702        }
703    POSITION_STATE_MAP
704        0: 0
705        2: 1
706}#"#;
707        let metadata = CalyxParser::parse_source_info_table(input_str).unwrap();
708        assert!(metadata.is_err());
709        let err = metadata.unwrap_err();
710        assert!(matches!(
711            &err,
712            SourceInfoTableError::UnknownMemoryId {
713                id: MemoryLocationId(1)
714            }
715        ));
716    }
717
718    #[test]
719    fn test_undefined_variable() {
720        let input_str = r#"sourceinfo #{
721    FILES
722        0: test.calyx
723    POSITIONS
724        0: 0 5
725        1: 0 1
726        2: 0 2
727    MEMORY_LOCATIONS
728        0: main.reg1
729        1: main.reg2
730        2: main.mem1 [1,4]
731    VARIABLE_ASSIGNMENTS
732        0: {
733            x: 0
734            y: 1
735            z: 2
736        }
737        1: {
738            q: 0
739        }
740    POSITION_STATE_MAP
741        0: 0
742        2: 2
743}#"#;
744        let metadata = CalyxParser::parse_source_info_table(input_str).unwrap();
745        assert!(metadata.is_err());
746        let err = metadata.unwrap_err();
747
748        assert!(matches!(
749            &err,
750            SourceInfoTableError::UnknownVariableMapping {
751                id: VariableAssignmentId(2)
752            }
753        ));
754    }
755
756    #[test]
757    fn test_duplicate_variable_maps() {
758        let input_str = r#"sourceinfo #{
759    FILES
760        0: test.calyx
761    POSITIONS
762        0: 0 5
763        1: 0 1
764        2: 0 2
765    MEMORY_LOCATIONS
766        0: main.reg1
767        1: main.reg2
768        2: main.mem1 [1,4]
769    VARIABLE_ASSIGNMENTS
770        0: {
771            x: 0
772            y: 1
773            z: 2
774        }
775        1: {
776            q: 0
777        }
778        1: {
779            a: 0
780        }
781    POSITION_STATE_MAP
782        0: 0
783        2: 1
784}#"#;
785        let metadata = CalyxParser::parse_source_info_table(input_str).unwrap();
786        assert!(metadata.is_err());
787        let err = metadata.unwrap_err();
788        assert!(matches!(
789            &err,
790            SourceInfoTableError::DuplicateVariableMappings {
791                id: VariableAssignmentId(1)
792            }
793        ));
794    }
795
796    #[test]
797    fn test_duplicate_variable_assignment() {
798        let input_str = r#"sourceinfo #{
799    FILES
800        0: test.calyx
801    POSITIONS
802        0: 0 5
803        1: 0 1
804        2: 0 2
805    MEMORY_LOCATIONS
806        0: main.reg1
807        1: main.reg2
808        2: main.mem1 [1,4]
809    VARIABLE_ASSIGNMENTS
810        0: {
811            x: 0
812            y: 1
813            z: 2
814        }
815        1: {
816            q: 0
817            q: 1
818        }
819    POSITION_STATE_MAP
820        0: 0
821        2: 1
822}#"#;
823        let metadata = CalyxParser::parse_source_info_table(input_str).unwrap();
824        assert!(metadata.is_err());
825        let err = metadata.unwrap_err();
826        assert!(matches!(
827            &err,
828            SourceInfoTableError::DuplicateVariableAssignments {
829                id: VariableAssignmentId(1),
830                var
831            } if var == "q"
832        ));
833    }
834
835    #[test]
836    fn test_duplicate_mem_def() {
837        let input_str = r#"sourceinfo #{
838    FILES
839        0: test.calyx
840    POSITIONS
841        0: 0 5
842        1: 0 1
843        2: 0 2
844    MEMORY_LOCATIONS
845        0: main.reg1
846        1: main.reg2
847        1: main.mem1 [1,4]
848    VARIABLE_ASSIGNMENTS
849        0: {
850            x: 0
851            y: 1
852            z: 2
853        }
854        1: {
855            q: 0
856        }
857    POSITION_STATE_MAP
858        0: 0
859        2: 1
860}#"#;
861        let metadata = CalyxParser::parse_source_info_table(input_str).unwrap();
862        assert!(metadata.is_err());
863        let err = metadata.unwrap_err();
864        assert!(matches!(
865            &err,
866            SourceInfoTableError::DuplicateMemoryIdentifiers {
867                id: MemoryLocationId(1)
868            }
869        ));
870    }
871
872    #[test]
873    fn test_duplicate_pos_state() {
874        let input_str = r#"sourceinfo #{
875    FILES
876        0: test.calyx
877    POSITIONS
878        0: 0 5
879        1: 0 1
880        2: 0 2
881    MEMORY_LOCATIONS
882        0: main.reg1
883        1: main.reg2
884        2: main.mem1 [1,4]
885    VARIABLE_ASSIGNMENTS
886        0: {
887            x: 0
888            y: 1
889            z: 2
890        }
891        1: {
892            q: 0
893        }
894    POSITION_STATE_MAP
895        0: 0
896        0: 1
897}#"#;
898        let metadata = CalyxParser::parse_source_info_table(input_str).unwrap();
899        assert!(metadata.is_err());
900        let err = metadata.unwrap_err();
901        assert!(matches!(
902            &err,
903            SourceInfoTableError::DuplicatePosStateMappings {
904                id: PositionId(0)
905            }
906        ));
907    }
908
909    #[test]
910    fn test_duplicate_file_parse() {
911        let input_str = r#"sourceinfo #{
912            FILES
913                0: test.calyx
914                0: test2.calyx
915                2: test3.calyx
916            POSITIONS
917                0: 0 5
918                1: 0 1
919                2: 0 2
920        }#"#;
921        let metadata = CalyxParser::parse_source_info_table(input_str).unwrap();
922
923        assert!(metadata.is_err());
924        let err = metadata.unwrap_err();
925        assert!(matches!(&err, SourceInfoTableError::DuplicateFiles { .. }));
926        if let SourceInfoTableError::DuplicateFiles { id1, .. } = &err {
927            assert_eq!(id1, &FileId::new(0))
928        } else {
929            unreachable!()
930        }
931    }
932
933    #[test]
934    fn test_duplicate_position_parse() {
935        let input_str = r#"sourceinfo #{
936            FILES
937                0: test.calyx
938                1: test2.calyx
939                2: test3.calyx
940            POSITIONS
941                0: 0 5
942                0: 0 1
943                2: 0 2
944        }#"#;
945        let metadata = CalyxParser::parse_source_info_table(input_str).unwrap();
946
947        assert!(metadata.is_err());
948        let err = metadata.unwrap_err();
949        assert!(matches!(
950            &err,
951            SourceInfoTableError::DuplicatePositions { .. }
952        ));
953        if let SourceInfoTableError::DuplicatePositions { pos, .. } = err {
954            assert_eq!(pos, PositionId::new(0))
955        } else {
956            unreachable!()
957        }
958    }
959
960    #[test]
961    fn test_serialize() {
962        let mut metadata = SourceInfoTable::new_empty();
963        metadata.add_file(0.into(), "test.calyx".into());
964        metadata.add_file(1.into(), "test2.calyx".into());
965        metadata.add_file(2.into(), "test3.calyx".into());
966
967        metadata.add_position(0.into(), 0.into(), LineNum::new(1));
968        metadata.add_position(1.into(), 1.into(), LineNum::new(2));
969        metadata.add_position(150.into(), 2.into(), LineNum::new(148));
970
971        let mut serialized_str = vec![];
972        metadata.serialize(&mut serialized_str).unwrap();
973        let serialized_str = String::from_utf8(serialized_str).unwrap();
974
975        let parsed_metadata =
976            CalyxParser::parse_source_info_table(&serialized_str)
977                .unwrap()
978                .unwrap();
979
980        assert_eq!(metadata, parsed_metadata)
981    }
982}