Skip to main content

lemma/parsing/
source.rs

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