Skip to main content

ass_core/utils/
spans.rs

1//! Zero-copy span utilities for AST references.
2//!
3//! Provides [`Spans`], a helper for validating and locating string slices that
4//! reference the original source text while maintaining zero-copy semantics.
5
6use core::ops::Range;
7
8/// Zero-copy span utilities for AST node validation and manipulation
9///
10/// Provides safe methods to work with string slices that reference
11/// the original source text, maintaining zero-copy semantics.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct Spans<'a> {
14    /// Reference to the original source text
15    source: &'a str,
16}
17
18impl<'a> Spans<'a> {
19    /// Create new span utilities for source text
20    #[must_use]
21    pub const fn new(source: &'a str) -> Self {
22        Self { source }
23    }
24
25    /// Validate that a span references this source text
26    ///
27    /// Returns `true` if the span is a valid substring of the source.
28    /// Used for debug assertions to ensure zero-copy invariants.
29    #[must_use]
30    pub fn validate_span(&self, span: &str) -> bool {
31        let source_start = self.source.as_ptr() as usize;
32        let source_end = source_start + self.source.len();
33
34        let span_start = span.as_ptr() as usize;
35        let span_end = span_start + span.len();
36
37        span_start >= source_start && span_end <= source_end
38    }
39
40    /// Get byte offset of span within source
41    #[must_use]
42    pub fn span_offset(&self, span: &str) -> Option<usize> {
43        let source_start = self.source.as_ptr() as usize;
44        let span_start = span.as_ptr() as usize;
45
46        if self.validate_span(span) {
47            Some(span_start - source_start)
48        } else {
49            None
50        }
51    }
52
53    /// Get line number (1-based) for a span
54    #[must_use]
55    pub fn span_line(&self, span: &str) -> Option<usize> {
56        let offset = self.span_offset(span)?;
57        Some(self.source[..offset].chars().filter(|&c| c == '\n').count() + 1)
58    }
59
60    /// Get column number (1-based) for a span
61    #[must_use]
62    pub fn span_column(&self, span: &str) -> Option<usize> {
63        let offset = self.span_offset(span)?;
64        let line_start = self.source[..offset].rfind('\n').map_or(0, |pos| pos + 1);
65
66        Some(self.source[line_start..offset].chars().count() + 1)
67    }
68
69    /// Extract substring by byte range
70    #[must_use]
71    pub fn substring(&self, range: Range<usize>) -> Option<&'a str> {
72        self.source.get(range)
73    }
74}