Skip to main content

lemma/parsing/
source.rs

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