Skip to main content

lemma/parsing/
source.rs

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