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_full_string() {
70        let source = "hello world";
71        let span = Span {
72            start: 0,
73            end: 11,
74            line: 1,
75            col: 0,
76        };
77        let loc = Source::new("test.lemma", span);
78        assert_eq!(loc.extract_text(source), Some("hello world".to_string()));
79    }
80
81    #[test]
82    fn test_extract_text_empty() {
83        let source = "hello world";
84        let span = Span {
85            start: 5,
86            end: 5,
87            line: 1,
88            col: 5,
89        };
90        let loc = Source::new("test.lemma", span);
91        assert_eq!(loc.extract_text(source), Some("".to_string()));
92    }
93
94    #[test]
95    fn test_extract_text_out_of_bounds_start() {
96        let source = "hello";
97        let span = Span {
98            start: 10,
99            end: 15,
100            line: 1,
101            col: 10,
102        };
103        let loc = Source::new("test.lemma", span);
104        assert_eq!(loc.extract_text(source), None);
105    }
106
107    #[test]
108    fn test_extract_text_out_of_bounds_end() {
109        let source = "hello";
110        let span = Span {
111            start: 0,
112            end: 10,
113            line: 1,
114            col: 0,
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_unicode() {
122        let source = "hello 世界";
123        let span = Span {
124            start: 6,
125            end: 12,
126            line: 1,
127            col: 6,
128        };
129        let loc = Source::new("test.lemma", span);
130        assert_eq!(loc.extract_text(source), Some("世界".to_string()));
131    }
132
133    #[test]
134    fn test_text_from() {
135        let mut sources = HashMap::new();
136        sources.insert("test.lemma".to_string(), "hello world".to_string());
137        let loc = Source::new(
138            "test.lemma",
139            Span {
140                start: 0,
141                end: 5,
142                line: 1,
143                col: 0,
144            },
145        );
146        assert_eq!(loc.text_from(&sources).as_deref(), Some("hello"));
147    }
148}