use std::fmt;
use std::sync::{Arc, OnceLock};
use crate::{BytePos, Span};
#[derive(Clone)]
pub struct SourceBytes(Arc<dyn AsRef<[u8]> + Send + Sync>);
impl SourceBytes {
pub fn new(bytes: impl AsRef<[u8]> + Send + Sync + 'static) -> SourceBytes {
SourceBytes(Arc::new(bytes))
}
#[inline]
pub fn as_slice(&self) -> &[u8] {
(*self.0).as_ref()
}
}
impl fmt::Debug for SourceBytes {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "SourceBytes({} bytes)", self.as_slice().len())
}
}
impl AsRef<[u8]> for SourceBytes {
#[inline]
fn as_ref(&self) -> &[u8] {
self.as_slice()
}
}
impl std::ops::Deref for SourceBytes {
type Target = [u8];
#[inline]
fn deref(&self) -> &[u8] {
self.as_slice()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FileId(u32);
impl FileId {
#[inline]
pub const fn index(self) -> usize {
self.0 as usize
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Loc {
pub file: FileId,
pub line: u32,
pub column: u32,
}
pub struct SourceFile {
pub id: FileId,
pub name: String,
pub start: BytePos,
pub end: BytePos,
pub included_from: Option<Span>,
bytes: SourceBytes,
lines: OnceLock<Vec<BytePos>>,
}
impl fmt::Debug for SourceFile {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SourceFile")
.field("id", &self.id)
.field("name", &self.name)
.field("start", &self.start)
.field("end", &self.end)
.field("included_from", &self.included_from)
.finish()
}
}
impl SourceFile {
#[inline]
pub fn bytes(&self) -> &[u8] {
self.bytes.as_slice()
}
#[inline]
pub fn shared_bytes(&self) -> SourceBytes {
self.bytes.clone()
}
#[inline]
pub fn len(&self) -> u32 {
self.end - self.start
}
#[inline]
pub fn is_empty(&self) -> bool {
self.start == self.end
}
#[inline]
pub fn contains(&self, pos: BytePos) -> bool {
self.start <= pos && pos <= self.end
}
pub fn line_count(&self) -> u32 {
u32::try_from(self.lines().len()).unwrap_or(u32::MAX)
}
pub fn line_bytes(&self, line: u32) -> Option<&[u8]> {
let lines = self.lines();
let index = usize::try_from(line.checked_sub(1)?).ok()?;
let from = *lines.get(index)? - self.start;
let to = lines.get(index + 1).map_or(self.len(), |next| *next - self.start);
let text = self.bytes().get(from as usize..to as usize)?;
let text = text.strip_suffix(b"\n").unwrap_or(text);
Some(text.strip_suffix(b"\r").unwrap_or(text))
}
pub fn position(&self, pos: BytePos) -> Option<Loc> {
let (line, begin) = self.line_of(pos)?;
Some(Loc { file: self.id, line, column: pos - begin + 1 })
}
pub fn line_span(&self, pos: BytePos) -> Option<Span> {
let (line, begin) = self.line_of(pos)?;
let end = self.lines().get(line as usize).copied().unwrap_or(self.end);
Some(Span::new(begin, end))
}
fn line_of(&self, pos: BytePos) -> Option<(u32, BytePos)> {
if !self.contains(pos) {
return None;
}
let lines = self.lines();
let line = lines.partition_point(|&start| start <= pos);
let begin = lines.get(line.saturating_sub(1)).copied().unwrap_or(self.start);
Some((u32::try_from(line).unwrap_or(u32::MAX), begin))
}
fn lines(&self) -> &[BytePos] {
self.lines.get_or_init(|| {
let bytes = self.bytes();
let mut starts = Vec::with_capacity(bytes.len() / 24 + 1);
starts.push(self.start);
for (at, _) in bytes.iter().enumerate().filter(|&(_, &b)| b == b'\n') {
let next = self.start + u32::try_from(at).unwrap_or(u32::MAX - 1) + 1;
if next < self.end {
starts.push(next);
}
}
starts
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SourceMapFull;
impl fmt::Display for SourceMapFull {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("the translation unit does not fit in the four gigabyte source map")
}
}
impl std::error::Error for SourceMapFull {}
#[derive(Debug, Default)]
pub struct SourceMap {
files: Vec<SourceFile>,
next: BytePos,
}
impl SourceMap {
pub fn new() -> SourceMap {
SourceMap::default()
}
pub fn add(
&mut self,
name: impl Into<String>,
bytes: impl AsRef<[u8]> + Send + Sync + 'static,
) -> Result<FileId, SourceMapFull> {
self.push(name.into(), SourceBytes::new(bytes), None)
}
pub fn add_shared(
&mut self,
name: impl Into<String>,
bytes: SourceBytes,
included_from: Option<Span>,
) -> Result<FileId, SourceMapFull> {
self.push(name.into(), bytes, included_from)
}
pub fn add_included(
&mut self,
name: impl Into<String>,
bytes: impl AsRef<[u8]> + Send + Sync + 'static,
from: Span,
) -> Result<FileId, SourceMapFull> {
self.push(name.into(), SourceBytes::new(bytes), Some(from))
}
fn push(
&mut self,
name: String,
bytes: SourceBytes,
included_from: Option<Span>,
) -> Result<FileId, SourceMapFull> {
let len = u32::try_from(bytes.as_slice().len()).map_err(|_| SourceMapFull)?;
let start = self.next;
let end = start.checked_add(len).ok_or(SourceMapFull)?;
self.next = end.checked_add(1).filter(|&n| n < BytePos::MAX).ok_or(SourceMapFull)?;
let id = FileId(u32::try_from(self.files.len()).map_err(|_| SourceMapFull)?);
self.files.push(SourceFile {
id,
name,
start,
end,
included_from,
bytes,
lines: OnceLock::new(),
});
Ok(id)
}
pub fn files(&self) -> &[SourceFile] {
&self.files
}
pub fn file(&self, id: FileId) -> &SourceFile {
&self.files[id.index()]
}
pub fn lookup_file(&self, pos: BytePos) -> Option<FileId> {
if pos == BytePos::MAX {
return None;
}
let at = self.files.partition_point(|f| f.start <= pos);
let file = self.files.get(at.checked_sub(1)?)?;
file.contains(pos).then_some(file.id)
}
pub fn lookup(&self, pos: BytePos) -> Option<Loc> {
self.file(self.lookup_file(pos)?).position(pos)
}
pub fn render_position(&self, pos: BytePos) -> String {
match self.lookup(pos) {
Some(loc) => format!("{}:{}:{}", self.file(loc.file).name, loc.line, loc.column),
None => "<unknown>".to_owned(),
}
}
pub fn include_stack(&self, pos: BytePos) -> Vec<Span> {
let mut stack = Vec::new();
let mut at = self.lookup_file(pos);
while let Some(file) = at {
let Some(from) = self.file(file).included_from else { break };
stack.push(from);
at = self.lookup_file(from.lo);
if stack.len() > self.files.len() {
break;
}
}
stack
}
pub fn used(&self) -> BytePos {
self.next
}
}
#[cfg(test)]
mod tests {
use super::*;
fn map_with(files: &[(&str, &str)]) -> (SourceMap, Vec<FileId>) {
let mut map = SourceMap::new();
let ids = files
.iter()
.map(|(name, text)| map.add(*name, text.as_bytes().to_vec()).unwrap())
.collect();
(map, ids)
}
#[test]
fn the_first_file_starts_at_zero_and_the_next_one_after_a_gap() {
let (map, ids) = map_with(&[("a.c", "ab"), ("b.c", "cd")]);
assert_eq!(map.file(ids[0]).start, 0);
assert_eq!(map.file(ids[0]).end, 2);
assert_eq!(map.file(ids[1]).start, 3);
assert_eq!(map.used(), 6);
}
#[test]
fn the_position_after_a_file_belongs_to_that_file_and_not_the_next() {
let (map, ids) = map_with(&[("a.c", "ab"), ("b.c", "cd")]);
assert_eq!(map.lookup_file(2), Some(ids[0]));
assert_eq!(map.lookup_file(3), Some(ids[1]));
}
#[test]
fn a_position_in_the_gap_is_in_no_file() {
let mut map = SourceMap::new();
map.add("a.c", b"ab".to_vec()).unwrap();
assert_eq!(map.lookup_file(3), None);
assert_eq!(map.render_position(3), "<unknown>");
}
#[test]
fn a_dummy_span_resolves_to_nothing() {
let (map, _) = map_with(&[("a.c", "ab")]);
assert_eq!(map.lookup(Span::DUMMY.lo), None);
assert_eq!(map.lookup_file(BytePos::MAX), None);
}
#[test]
fn lines_and_columns_count_from_one() {
let (map, ids) = map_with(&[("a.c", "one\ntwo\nthree\n")]);
let start = map.file(ids[0]).start;
assert_eq!(map.lookup(start).unwrap(), Loc { file: ids[0], line: 1, column: 1 });
assert_eq!(map.lookup(start + 4).unwrap(), Loc { file: ids[0], line: 2, column: 1 });
assert_eq!(map.lookup(start + 6).unwrap(), Loc { file: ids[0], line: 2, column: 3 });
assert_eq!(map.render_position(start + 8), "a.c:3:1");
}
#[test]
fn a_trailing_newline_does_not_open_a_line() {
let (map, ids) = map_with(&[("a.c", "one\ntwo\n"), ("b.c", "one\ntwo")]);
assert_eq!(map.file(ids[0]).line_count(), 2);
assert_eq!(map.file(ids[1]).line_count(), 2);
}
#[test]
fn a_blank_line_is_a_line() {
let (map, ids) = map_with(&[("a.c", "one\n\nthree\n")]);
let file = map.file(ids[0]);
assert_eq!(file.line_count(), 3);
assert_eq!(file.line_bytes(2), Some(&b""[..]));
assert_eq!(file.line_bytes(3), Some(&b"three"[..]));
assert_eq!(file.line_bytes(4), None);
assert_eq!(file.line_bytes(0), None);
}
#[test]
fn a_carriage_return_is_not_part_of_the_line() {
let (map, ids) = map_with(&[("a.c", "one\r\ntwo\r\n")]);
let file = map.file(ids[0]);
assert_eq!(file.line_bytes(1), Some(&b"one"[..]));
assert_eq!(file.line_bytes(2), Some(&b"two"[..]));
}
#[test]
fn an_empty_file_has_one_position_and_no_lines_to_read() {
let (map, ids) = map_with(&[("a.c", "")]);
let file = map.file(ids[0]);
assert!(file.is_empty());
assert_eq!(map.lookup(file.start).unwrap().line, 1);
assert_eq!(file.line_bytes(1), Some(&b""[..]));
assert_eq!(file.line_bytes(2), None);
}
#[test]
fn a_line_span_covers_the_terminator() {
let (map, ids) = map_with(&[("a.c", "one\ntwo\n")]);
let file = map.file(ids[0]);
assert_eq!(file.line_span(file.start + 1), Some(Span::new(0, 4)));
assert_eq!(file.line_span(file.start + 5), Some(Span::new(4, 8)));
}
#[test]
fn the_include_stack_runs_from_the_innermost_out() {
let mut map = SourceMap::new();
let main = map.add("main.c", b"#include <a.h>\n".to_vec()).unwrap();
let outer = Span::new(map.file(main).start, map.file(main).start + 14);
let a = map.add_included("a.h", b"#include <b.h>\n".to_vec(), outer).unwrap();
let inner = Span::new(map.file(a).start, map.file(a).start + 14);
let b = map.add_included("b.h", b"int x;\n".to_vec(), inner).unwrap();
let stack = map.include_stack(map.file(b).start);
assert_eq!(stack, vec![inner, outer]);
assert_eq!(map.lookup(stack[0].lo).unwrap().file, a);
assert_eq!(map.lookup(stack[1].lo).unwrap().file, main);
assert!(map.include_stack(outer.lo).is_empty());
}
#[test]
fn a_file_that_does_not_fit_is_refused_rather_than_wrapped() {
let mut map = SourceMap::new();
map.add("a.c", b"x".to_vec()).unwrap();
map.next = BytePos::MAX - 2;
assert_eq!(map.add("b.c", b"xx".to_vec()), Err(SourceMapFull));
assert_eq!(map.files().len(), 1);
}
#[test]
fn contents_can_be_anything_that_is_a_slice_of_bytes() {
struct Mapped(&'static [u8]);
impl AsRef<[u8]> for Mapped {
fn as_ref(&self) -> &[u8] {
self.0
}
}
let mut map = SourceMap::new();
let id = map.add("a.c", Mapped(b"int x;\n")).unwrap();
assert_eq!(map.file(id).bytes(), b"int x;\n");
assert_eq!(map.file(id).line_count(), 1);
}
}