Skip to main content

nika_core/source/
span.rs

1//! Span and source position tracking for Nika workflows.
2//!
3//! This module provides the foundation for error reporting with precise
4//! line:col positions, enabling cargo-style diagnostics via miette.
5
6use std::ops::Range;
7
8/// Unique identifier for a source file in the SourceRegistry.
9#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Default)]
10pub struct FileId(pub u32);
11
12impl FileId {
13    /// The dummy file ID used for synthetic or unknown locations.
14    pub const DUMMY: FileId = FileId(u32::MAX);
15
16    /// Check if this is the dummy file ID.
17    pub fn is_dummy(self) -> bool {
18        self == Self::DUMMY
19    }
20}
21
22/// A byte offset in a source file.
23#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Default)]
24pub struct ByteOffset(pub u32);
25
26impl ByteOffset {
27    /// Create a new byte offset.
28    pub fn new(offset: u32) -> Self {
29        Self(offset)
30    }
31
32    /// Get the offset as usize.
33    pub fn as_usize(self) -> usize {
34        self.0 as usize
35    }
36}
37
38impl From<usize> for ByteOffset {
39    fn from(offset: usize) -> Self {
40        Self(offset as u32)
41    }
42}
43
44impl From<ByteOffset> for usize {
45    fn from(offset: ByteOffset) -> Self {
46        offset.0 as usize
47    }
48}
49
50/// A span in a source file (byte range + file ID).
51///
52/// Spans are the foundation for error reporting. Every AST node in the
53/// raw phase carries a span, enabling precise error locations.
54#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)]
55pub struct Span {
56    /// The source file this span belongs to.
57    pub file: FileId,
58    /// Start byte offset (inclusive).
59    pub start: ByteOffset,
60    /// End byte offset (exclusive).
61    pub end: ByteOffset,
62}
63
64impl Span {
65    /// Create a new span.
66    pub fn new(file: FileId, start: u32, end: u32) -> Self {
67        Self {
68            file,
69            start: ByteOffset(start),
70            end: ByteOffset(end),
71        }
72    }
73
74    /// Create a dummy span for synthetic nodes or unknown locations.
75    pub const fn dummy() -> Self {
76        Self {
77            file: FileId::DUMMY,
78            start: ByteOffset(0),
79            end: ByteOffset(0),
80        }
81    }
82
83    /// Check if this is a dummy span.
84    pub fn is_dummy(&self) -> bool {
85        self.file.is_dummy()
86    }
87
88    /// Get the byte range as a `Range<usize>`.
89    pub fn range(&self) -> Range<usize> {
90        self.start.as_usize()..self.end.as_usize()
91    }
92
93    /// Get the length in bytes.
94    pub fn len(&self) -> usize {
95        (self.end.0 - self.start.0) as usize
96    }
97
98    /// Check if the span is empty.
99    pub fn is_empty(&self) -> bool {
100        self.start.0 >= self.end.0
101    }
102
103    /// Merge two spans into one that covers both.
104    /// Both spans must be from the same file.
105    pub fn merge(self, other: Span) -> Span {
106        debug_assert!(
107            self.file == other.file || self.is_dummy() || other.is_dummy(),
108            "Cannot merge spans from different files"
109        );
110
111        if self.is_dummy() {
112            return other;
113        }
114        if other.is_dummy() {
115            return self;
116        }
117
118        Span {
119            file: self.file,
120            start: ByteOffset(self.start.0.min(other.start.0)),
121            end: ByteOffset(self.end.0.max(other.end.0)),
122        }
123    }
124
125    /// Create a span that points to the start position (zero-length).
126    pub fn start_point(self) -> Span {
127        Span {
128            file: self.file,
129            start: self.start,
130            end: self.start,
131        }
132    }
133
134    /// Create a span that points to the end position (zero-length).
135    pub fn end_point(self) -> Span {
136        Span {
137            file: self.file,
138            start: self.end,
139            end: self.end,
140        }
141    }
142}
143
144/// A value with an associated span.
145///
146/// This is the core type for the raw AST phase. Every parsed value
147/// carries its source location for error reporting.
148#[derive(Clone, Debug)]
149pub struct Spanned<T> {
150    /// The wrapped value.
151    pub value: T,
152    /// The source location of the value.
153    pub span: Span,
154}
155
156impl<T> Spanned<T> {
157    /// Create a new spanned value.
158    pub fn new(value: T, span: Span) -> Self {
159        Self { value, span }
160    }
161
162    /// Create a spanned value with a dummy span.
163    pub fn dummy(value: T) -> Self {
164        Self {
165            value,
166            span: Span::dummy(),
167        }
168    }
169
170    /// Map the inner value while preserving the span.
171    pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Spanned<U> {
172        Spanned {
173            value: f(self.value),
174            span: self.span,
175        }
176    }
177
178    /// Get a reference to the inner value.
179    pub fn inner(&self) -> &T {
180        &self.value
181    }
182
183    /// Get a mutable reference to the inner value.
184    pub fn inner_mut(&mut self) -> &mut T {
185        &mut self.value
186    }
187
188    /// Consume self and return the inner value.
189    pub fn into_inner(self) -> T {
190        self.value
191    }
192
193    /// Create a reference to a spanned reference.
194    pub fn as_ref(&self) -> Spanned<&T> {
195        Spanned {
196            value: &self.value,
197            span: self.span,
198        }
199    }
200}
201
202impl<T> std::ops::Deref for Spanned<T> {
203    type Target = T;
204
205    fn deref(&self) -> &Self::Target {
206        &self.value
207    }
208}
209
210impl<T> std::ops::DerefMut for Spanned<T> {
211    fn deref_mut(&mut self) -> &mut Self::Target {
212        &mut self.value
213    }
214}
215
216impl<T: PartialEq> PartialEq for Spanned<T> {
217    fn eq(&self, other: &Self) -> bool {
218        // Compare only values, not spans (for semantic equality)
219        self.value == other.value
220    }
221}
222
223impl<T: Eq> Eq for Spanned<T> {}
224
225impl<T: std::hash::Hash> std::hash::Hash for Spanned<T> {
226    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
227        // Hash only the value, not the span
228        self.value.hash(state);
229    }
230}
231
232impl<T: Default> Default for Spanned<T> {
233    fn default() -> Self {
234        Self::dummy(T::default())
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241
242    #[test]
243    fn test_span_creation() {
244        let span = Span::new(FileId(1), 10, 20);
245        assert_eq!(span.file, FileId(1));
246        assert_eq!(span.start.0, 10);
247        assert_eq!(span.end.0, 20);
248        assert_eq!(span.len(), 10);
249        assert!(!span.is_dummy());
250    }
251
252    #[test]
253    fn test_dummy_span() {
254        let span = Span::dummy();
255        assert!(span.is_dummy());
256        assert!(span.file.is_dummy());
257    }
258
259    #[test]
260    fn test_span_merge() {
261        let file = FileId(1);
262        let a = Span::new(file, 10, 20);
263        let b = Span::new(file, 15, 30);
264        let merged = a.merge(b);
265
266        assert_eq!(merged.start.0, 10);
267        assert_eq!(merged.end.0, 30);
268    }
269
270    #[test]
271    fn test_span_merge_with_dummy() {
272        let file = FileId(1);
273        let real = Span::new(file, 10, 20);
274        let dummy = Span::dummy();
275
276        assert_eq!(real.merge(dummy), real);
277        assert_eq!(dummy.merge(real), real);
278    }
279
280    #[test]
281    fn test_spanned_map() {
282        let spanned = Spanned::new(42, Span::new(FileId(1), 0, 5));
283        let mapped = spanned.map(|x| x.to_string());
284
285        assert_eq!(mapped.value, "42");
286        assert_eq!(mapped.span.start.0, 0);
287    }
288
289    #[test]
290    fn test_spanned_equality_ignores_span() {
291        let a = Spanned::new(42, Span::new(FileId(1), 0, 5));
292        let b = Spanned::new(42, Span::new(FileId(2), 100, 200));
293
294        assert_eq!(a, b); // Same value, different spans
295    }
296}