Skip to main content

lemma/parsing/
source.rs

1use crate::parsing::ast::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    Dependency(String),
12    Volatile,
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::Dependency(id) => write!(f, "{id}"),
20            SourceType::Volatile => write!(f, "volatile"),
21        }
22    }
23}
24
25impl SourceType {
26    /// Decode a binding-layer source label (JS/Hex/Java `load` labeled form) into a [`SourceType`].
27    pub fn from_binding_label(label: &str) -> Result<Self, String> {
28        if label.trim().is_empty() {
29            return Err("source label must be non-empty".to_string());
30        }
31        if label.starts_with('@') {
32            return Ok(SourceType::Dependency(label.to_string()));
33        }
34        Ok(SourceType::Path(Arc::new(PathBuf::from(label))))
35    }
36}
37
38/// Positional source location: source id and span.
39///
40/// Text is resolved via `text_from(sources)` when needed. No embedded source text.
41#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
42pub struct Source {
43    /// Source identifier (e.g. filesystem path label)
44    pub source_type: SourceType,
45
46    /// Span in source code
47    pub span: Span,
48}
49
50impl Source {
51    #[must_use]
52    pub fn new(source_type: SourceType, span: Span) -> Self {
53        Self { source_type, span }
54    }
55
56    /// Resolve source text from the sources map.
57    #[must_use]
58    pub fn text_from<'a>(
59        &self,
60        sources: &'a HashMap<SourceType, String>,
61    ) -> Option<std::borrow::Cow<'a, str>> {
62        let s = sources.get(&self.source_type)?;
63        s.get(self.span.start..self.span.end)
64            .map(std::borrow::Cow::Borrowed)
65    }
66
67    /// Extract the source text from the given source string.
68    ///
69    /// Returns `None` if the span is out of bounds.
70    #[must_use]
71    pub fn extract_text(&self, source: &str) -> Option<String> {
72        let bytes = source.as_bytes();
73        if self.span.start < bytes.len() && self.span.end <= bytes.len() {
74            Some(String::from_utf8_lossy(&bytes[self.span.start..self.span.end]).to_string())
75        } else {
76            None
77        }
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84    use std::collections::HashMap;
85
86    #[test]
87    fn test_extract_text_valid() {
88        let source = "hello world";
89        let span = Span {
90            start: 0,
91            end: 5,
92            line: 1,
93            col: 0,
94        };
95        let loc = Source::new(SourceType::Volatile, span);
96        assert_eq!(loc.extract_text(source), Some("hello".to_string()));
97    }
98
99    #[test]
100    fn test_extract_text_full_string() {
101        let source = "hello world";
102        let span = Span {
103            start: 0,
104            end: 11,
105            line: 1,
106            col: 0,
107        };
108        let loc = Source::new(SourceType::Volatile, span);
109        assert_eq!(loc.extract_text(source), Some("hello world".to_string()));
110    }
111
112    #[test]
113    fn test_extract_text_empty() {
114        let source = "hello world";
115        let span = Span {
116            start: 5,
117            end: 5,
118            line: 1,
119            col: 5,
120        };
121        let loc = Source::new(SourceType::Volatile, span);
122        assert_eq!(loc.extract_text(source), Some("".to_string()));
123    }
124
125    #[test]
126    fn test_extract_text_out_of_bounds_start() {
127        let source = "hello";
128        let span = Span {
129            start: 10,
130            end: 15,
131            line: 1,
132            col: 10,
133        };
134        let loc = Source::new(SourceType::Volatile, span);
135        assert_eq!(loc.extract_text(source), None);
136    }
137
138    #[test]
139    fn test_extract_text_out_of_bounds_end() {
140        let source = "hello";
141        let span = Span {
142            start: 0,
143            end: 10,
144            line: 1,
145            col: 0,
146        };
147        let loc = Source::new(SourceType::Volatile, span);
148        assert_eq!(loc.extract_text(source), None);
149    }
150
151    #[test]
152    fn test_extract_text_unicode() {
153        let source = "hello 世界";
154        let span = Span {
155            start: 6,
156            end: 12,
157            line: 1,
158            col: 6,
159        };
160        let loc = Source::new(SourceType::Volatile, span);
161        assert_eq!(loc.extract_text(source), Some("世界".to_string()));
162    }
163
164    #[test]
165    fn test_text_from() {
166        let mut sources = HashMap::new();
167        let st = SourceType::Path(Arc::new(PathBuf::from("test.lemma")));
168        sources.insert(st.clone(), "hello world".to_string());
169        let loc = Source::new(
170            st,
171            Span {
172                start: 0,
173                end: 5,
174                line: 1,
175                col: 0,
176            },
177        );
178        assert_eq!(loc.text_from(&sources).as_deref(), Some("hello"));
179    }
180
181    #[test]
182    fn from_binding_label_path_and_dependency() {
183        assert_eq!(
184            SourceType::from_binding_label("app.lemma").unwrap(),
185            SourceType::Path(Arc::new(PathBuf::from("app.lemma")))
186        );
187        assert_eq!(
188            SourceType::from_binding_label("@iso/countries").unwrap(),
189            SourceType::Dependency("@iso/countries".to_string())
190        );
191        assert_eq!(
192            SourceType::from_binding_label("volatile").unwrap(),
193            SourceType::Path(Arc::new(PathBuf::from("volatile")))
194        );
195        assert!(SourceType::from_binding_label("").is_err());
196        assert!(SourceType::from_binding_label("   ").is_err());
197    }
198}