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