logicaffeine_base/span.rs
1//! Source location tracking for error reporting.
2//!
3//! A [`Span`] represents a contiguous region of source text using byte offsets.
4//! Every token, expression, and error in logicaffeine carries a span, enabling
5//! precise error messages that point to the exact location of problems.
6//!
7//! # Byte Offsets
8//!
9//! Spans use byte offsets, not character indices. This matches Rust's string
10//! slicing semantics: `&source[span.start..span.end]` extracts the spanned text.
11//!
12//! # Example
13//!
14//! ```
15//! use logicaffeine_base::Span;
16//!
17//! let source = "hello world";
18//! let span = Span::new(0, 5);
19//!
20//! assert_eq!(&source[span.start..span.end], "hello");
21//! assert_eq!(span.len(), 5);
22//! ```
23
24/// A byte-offset range in source text.
25///
26/// Spans are `Copy` and cheap to pass around. Use [`Span::merge`] to combine
27/// spans when building compound expressions.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
29pub struct Span {
30 /// Byte offset of the first character (inclusive).
31 pub start: usize,
32 /// Byte offset past the last character (exclusive).
33 pub end: usize,
34}
35
36impl Span {
37 /// Creates a span from byte offsets.
38 ///
39 /// No validation is performed; `start` may exceed `end`.
40 pub fn new(start: usize, end: usize) -> Self {
41 Self { start, end }
42 }
43
44 /// Creates a span covering from the start of `self` to the end of `other`.
45 ///
46 /// Useful for building compound expressions: the span of `a + b` is
47 /// `a.span.merge(b.span)`.
48 pub fn merge(self, other: Span) -> Span {
49 Span {
50 start: self.start.min(other.start),
51 end: self.end.max(other.end),
52 }
53 }
54
55 /// Returns the length of the span in bytes.
56 pub fn len(&self) -> usize {
57 self.end.saturating_sub(self.start)
58 }
59
60 /// Returns `true` if this span covers no bytes.
61 pub fn is_empty(&self) -> bool {
62 self.start >= self.end
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69
70 #[test]
71 fn span_new_stores_positions() {
72 let span = Span::new(5, 10);
73 assert_eq!(span.start, 5);
74 assert_eq!(span.end, 10);
75 }
76
77 #[test]
78 fn span_default_is_zero() {
79 let span = Span::default();
80 assert_eq!(span.start, 0);
81 assert_eq!(span.end, 0);
82 }
83
84 #[test]
85 fn span_merge_combines_ranges() {
86 let a = Span::new(5, 10);
87 let b = Span::new(8, 15);
88 let merged = a.merge(b);
89 assert_eq!(merged.start, 5);
90 assert_eq!(merged.end, 15);
91 }
92
93 #[test]
94 fn span_len_returns_size() {
95 let span = Span::new(5, 10);
96 assert_eq!(span.len(), 5);
97 }
98
99 #[test]
100 fn span_is_empty_for_zero_length() {
101 let empty = Span::new(5, 5);
102 assert!(empty.is_empty());
103
104 let nonempty = Span::new(5, 10);
105 assert!(!nonempty.is_empty());
106 }
107}