Skip to main content

lemma/parsing/
source.rs

1use crate::parsing::ast::Span;
2use std::sync::Arc;
3
4/// Positional source location: file, span, and source text.
5///
6/// Purely positional — does not carry spec identity. Spec context for errors
7/// is on `ErrorDetails.spec_context`; planning functions receive spec identity
8/// as an explicit parameter.
9#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
10pub struct Source {
11    /// Source file identifier (e.g., filename)
12    pub attribute: String,
13
14    /// Span in source code
15    pub span: Span,
16
17    /// Full source text of the file this location refers to
18    pub source_text: Arc<str>,
19}
20
21impl Source {
22    #[must_use]
23    pub fn new(attribute: impl Into<String>, span: Span, source_text: Arc<str>) -> Self {
24        Self {
25            attribute: attribute.into(),
26            span,
27            source_text,
28        }
29    }
30}
31
32impl Source {
33    /// Extract the source text for this location from the given source string
34    ///
35    /// Returns `None` if the span is out of bounds for the source.
36    pub fn extract_text(&self, source: &str) -> Option<String> {
37        let bytes = source.as_bytes();
38        if self.span.start < bytes.len() && self.span.end <= bytes.len() {
39            Some(String::from_utf8_lossy(&bytes[self.span.start..self.span.end]).to_string())
40        } else {
41            None
42        }
43    }
44}
45
46#[cfg(test)]
47mod tests {
48    use super::*;
49
50    fn test_arc() -> Arc<str> {
51        Arc::from("hello world")
52    }
53
54    #[test]
55    fn test_extract_text_valid() {
56        let source = "hello world";
57        let span = Span {
58            start: 0,
59            end: 5,
60            line: 1,
61            col: 0,
62        };
63        let loc = Source::new("test.lemma", span, test_arc());
64        assert_eq!(loc.extract_text(source), Some("hello".to_string()));
65    }
66
67    #[test]
68    fn test_extract_text_middle() {
69        let source = "hello world";
70        let span = Span {
71            start: 6,
72            end: 11,
73            line: 1,
74            col: 6,
75        };
76        let loc = Source::new("test.lemma", span, test_arc());
77        assert_eq!(loc.extract_text(source), Some("world".to_string()));
78    }
79
80    #[test]
81    fn test_extract_text_full_string() {
82        let source = "hello world";
83        let span = Span {
84            start: 0,
85            end: 11,
86            line: 1,
87            col: 0,
88        };
89        let loc = Source::new("test.lemma", span, test_arc());
90        assert_eq!(loc.extract_text(source), Some("hello world".to_string()));
91    }
92
93    #[test]
94    fn test_extract_text_empty() {
95        let source = "hello world";
96        let span = Span {
97            start: 5,
98            end: 5,
99            line: 1,
100            col: 5,
101        };
102        let loc = Source::new("test.lemma", span, test_arc());
103        assert_eq!(loc.extract_text(source), Some("".to_string()));
104    }
105
106    #[test]
107    fn test_extract_text_out_of_bounds_start() {
108        let source = "hello";
109        let span = Span {
110            start: 10,
111            end: 15,
112            line: 1,
113            col: 10,
114        };
115        let loc = Source::new("test.lemma", span, test_arc());
116        assert_eq!(loc.extract_text(source), None);
117    }
118
119    #[test]
120    fn test_extract_text_out_of_bounds_end() {
121        let source = "hello";
122        let span = Span {
123            start: 0,
124            end: 10,
125            line: 1,
126            col: 0,
127        };
128        let loc = Source::new("test.lemma", span, test_arc());
129        assert_eq!(loc.extract_text(source), None);
130    }
131
132    #[test]
133    fn test_extract_text_unicode() {
134        let source = "hello 世界";
135        let span = Span {
136            start: 6,
137            end: 12,
138            line: 1,
139            col: 6,
140        };
141        let loc = Source::new("test.lemma", span, test_arc());
142        assert_eq!(loc.extract_text(source), Some("世界".to_string()));
143    }
144
145    #[test]
146    fn test_new_with_string() {
147        let span = Span {
148            start: 0,
149            end: 5,
150            line: 1,
151            col: 0,
152        };
153        let loc = Source::new("test.lemma", span, test_arc());
154        assert_eq!(loc.attribute, "test.lemma");
155        assert_eq!(&*loc.source_text, "hello world");
156    }
157
158    #[test]
159    fn test_new_with_str() {
160        let span = Span {
161            start: 0,
162            end: 5,
163            line: 1,
164            col: 0,
165        };
166        let loc = Source::new("test.lemma", span, Arc::from("other"));
167        assert_eq!(loc.attribute, "test.lemma");
168        assert_eq!(&*loc.source_text, "other");
169    }
170}