use alloc::string::{String, ToString};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Span {
pub start: u32,
pub end: u32,
}
impl Span {
pub const fn new(start: u32, end: u32) -> Self {
Span {
start,
end: if end < start { start } else { end },
}
}
pub const fn at(offset: u32) -> Self {
Span {
start: offset,
end: offset,
}
}
pub const fn len(self) -> u32 {
self.end - self.start
}
pub const fn is_empty(self) -> bool {
self.start == self.end
}
pub const fn join(self, other: Span) -> Span {
let start = if self.start < other.start {
self.start
} else {
other.start
};
let end = if self.end > other.end {
self.end
} else {
other.end
};
Span::new(start, end)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Spanned<T> {
pub node: T,
pub span: Span,
}
impl<T> Spanned<T> {
pub const fn new(node: T, span: Span) -> Self {
Spanned { node, span }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Location {
pub line: u32,
pub col: u32,
}
#[derive(Debug, Clone, Copy)]
pub struct SourceFile<'a> {
name: &'a str,
text: &'a str,
}
impl<'a> SourceFile<'a> {
pub const fn new(name: &'a str, text: &'a str) -> Self {
SourceFile { name, text }
}
pub const fn name(&self) -> &'a str {
self.name
}
pub const fn text(&self) -> &'a str {
self.text
}
pub fn location(&self, offset: u32) -> Location {
let mut off = offset as usize;
if off > self.text.len() {
off = self.text.len();
}
while off > 0 && !self.text.is_char_boundary(off) {
off -= 1;
}
let before = &self.text[..off];
let line = before.bytes().filter(|b| *b == b'\n').count() + 1;
let line_start = before.rfind('\n').map_or(0, |i| i + 1);
let col = self.text[line_start..off].chars().count() + 1;
Location {
line: u32::try_from(line).unwrap_or(u32::MAX),
col: u32::try_from(col).unwrap_or(u32::MAX),
}
}
pub fn line_text(&self, line: u32) -> &'a str {
if line == 0 {
return "";
}
self.text.lines().nth(line as usize - 1).unwrap_or("")
}
pub fn position(&self, offset: u32) -> String {
let loc = self.location(offset);
let mut s = String::from(self.name);
s.push(':');
s.push_str(&loc.line.to_string());
s.push(':');
s.push_str(&loc.col.to_string());
s
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn spans_join_and_measure() {
let a = Span::new(3, 7);
let b = Span::new(10, 12);
assert_eq!(a.len(), 4);
assert!(!a.is_empty());
assert!(Span::at(5).is_empty());
assert_eq!(a.join(b), Span::new(3, 12));
assert_eq!(b.join(a), Span::new(3, 12));
assert_eq!(Span::new(9, 4).len(), 0);
}
#[test]
fn locations_are_one_based() {
let src = SourceFile::new("m.machine", "abc\ndef\n");
assert_eq!(src.location(0), Location { line: 1, col: 1 });
assert_eq!(src.location(2), Location { line: 1, col: 3 });
assert_eq!(src.location(4), Location { line: 2, col: 1 });
assert_eq!(src.position(5), "m.machine:2:2");
}
#[test]
fn columns_count_characters_not_bytes() {
let src = SourceFile::new("m", "# é é\nx");
assert_eq!(src.location(7), Location { line: 1, col: 6 });
}
#[test]
fn out_of_range_offsets_clamp() {
let src = SourceFile::new("m", "ab");
assert_eq!(src.location(999), Location { line: 1, col: 3 });
let src = SourceFile::new("m", "é");
assert_eq!(src.location(1), Location { line: 1, col: 1 });
assert_eq!(src.line_text(9), "");
assert_eq!(src.line_text(0), "");
}
#[test]
fn line_text_drops_terminators() {
let src = SourceFile::new("m", "one\r\ntwo\n");
assert_eq!(src.line_text(1), "one");
assert_eq!(src.line_text(2), "two");
assert_eq!(src.line_text(3), "");
}
}