lemma/parsing/
source.rs

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