calyx_utils/
position.rs

1//! Definitions for tracking source position information of Calyx programs
2
3use std::{cmp, fmt::Write, sync::LazyLock};
4
5use itertools::Itertools;
6
7#[derive(Clone, Copy, PartialEq, Eq, Debug)]
8/// Handle to a position in a [PositionTable]
9/// The index refers to the index in the [PositionTable::indices] vector.
10pub struct PosIdx(u32);
11
12#[derive(Clone, Copy, PartialEq, Eq)]
13/// Handle to a file in a [PositionTable]
14/// The index refers to the index in the [PositionTable::files] vector.
15pub struct FileIdx(u32);
16
17/// A source program file
18struct File {
19    /// Name of the file
20    name: Box<str>,
21    /// The source code of the file
22    source: Box<str>,
23}
24
25struct PosData {
26    /// The file in the program. The index refers to the index in the
27    /// [PositionTable::files] vector.
28    file: FileIdx,
29    /// Start of the span
30    start: usize,
31    /// End of the span
32    end: usize,
33}
34
35/// Source position information for a Calyx program.
36struct PositionTable {
37    /// The source files of the program
38    files: boxcar::Vec<File>,
39    /// Mapping from indexes to position data
40    indices: boxcar::Vec<PosData>,
41}
42
43impl Default for PositionTable {
44    fn default() -> Self {
45        Self::new()
46    }
47}
48
49impl PositionTable {
50    /// The unknown position
51    pub const UNKNOWN: PosIdx = PosIdx(0);
52
53    /// Create a new position table where the first file and first position are unknown
54    pub fn new() -> Self {
55        let table = PositionTable {
56            files: boxcar::Vec::new(),
57            indices: boxcar::Vec::new(),
58        };
59        table.add_file("unknown".to_string(), "".to_string());
60        let pos = table.add_pos(FileIdx(0), 0, 0);
61        debug_assert!(pos == Self::UNKNOWN);
62        table
63    }
64
65    /// Add a new file to the position table
66    pub fn add_file(&self, name: String, source: String) -> FileIdx {
67        let file = File {
68            name: name.into(),
69            source: source.into(),
70        };
71        let file_idx = self.files.push(file);
72        FileIdx(file_idx as u32)
73    }
74
75    /// Return a reference to the file with the given index
76    fn get_file_data(&self, file: FileIdx) -> &File {
77        &self.files[file.0 as usize]
78    }
79
80    pub fn get_source(&self, file: FileIdx) -> &str {
81        &self.get_file_data(file).source
82    }
83
84    /// Add a new position to the position table
85    pub fn add_pos(&self, file: FileIdx, start: usize, end: usize) -> PosIdx {
86        let pos = PosData { file, start, end };
87        let pos_idx = self.indices.push(pos);
88        PosIdx(pos_idx as u32)
89    }
90
91    fn get_pos(&self, pos: PosIdx) -> &PosData {
92        &self.indices[pos.0 as usize]
93    }
94}
95
96/// The global position table
97pub struct GlobalPositionTable;
98
99static GPOS_TABLE: LazyLock<PositionTable> = LazyLock::new(PositionTable::new);
100
101impl GlobalPositionTable {
102    fn get_pos(pos: PosIdx) -> &'static PosData {
103        GPOS_TABLE.get_pos(pos)
104    }
105
106    fn get_file_data(file: FileIdx) -> &'static File {
107        GPOS_TABLE.get_file_data(file)
108    }
109
110    pub fn get_source(file: FileIdx) -> &'static str {
111        GPOS_TABLE.get_source(file)
112    }
113
114    pub fn add_file(name: String, source: String) -> FileIdx {
115        GPOS_TABLE.add_file(name, source)
116    }
117
118    pub fn add_pos(file: FileIdx, start: usize, end: usize) -> PosIdx {
119        GPOS_TABLE.add_pos(file, start, end)
120    }
121}
122
123#[derive(Clone, Copy, PartialEq, Eq, Debug)]
124/// A position index backed by a global [PositionTable]
125pub struct GPosIdx(pub PosIdx);
126
127impl Default for GPosIdx {
128    fn default() -> Self {
129        Self::UNKNOWN
130    }
131}
132
133impl GPosIdx {
134    /// Symbol for the unknown position
135    pub const UNKNOWN: GPosIdx = GPosIdx(PosIdx(0));
136
137    /// Convert the position into an optional.
138    /// Returns `None` if the position is the unknown position.
139    pub fn into_option(self) -> Option<Self> {
140        if self == Self::UNKNOWN {
141            None
142        } else {
143            Some(self)
144        }
145    }
146
147    /// Returns the
148    /// 1. lines associated with this span
149    /// 2. start position of the first line in span
150    /// 3. line number of the span
151    fn get_lines(&self) -> (Vec<&str>, usize, usize) {
152        let pos_d = GlobalPositionTable::get_pos(self.0);
153        let file = &*GlobalPositionTable::get_file_data(pos_d.file).source;
154
155        let lines = file.split('\n').collect_vec();
156        let mut pos: usize = 0;
157        let mut linum: usize = 1;
158        let mut collect_lines = false;
159        let mut buf = Vec::new();
160
161        let mut out_line: usize = 0;
162        let mut out_idx: usize = 0;
163        for l in lines {
164            let next_pos = pos + l.len();
165            if pos_d.start >= pos && pos_d.start <= next_pos {
166                out_line = linum;
167                out_idx = pos;
168                collect_lines = true;
169            }
170            if collect_lines && pos_d.end >= pos {
171                buf.push(l)
172            }
173            if pos_d.end <= next_pos {
174                break;
175            }
176            pos = next_pos + 1;
177            linum += 1;
178        }
179        (buf, out_idx, out_line)
180    }
181
182    /// returns:
183    /// 1. the name of the file the span is in
184    /// 2. the (inclusive) range of lines within the span
185    pub fn get_line_num(&self) -> (&str, (usize, usize)) {
186        let pos_data = GlobalPositionTable::get_pos(self.0);
187        let file_name = &GlobalPositionTable::get_file_data(pos_data.file).name;
188        let (buf, _, line_num) = self.get_lines();
189        //reformat to return the range (inclusive)
190        let rng = (line_num, line_num + buf.len() - 1);
191        (file_name, rng)
192    }
193
194    /// Format this position with the error message `err_msg`
195    pub fn format_raw<S: AsRef<str>>(&self, err_msg: S) -> String {
196        let pos_d = GlobalPositionTable::get_pos(self.0);
197
198        let (lines, pos, linum) = self.get_lines();
199        let mut buf = String::new();
200
201        let l = lines[0];
202        let linum_text = format!("{linum} ");
203        let linum_space: String = " ".repeat(linum_text.len());
204        let mark: String = "^".repeat(cmp::min(
205            pos_d.end - pos_d.start,
206            l.len() - (pos_d.start - pos),
207        ));
208        let space: String = " ".repeat(pos_d.start - pos);
209        writeln!(buf, "{linum_text}|{l}").unwrap();
210        write!(
211            buf,
212            "{}|{}{} {}",
213            linum_space,
214            space,
215            mark,
216            err_msg.as_ref()
217        )
218        .unwrap();
219        buf
220    }
221
222    /// Format this position with filename header and the error message `err_msg`
223    pub fn format<S: AsRef<str>>(&self, err_msg: S) -> String {
224        let pos_d = GlobalPositionTable::get_pos(self.0);
225        let name = &*GlobalPositionTable::get_file_data(pos_d.file).name;
226
227        let mut buf = name.to_string();
228        writeln!(buf).unwrap();
229        write!(buf, "{}", self.format_raw(err_msg)).unwrap();
230        buf
231    }
232
233    pub fn get_location(&self) -> (&str, usize, usize) {
234        let pos_d = GlobalPositionTable::get_pos(self.0);
235        let name = &*GlobalPositionTable::get_file_data(pos_d.file).name;
236        (name, pos_d.start, pos_d.end)
237    }
238
239    /// Visualizes the span without any message or marking
240    pub fn show(&self) -> String {
241        let (lines, _, linum) = self.get_lines();
242        let l = lines[0];
243        let linum_text = format!("{linum} ");
244        format!("{linum_text}|{l}\n")
245    }
246}
247
248/// An IR node that may contain position information.
249pub trait WithPos {
250    /// Copy the span associated with this node.
251    fn copy_span(&self) -> GPosIdx;
252}
253
254impl WithPos for GPosIdx {
255    fn copy_span(&self) -> GPosIdx {
256        *self
257    }
258}