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