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