rucc_diag/source.rs
1//! The source map: which file a [`BytePos`] lands in, and where in that file.
2//!
3//! Design: `spec/03-architecture.md`, and `spec/05-preprocessor.md` section 5.2 for the
4//! coordinate space a token span lives in.
5//!
6//! Every file in a translation unit gets a range of one flat coordinate space, so a [`Span`]
7//! is two integers and comparing two of them does not need to know which file each came from.
8//! That is what keeps a token at sixteen bytes with a header nest twelve deep, and it is why
9//! the expander can join the span of a macro argument with the span of the call site without
10//! a special case.
11//!
12//! The price is that turning an offset back into a file, a line and a column is a search
13//! rather than a field read. It is paid only when a diagnostic is rendered, which happens for
14//! a handful of positions out of the millions the lexer produces, so the line table for a
15//! file is built the first time somebody asks about that file and never for a file nobody
16//! asks about.
17//!
18//! ```
19//! use rucc_diag::SourceMap;
20//!
21//! let mut map = SourceMap::new();
22//! let file = map.add("hello.c", b"int main(void)\n{\n return 0;\n}\n".to_vec()).unwrap();
23//! let brace = map.file(file).start + 15;
24//! let loc = map.lookup(brace).unwrap();
25//! assert_eq!(loc.line, 2);
26//! assert_eq!(loc.column, 1);
27//! assert_eq!(map.render_position(brace), "hello.c:2:1");
28//! ```
29
30use std::fmt;
31use std::sync::{Arc, OnceLock};
32
33use crate::{BytePos, Span};
34
35/// The contents of a file, shared rather than copied.
36///
37/// A trait object rather than a `Vec`, so that the memory mapped input in
38/// `spec/05-preprocessor.md` section 5.2 can be handed over as it is, and shared so that a
39/// header included twice, or served twice out of the header cache, is held once.
40///
41/// It is a type of its own rather than a bare `Arc` so that it can have a `Debug` that says
42/// how long a file is instead of printing it. A `{:#?}` of anything holding one of these
43/// should not dump the whole of `stdio.h` into a test failure.
44#[derive(Clone)]
45pub struct SourceBytes(Arc<dyn AsRef<[u8]> + Send + Sync>);
46
47impl SourceBytes {
48 /// Takes ownership of anything that is a slice of bytes.
49 pub fn new(bytes: impl AsRef<[u8]> + Send + Sync + 'static) -> SourceBytes {
50 SourceBytes(Arc::new(bytes))
51 }
52
53 /// The bytes.
54 #[inline]
55 pub fn as_slice(&self) -> &[u8] {
56 (*self.0).as_ref()
57 }
58}
59
60impl fmt::Debug for SourceBytes {
61 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62 write!(f, "SourceBytes({} bytes)", self.as_slice().len())
63 }
64}
65
66impl AsRef<[u8]> for SourceBytes {
67 #[inline]
68 fn as_ref(&self) -> &[u8] {
69 self.as_slice()
70 }
71}
72
73impl std::ops::Deref for SourceBytes {
74 type Target = [u8];
75
76 #[inline]
77 fn deref(&self) -> &[u8] {
78 self.as_slice()
79 }
80}
81
82/// A file in the source map.
83///
84/// Only meaningful against the map that issued it. A `FileId` is an index, so passing one to
85/// a different map is a bug the type system does not catch, which is fine because there is
86/// one map per compilation and it lives on the session.
87#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
88pub struct FileId(u32);
89
90impl FileId {
91 /// The index of this file in the map, for a caller keeping a side table.
92 #[inline]
93 pub const fn index(self) -> usize {
94 self.0 as usize
95 }
96}
97
98/// A position resolved back to a human coordinate.
99///
100/// Lines and columns both count from one, because that is what every editor, every other
101/// compiler and every user expects, and an off by one here is the kind of bug that survives
102/// for years because nobody quite trusts their own arithmetic enough to file it.
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub struct Loc {
105 /// Which file.
106 pub file: FileId,
107 /// The line, counting from one.
108 pub line: u32,
109 /// The column in bytes, counting from one.
110 ///
111 /// Bytes rather than characters or display columns. A tab counts as one and a multibyte
112 /// character counts as its encoded length, which is what a caret line drawn from the same
113 /// bytes needs. `-ftabstop` and the width of a CJK character are the renderer's problem;
114 /// what belongs here is the offset into the line.
115 pub column: u32,
116}
117
118/// The bytes of one file, plus where they sit in the flat space.
119///
120/// Contents are held behind a trait object rather than as a `Vec`, so that the memory mapped
121/// input in `spec/05-preprocessor.md` section 5.2 can be handed over as it is instead of
122/// being copied into one. Reading the bytes goes through one virtual call, which is fine
123/// because it happens once per file in the lexer and once per rendered diagnostic, never in
124/// a loop.
125pub struct SourceFile {
126 /// This file's own id, so that anything holding a `&SourceFile` can name it.
127 pub id: FileId,
128 /// The name to print in a diagnostic, which is the path as the user wrote it rather than
129 /// a canonical one. Somebody who typed `-I../include` wants to read `../include/foo.h`.
130 pub name: String,
131 /// First byte of this file in the flat space.
132 pub start: BytePos,
133 /// One past this file's last byte.
134 pub end: BytePos,
135 /// The `#include` that pulled this file in, or `None` for a file named on the command
136 /// line. This is what "in file included from" is printed from.
137 pub included_from: Option<Span>,
138 bytes: SourceBytes,
139 /// Absolute offset of the first byte of each line. Built on first use, because most files
140 /// in a build are never the subject of a diagnostic.
141 lines: OnceLock<Vec<BytePos>>,
142 /// The `#line` directives in this file, in the order they were read, which is also the
143 /// order of the positions they start at. Empty for almost every file there is.
144 presumed: Vec<Presumed>,
145}
146
147/// What a stretch of a file is presented as, which is what a `#line` in front of it said.
148///
149/// A `#line` does not move any bytes. It changes the answer to "where is this", for
150/// `__FILE__` and `__LINE__`, for a diagnostic and for the markers `-E` writes, and for
151/// nothing else: the text of the line is still read out of the real file at the real offset,
152/// because that is where the characters are.
153#[derive(Debug, Clone)]
154struct Presumed {
155 /// First byte of the line this applies from, which is the line after the directive.
156 at: BytePos,
157 /// The real line `at` is on, so the distance from it can be added back on.
158 real: u32,
159 /// The line `at` is presented as.
160 line: u32,
161 /// The name the file is presented under from `at` on. A `#line` with no name carries the
162 /// one already in force, so this is never empty and the lookup never has to walk back.
163 name: String,
164}
165
166/// Where a position is presented as being, which is where a `#line` says it is.
167///
168/// The name is borrowed rather than owned because the common case is that it is the file's
169/// own name and there is nothing to clone.
170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
171pub struct PresumedLoc<'a> {
172 /// The file name to print.
173 pub name: &'a str,
174 /// The line to print, counting from one.
175 pub line: u32,
176 /// The column, which no `#line` ever changes.
177 pub column: u32,
178}
179
180impl fmt::Debug for SourceFile {
181 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182 // The bytes are deliberately not printed. A `{:#?}` of a session should not dump the
183 // whole of `stdio.h` into a test failure.
184 f.debug_struct("SourceFile")
185 .field("id", &self.id)
186 .field("name", &self.name)
187 .field("start", &self.start)
188 .field("end", &self.end)
189 .field("included_from", &self.included_from)
190 .finish()
191 }
192}
193
194impl SourceFile {
195 /// The file's contents.
196 #[inline]
197 pub fn bytes(&self) -> &[u8] {
198 self.bytes.as_slice()
199 }
200
201 /// The file's contents, shared.
202 #[inline]
203 pub fn shared_bytes(&self) -> SourceBytes {
204 self.bytes.clone()
205 }
206
207 /// Length in bytes.
208 #[inline]
209 pub fn len(&self) -> u32 {
210 self.end - self.start
211 }
212
213 /// Whether the file is empty.
214 #[inline]
215 pub fn is_empty(&self) -> bool {
216 self.start == self.end
217 }
218
219 /// Whether `pos` falls in this file.
220 ///
221 /// The end position is included, so a diagnostic about something missing at the end of a
222 /// file still names the file rather than falling into the gap after it.
223 #[inline]
224 pub fn contains(&self, pos: BytePos) -> bool {
225 self.start <= pos && pos <= self.end
226 }
227
228 /// How many lines the file has, counting a trailing newline as ending the last line
229 /// rather than starting another. An empty file has one line, which is empty.
230 pub fn line_count(&self) -> u32 {
231 u32::try_from(self.lines().len()).unwrap_or(u32::MAX)
232 }
233
234 /// The bytes of line `line`, counting from one, without its line terminator.
235 ///
236 /// `None` if the file has no such line.
237 pub fn line_bytes(&self, line: u32) -> Option<&[u8]> {
238 let lines = self.lines();
239 let index = usize::try_from(line.checked_sub(1)?).ok()?;
240 let from = *lines.get(index)? - self.start;
241 let to = lines.get(index + 1).map_or(self.len(), |next| *next - self.start);
242 let text = self.bytes().get(from as usize..to as usize)?;
243 // Strip the terminator rather than the last byte, so that a file with CRLF endings
244 // does not put a carriage return in the middle of a rendered caret line.
245 let text = text.strip_suffix(b"\n").unwrap_or(text);
246 Some(text.strip_suffix(b"\r").unwrap_or(text))
247 }
248
249 /// The line and column of `pos`, or `None` if `pos` is not in this file.
250 pub fn position(&self, pos: BytePos) -> Option<Loc> {
251 let (line, begin) = self.line_of(pos)?;
252 Some(Loc { file: self.id, line, column: pos - begin + 1 })
253 }
254
255 /// Where `pos` is presented as being, once the `#line` directives in front of it are
256 /// taken into account. The same as [`SourceFile::position`] for a file that has none.
257 pub fn presumed_position(&self, pos: BytePos) -> Option<PresumedLoc<'_>> {
258 let loc = self.position(pos)?;
259 // The entries are in increasing order of `at`, so the one in force is the last one
260 // starting at or before `pos`.
261 let after = self.presumed.partition_point(|p| p.at <= pos);
262 let Some(entry) = after.checked_sub(1).map(|i| &self.presumed[i]) else {
263 return Some(PresumedLoc { name: &self.name, line: loc.line, column: loc.column });
264 };
265 // `pos` is at or after `entry.at`, so the subtraction cannot go below zero.
266 let line = entry.line.saturating_add(loc.line - entry.real);
267 Some(PresumedLoc { name: &entry.name, line, column: loc.column })
268 }
269
270 /// The span covering the line `pos` is on, including its terminator.
271 pub fn line_span(&self, pos: BytePos) -> Option<Span> {
272 let (line, begin) = self.line_of(pos)?;
273 let end = self.lines().get(line as usize).copied().unwrap_or(self.end);
274 Some(Span::new(begin, end))
275 }
276
277 /// The one-based line `pos` is on, and where that line starts.
278 fn line_of(&self, pos: BytePos) -> Option<(u32, BytePos)> {
279 if !self.contains(pos) {
280 return None;
281 }
282 let lines = self.lines();
283 // `partition_point` gives the number of line starts at or before `pos`, which is the
284 // one-based line number, and is never zero because the first entry is the file start.
285 let line = lines.partition_point(|&start| start <= pos);
286 let begin = lines.get(line.saturating_sub(1)).copied().unwrap_or(self.start);
287 Some((u32::try_from(line).unwrap_or(u32::MAX), begin))
288 }
289
290 /// The line start table, built on first use.
291 fn lines(&self) -> &[BytePos] {
292 self.lines.get_or_init(|| {
293 let bytes = self.bytes();
294 // Twenty four bytes a line is roughly what C source averages. Getting this wrong
295 // costs a reallocation, not a correctness problem.
296 let mut starts = Vec::with_capacity(bytes.len() / 24 + 1);
297 starts.push(self.start);
298 for (at, _) in bytes.iter().enumerate().filter(|&(_, &b)| b == b'\n') {
299 let next = self.start + u32::try_from(at).unwrap_or(u32::MAX - 1) + 1;
300 // A newline as the very last byte ends the last line, it does not open an
301 // empty one. Every other newline opens a line, including one followed
302 // immediately by another newline.
303 if next < self.end {
304 starts.push(next);
305 }
306 }
307 starts
308 })
309 }
310}
311
312/// The flat coordinate space is full.
313///
314/// Reaching this needs four gigabytes of source in one translation unit, counting every
315/// header once per time it is included. It is reported rather than ignored because the
316/// alternative is spans that silently point at the wrong file.
317#[derive(Debug, Clone, Copy, PartialEq, Eq)]
318pub struct SourceMapFull;
319
320impl fmt::Display for SourceMapFull {
321 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
322 f.write_str("the translation unit does not fit in the four gigabyte source map")
323 }
324}
325
326impl std::error::Error for SourceMapFull {}
327
328/// Every file of one translation unit, laid end to end.
329#[derive(Debug, Default)]
330pub struct SourceMap {
331 files: Vec<SourceFile>,
332 next: BytePos,
333}
334
335impl SourceMap {
336 /// An empty map.
337 pub fn new() -> SourceMap {
338 SourceMap::default()
339 }
340
341 /// Adds a file named on the command line.
342 ///
343 /// # Errors
344 ///
345 /// [`SourceMapFull`] if the file does not fit in what is left of the coordinate space.
346 pub fn add(
347 &mut self,
348 name: impl Into<String>,
349 bytes: impl AsRef<[u8]> + Send + Sync + 'static,
350 ) -> Result<FileId, SourceMapFull> {
351 self.push(name.into(), SourceBytes::new(bytes), None)
352 }
353
354 /// Adds a file whose contents are already shared.
355 ///
356 /// This is the entry point the file system abstraction uses, because it hands out bytes
357 /// it may also be holding in a cache.
358 ///
359 /// # Errors
360 ///
361 /// [`SourceMapFull`] if the file does not fit in what is left of the coordinate space.
362 pub fn add_shared(
363 &mut self,
364 name: impl Into<String>,
365 bytes: SourceBytes,
366 included_from: Option<Span>,
367 ) -> Result<FileId, SourceMapFull> {
368 self.push(name.into(), bytes, included_from)
369 }
370
371 /// Adds a file reached through the `#include` at `from`.
372 ///
373 /// # Errors
374 ///
375 /// [`SourceMapFull`] if the file does not fit in what is left of the coordinate space.
376 pub fn add_included(
377 &mut self,
378 name: impl Into<String>,
379 bytes: impl AsRef<[u8]> + Send + Sync + 'static,
380 from: Span,
381 ) -> Result<FileId, SourceMapFull> {
382 self.push(name.into(), SourceBytes::new(bytes), Some(from))
383 }
384
385 fn push(
386 &mut self,
387 name: String,
388 bytes: SourceBytes,
389 included_from: Option<Span>,
390 ) -> Result<FileId, SourceMapFull> {
391 let len = u32::try_from(bytes.as_slice().len()).map_err(|_| SourceMapFull)?;
392 let start = self.next;
393 let end = start.checked_add(len).ok_or(SourceMapFull)?;
394 // One byte of padding after every file, so that the position one past the end of a
395 // file is still that file's and not the first byte of the next one. Without it a
396 // diagnostic about a missing `}` at the end of a header names whatever came after it.
397 // `BytePos::MAX` is `Span::DUMMY` and belongs to nobody, so the space stops one short.
398 self.next = end.checked_add(1).filter(|&n| n < BytePos::MAX).ok_or(SourceMapFull)?;
399 let id = FileId(u32::try_from(self.files.len()).map_err(|_| SourceMapFull)?);
400 self.files.push(SourceFile {
401 id,
402 name,
403 start,
404 end,
405 included_from,
406 bytes,
407 lines: OnceLock::new(),
408 presumed: Vec::new(),
409 });
410 Ok(id)
411 }
412
413 /// Every file, in the order they were added.
414 pub fn files(&self) -> &[SourceFile] {
415 &self.files
416 }
417
418 /// The file `id` names.
419 ///
420 /// # Panics
421 ///
422 /// Panics if `id` came from a different map. There is one map per compilation, on the
423 /// session, so this is a programming error rather than something a caller handles.
424 pub fn file(&self, id: FileId) -> &SourceFile {
425 &self.files[id.index()]
426 }
427
428 /// Which file `pos` is in.
429 pub fn lookup_file(&self, pos: BytePos) -> Option<FileId> {
430 if pos == BytePos::MAX {
431 return None;
432 }
433 // Files are laid out in increasing order and never overlap, so the candidate is the
434 // last one starting at or before `pos`. It is a candidate rather than the answer
435 // because `pos` may be in the padding byte after that file.
436 let at = self.files.partition_point(|f| f.start <= pos);
437 let file = self.files.get(at.checked_sub(1)?)?;
438 file.contains(pos).then_some(file.id)
439 }
440
441 /// The file, line and column of `pos`.
442 pub fn lookup(&self, pos: BytePos) -> Option<Loc> {
443 self.file(self.lookup_file(pos)?).position(pos)
444 }
445
446 /// Where `pos` is presented as being, which is [`SourceMap::lookup`] with the `#line`
447 /// directives in front of it applied.
448 ///
449 /// This is the answer to give a user: it is what a diagnostic prints, what `__FILE__` and
450 /// `__LINE__` expand to and what a line marker says. [`SourceMap::lookup`] is the answer
451 /// to use when the bytes are wanted, which is reading the text of a line to draw a caret
452 /// under it.
453 pub fn presumed(&self, pos: BytePos) -> Option<PresumedLoc<'_>> {
454 self.file(self.lookup_file(pos)?).presumed_position(pos)
455 }
456
457 /// Where the line after the one `at` is on is presented as being.
458 ///
459 /// This is what a `#line` written at `at` did, asked after the fact. `-E` needs it to
460 /// write the marker the directive turns into, and asking it here rather than reading the
461 /// directive again is what keeps one answer about where anything is.
462 pub fn presumed_after(&self, at: BytePos) -> Option<PresumedLoc<'_>> {
463 let file = self.file(self.lookup_file(at)?);
464 file.presumed_position(file.line_span(at)?.hi)
465 }
466
467 /// Records a `#line` written at `at`, which presents the line after it as `line`, and the
468 /// file as `name` when one was given.
469 ///
470 /// The directive applies from the following line rather than from where it is written,
471 /// which is what makes `#line 1000` followed by `__LINE__` expand to 1000 and not 1001.
472 /// A `#line` with no name leaves the name alone, so the entry inherits whichever one is
473 /// already in force.
474 ///
475 /// Nothing happens if `at` is in no file, or if the directive is the last line of one.
476 /// There is nothing after it for the entry to apply to in either case.
477 pub fn set_presumed(&mut self, at: BytePos, line: u32, name: Option<String>) {
478 let Some(id) = self.lookup_file(at) else { return };
479 let Some(from) = self.file(id).line_span(at).map(|l| l.hi) else { return };
480 let Some(real) = self.file(id).position(from).map(|loc| loc.line) else { return };
481 let name = name.unwrap_or_else(|| {
482 let file = self.file(id);
483 file.presumed.last().map_or_else(|| file.name.clone(), |p| p.name.clone())
484 });
485 let file = &mut self.files[id.index()];
486 // A directive read twice is a file included twice, and a file included twice is two
487 // files in this map. So the entries only ever arrive in increasing order of `at`,
488 // which is the order the lookup binary searches in.
489 file.presumed.push(Presumed { at: from, real, line, name });
490 }
491
492 /// `name:line:column` for `pos`, or `<unknown>` for a position in no file.
493 ///
494 /// This is the prefix of a rendered diagnostic and the form every editor already knows
495 /// how to jump to.
496 pub fn render_position(&self, pos: BytePos) -> String {
497 match self.presumed(pos) {
498 Some(loc) => format!("{}:{}:{}", loc.name, loc.line, loc.column),
499 None => "<unknown>".to_owned(),
500 }
501 }
502
503 /// The chain of `#include` directives that led to `pos`, innermost first.
504 ///
505 /// Empty for a position in a file named on the command line. This is what the "in file
506 /// included from" block of a diagnostic is printed from, and reading it out of the map
507 /// rather than out of a stack the preprocessor keeps means it is still available long
508 /// after preprocessing has finished.
509 pub fn include_stack(&self, pos: BytePos) -> Vec<Span> {
510 let mut stack = Vec::new();
511 let mut at = self.lookup_file(pos);
512 while let Some(file) = at {
513 let Some(from) = self.file(file).included_from else { break };
514 stack.push(from);
515 at = self.lookup_file(from.lo);
516 // A file is always added after the one that includes it, so the walk terminates.
517 // A map built by hand in a test could say otherwise, and an infinite loop inside
518 // the diagnostic renderer is a bad way to find that out.
519 if stack.len() > self.files.len() {
520 break;
521 }
522 }
523 stack
524 }
525
526 /// How much of the coordinate space is used, which is where the next file will start.
527 pub fn used(&self) -> BytePos {
528 self.next
529 }
530}
531
532#[cfg(test)]
533mod tests {
534 use super::*;
535
536 fn map_with(files: &[(&str, &str)]) -> (SourceMap, Vec<FileId>) {
537 let mut map = SourceMap::new();
538 let ids = files
539 .iter()
540 .map(|(name, text)| map.add(*name, text.as_bytes().to_vec()).unwrap())
541 .collect();
542 (map, ids)
543 }
544
545 #[test]
546 fn the_first_file_starts_at_zero_and_the_next_one_after_a_gap() {
547 let (map, ids) = map_with(&[("a.c", "ab"), ("b.c", "cd")]);
548 assert_eq!(map.file(ids[0]).start, 0);
549 assert_eq!(map.file(ids[0]).end, 2);
550 assert_eq!(map.file(ids[1]).start, 3);
551 assert_eq!(map.used(), 6);
552 }
553
554 #[test]
555 fn the_position_after_a_file_belongs_to_that_file_and_not_the_next() {
556 let (map, ids) = map_with(&[("a.c", "ab"), ("b.c", "cd")]);
557 assert_eq!(map.lookup_file(2), Some(ids[0]));
558 assert_eq!(map.lookup_file(3), Some(ids[1]));
559 }
560
561 #[test]
562 fn a_position_in_the_gap_is_in_no_file() {
563 let mut map = SourceMap::new();
564 map.add("a.c", b"ab".to_vec()).unwrap();
565 // Offset 2 is the end of `a.c`, and offset 3 would be the next file, which does not
566 // exist, so nothing is there.
567 assert_eq!(map.lookup_file(3), None);
568 assert_eq!(map.render_position(3), "<unknown>");
569 }
570
571 #[test]
572 fn a_dummy_span_resolves_to_nothing() {
573 let (map, _) = map_with(&[("a.c", "ab")]);
574 assert_eq!(map.lookup(Span::DUMMY.lo), None);
575 assert_eq!(map.lookup_file(BytePos::MAX), None);
576 }
577
578 #[test]
579 fn lines_and_columns_count_from_one() {
580 let (map, ids) = map_with(&[("a.c", "one\ntwo\nthree\n")]);
581 let start = map.file(ids[0]).start;
582 assert_eq!(map.lookup(start).unwrap(), Loc { file: ids[0], line: 1, column: 1 });
583 assert_eq!(map.lookup(start + 4).unwrap(), Loc { file: ids[0], line: 2, column: 1 });
584 assert_eq!(map.lookup(start + 6).unwrap(), Loc { file: ids[0], line: 2, column: 3 });
585 assert_eq!(map.render_position(start + 8), "a.c:3:1");
586 }
587
588 #[test]
589 fn a_trailing_newline_does_not_open_a_line() {
590 let (map, ids) = map_with(&[("a.c", "one\ntwo\n"), ("b.c", "one\ntwo")]);
591 assert_eq!(map.file(ids[0]).line_count(), 2);
592 assert_eq!(map.file(ids[1]).line_count(), 2);
593 }
594
595 #[test]
596 fn a_blank_line_is_a_line() {
597 let (map, ids) = map_with(&[("a.c", "one\n\nthree\n")]);
598 let file = map.file(ids[0]);
599 assert_eq!(file.line_count(), 3);
600 assert_eq!(file.line_bytes(2), Some(&b""[..]));
601 assert_eq!(file.line_bytes(3), Some(&b"three"[..]));
602 assert_eq!(file.line_bytes(4), None);
603 assert_eq!(file.line_bytes(0), None);
604 }
605
606 #[test]
607 fn a_carriage_return_is_not_part_of_the_line() {
608 let (map, ids) = map_with(&[("a.c", "one\r\ntwo\r\n")]);
609 let file = map.file(ids[0]);
610 assert_eq!(file.line_bytes(1), Some(&b"one"[..]));
611 assert_eq!(file.line_bytes(2), Some(&b"two"[..]));
612 }
613
614 #[test]
615 fn an_empty_file_has_one_position_and_no_lines_to_read() {
616 let (map, ids) = map_with(&[("a.c", "")]);
617 let file = map.file(ids[0]);
618 assert!(file.is_empty());
619 assert_eq!(map.lookup(file.start).unwrap().line, 1);
620 assert_eq!(file.line_bytes(1), Some(&b""[..]));
621 assert_eq!(file.line_bytes(2), None);
622 }
623
624 #[test]
625 fn a_line_span_covers_the_terminator() {
626 let (map, ids) = map_with(&[("a.c", "one\ntwo\n")]);
627 let file = map.file(ids[0]);
628 assert_eq!(file.line_span(file.start + 1), Some(Span::new(0, 4)));
629 assert_eq!(file.line_span(file.start + 5), Some(Span::new(4, 8)));
630 }
631
632 #[test]
633 fn the_include_stack_runs_from_the_innermost_out() {
634 let mut map = SourceMap::new();
635 let main = map.add("main.c", b"#include <a.h>\n".to_vec()).unwrap();
636 let outer = Span::new(map.file(main).start, map.file(main).start + 14);
637 let a = map.add_included("a.h", b"#include <b.h>\n".to_vec(), outer).unwrap();
638 let inner = Span::new(map.file(a).start, map.file(a).start + 14);
639 let b = map.add_included("b.h", b"int x;\n".to_vec(), inner).unwrap();
640 let stack = map.include_stack(map.file(b).start);
641 assert_eq!(stack, vec![inner, outer]);
642 assert_eq!(map.lookup(stack[0].lo).unwrap().file, a);
643 assert_eq!(map.lookup(stack[1].lo).unwrap().file, main);
644 assert!(map.include_stack(outer.lo).is_empty());
645 }
646
647 #[test]
648 fn a_file_that_does_not_fit_is_refused_rather_than_wrapped() {
649 let mut map = SourceMap::new();
650 map.add("a.c", b"x".to_vec()).unwrap();
651 // The map is then asked for everything that is left plus the padding it always adds,
652 // which is one byte more than the space holds.
653 map.next = BytePos::MAX - 2;
654 assert_eq!(map.add("b.c", b"xx".to_vec()), Err(SourceMapFull));
655 assert_eq!(map.files().len(), 1);
656 }
657
658 #[test]
659 fn contents_can_be_anything_that_is_a_slice_of_bytes() {
660 // What a memory mapped file will look like when it arrives: not a `Vec`, not a
661 // `String`, just something that hands out a slice.
662 struct Mapped(&'static [u8]);
663 impl AsRef<[u8]> for Mapped {
664 fn as_ref(&self) -> &[u8] {
665 self.0
666 }
667 }
668 let mut map = SourceMap::new();
669 let id = map.add("a.c", Mapped(b"int x;\n")).unwrap();
670 assert_eq!(map.file(id).bytes(), b"int x;\n");
671 assert_eq!(map.file(id).line_count(), 1);
672 }
673
674 /// Position of the first byte of `line` in the only file of `map`.
675 fn start_of(map: &SourceMap, line: u32) -> BytePos {
676 let file = &map.files()[0];
677 let mut at = file.start;
678 for _ in 1..line {
679 at = file.line_span(at).expect("the file has that line").hi;
680 }
681 at
682 }
683
684 #[test]
685 fn a_line_directive_moves_the_lines_after_it_and_not_the_ones_before() {
686 let (mut map, _) = map_with(&[("a.c", "one\n#line 90\nthree\nfour\n")]);
687 map.set_presumed(start_of(&map, 2), 90, None);
688 assert_eq!(map.render_position(start_of(&map, 1)), "a.c:1:1");
689 assert_eq!(map.render_position(start_of(&map, 2)), "a.c:2:1");
690 assert_eq!(map.render_position(start_of(&map, 3)), "a.c:90:1");
691 assert_eq!(map.render_position(start_of(&map, 4)), "a.c:91:1");
692 }
693
694 #[test]
695 fn a_directive_with_a_name_renames_the_file_and_one_without_leaves_the_name() {
696 let (mut map, _) = map_with(&[("a.c", "one\n#line 90 \"gen.y\"\nthree\n#line 5\nfive\n")]);
697 map.set_presumed(start_of(&map, 2), 90, Some("gen.y".to_owned()));
698 map.set_presumed(start_of(&map, 4), 5, None);
699 assert_eq!(map.render_position(start_of(&map, 3)), "gen.y:90:1");
700 assert_eq!(map.render_position(start_of(&map, 5)), "gen.y:5:1");
701 }
702
703 #[test]
704 fn the_directive_says_where_the_line_after_it_is() {
705 let (mut map, _) = map_with(&[("a.c", "one\n#line 90 \"gen.y\"\nthree\n")]);
706 let directive = start_of(&map, 2);
707 map.set_presumed(directive, 90, Some("gen.y".to_owned()));
708 let after = map.presumed_after(directive).expect("the directive is in the file");
709 assert_eq!((after.name, after.line), ("gen.y", 90));
710 }
711
712 #[test]
713 fn the_bytes_of_a_line_are_still_read_from_where_the_bytes_are() {
714 // The whole risk of presenting a different line is that something goes looking for the
715 // text at the presented one and draws a caret under nothing.
716 let (mut map, ids) = map_with(&[("a.c", "one\n#line 90\nthree\n")]);
717 map.set_presumed(start_of(&map, 2), 90, None);
718 let at = start_of(&map, 3);
719 assert_eq!(map.lookup(at).expect("in the file").line, 3);
720 assert_eq!(map.file(ids[0]).line_bytes(3), Some(&b"three"[..]));
721 }
722}