Skip to main content

lumen_ir/
source_map.rs

1//! Where each line of a concatenated script came from.
2//!
3//! A language's program is every script file it owns plus the inline
4//! `<script>` blocks, joined in source order into one text. A compiler reports
5//! a line of that text; the map turns it back into the file the author wrote
6//! and the line in it.
7
8use serde::{Deserialize, Serialize};
9
10/// One piece of a concatenated script.
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12pub struct SourcePiece {
13    /// The 1-based line of the concatenated text the piece starts on.
14    pub at: u32,
15    /// The file the piece was read from, as an error names it.
16    pub file: String,
17    /// The 1-based line of `file` the piece's first line is.
18    pub first_line: u32,
19}
20
21/// The pieces of a concatenated script, in the order they were joined.
22#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
23pub struct SourceMap {
24    /// Every piece, ascending by [`SourcePiece::at`].
25    pub pieces: Vec<SourcePiece>,
26}
27
28impl SourceMap {
29    /// A map of a text read whole from `file`.
30    pub fn whole(file: impl Into<String>) -> Self {
31        Self {
32            pieces: vec![SourcePiece {
33                at: 1,
34                file: file.into(),
35                first_line: 1,
36            }],
37        }
38    }
39
40    /// Whether the map names no piece.
41    pub fn is_empty(&self) -> bool {
42        self.pieces.is_empty()
43    }
44
45    /// Append `body`, which starts on line `first_line` of `file`, to `text`,
46    /// separated from what `text` already holds by a newline, and record where
47    /// it landed.
48    pub fn append(&mut self, text: &mut String, body: &str, file: &str, first_line: u32) {
49        if !text.is_empty() {
50            text.push('\n');
51        }
52        let at = line_at(text, text.len());
53        text.push_str(body);
54        self.pieces.push(SourcePiece {
55            at,
56            file: file.to_owned(),
57            first_line,
58        });
59    }
60
61    /// Append the text `body` that `map` describes to `text`, the way
62    /// [`Self::append`] appends one piece, keeping each of its pieces.
63    pub fn append_mapped(&mut self, text: &mut String, body: &str, map: &SourceMap) {
64        if !text.is_empty() {
65            text.push('\n');
66        }
67        let offset = line_at(text, text.len()) - 1;
68        text.push_str(body);
69        self.pieces.extend(map.pieces.iter().map(|p| SourcePiece {
70            at: p.at + offset,
71            ..p.clone()
72        }));
73    }
74
75    /// Give every piece recorded without a file the name `file`.
76    pub fn name_unnamed(&mut self, file: &str) {
77        for piece in self.pieces.iter_mut().filter(|p| p.file.is_empty()) {
78            piece.file = file.to_owned();
79        }
80    }
81
82    /// Count each piece's first line in `written`, the markup as the author
83    /// wrote it, where the piece's text is found there as the body of an
84    /// element. `text` is the concatenation this map describes.
85    ///
86    /// A parse sees markup with its `<include>` files spliced in, which shifts
87    /// every line after an include; the author's file does not have them. A
88    /// piece whose text the file does not hold (one an include brought in)
89    /// keeps the line it was recorded with.
90    pub fn place_in(&mut self, text: &str, written: &str) {
91        let lines: Vec<&str> = text.split('\n').collect();
92        let ends: Vec<u32> = self
93            .pieces
94            .iter()
95            .skip(1)
96            .map(|p| p.at - 1)
97            .chain(std::iter::once(lines.len() as u32))
98            .collect();
99        for (piece, end) in self.pieces.iter_mut().zip(ends) {
100            let from = (piece.at as usize).saturating_sub(1);
101            let to = (end as usize).min(lines.len());
102            if from >= to {
103                continue;
104            }
105            let body = lines[from..to].join("\n");
106            if body.trim().is_empty() {
107                continue;
108            }
109            if let Some((found, _)) = written
110                .match_indices(body.as_str())
111                .find(|(i, _)| written[..*i].ends_with('>'))
112            {
113                piece.first_line = line_at(written, found);
114            }
115        }
116    }
117
118    /// The file and the line in it that line `line` of the concatenated text
119    /// was read from. `None` when the map names no piece that early.
120    pub fn locate(&self, line: u32) -> Option<(&str, u32)> {
121        let piece = self.pieces.iter().rev().find(|p| p.at <= line)?;
122        Some((piece.file.as_str(), piece.first_line + (line - piece.at)))
123    }
124}
125
126/// The 1-based line byte offset `offset` of `text` is on.
127pub fn line_at(text: &str, offset: usize) -> u32 {
128    let end = offset.min(text.len());
129    text.as_bytes()[..end]
130        .iter()
131        .filter(|b| **b == b'\n')
132        .count() as u32
133        + 1
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn appended_pieces_locate_back_to_their_files() {
142        let mut text = String::new();
143        let mut map = SourceMap::default();
144        map.append(&mut text, "a1\na2", "main.lmn", 7);
145        map.append(&mut text, "b1\nb2\nb3", "src/b.cdl", 1);
146        assert_eq!(map.locate(1), Some(("main.lmn", 7)));
147        assert_eq!(map.locate(2), Some(("main.lmn", 8)));
148        assert_eq!(map.locate(3), Some(("src/b.cdl", 1)));
149        assert_eq!(map.locate(5), Some(("src/b.cdl", 3)));
150        assert_eq!(map.locate(0), None);
151    }
152
153    #[test]
154    fn pieces_are_placed_at_their_line_in_the_written_file() {
155        let written = "<root>\n<include src=\"a.lmn\"/>\n<script>\nlet x = 1;\n</script>\n</root>";
156        // The spliced text carried two more lines ahead of the block.
157        let mut text = String::new();
158        let mut map = SourceMap::default();
159        map.append(&mut text, "\nlet x = 1;\n", "", 5);
160        map.name_unnamed("main.lmn");
161        map.place_in(&text, written);
162        assert_eq!(map.locate(2), Some(("main.lmn", 4)));
163    }
164
165    #[test]
166    fn a_mapped_append_shifts_every_piece() {
167        let mut inner_text = String::new();
168        let mut inner = SourceMap::default();
169        inner.append(&mut inner_text, "x", "a.lmn", 3);
170        inner.append(&mut inner_text, "y", "b.lmn", 9);
171
172        let mut text = String::new();
173        let mut map = SourceMap::default();
174        map.append(&mut text, "one\ntwo", "first.cdl", 1);
175        map.append_mapped(&mut text, &inner_text, &inner);
176        assert_eq!(text, "one\ntwo\nx\ny");
177        assert_eq!(map.locate(3), Some(("a.lmn", 3)));
178        assert_eq!(map.locate(4), Some(("b.lmn", 9)));
179    }
180}