1use crate::compiler_error::{CompilerFailure, CompilerStage};
9use crate::span::{FileId, LineIndex, Span};
10
11#[derive(Debug)]
12pub enum SourceError {
13 InvalidSpan { span: Span, reason: &'static str },
14 InvalidOffset { offset: u32 },
15 InvalidPosition { line: u32, col: u32 },
16 UnknownFile { file: FileId },
17 SourceLimit { len: usize },
18 FileLimit { len: usize },
19 Allocation(std::collections::TryReserveError),
20}
21
22impl SourceError {
23 pub const MAX_SOURCE_BYTES: usize = (u32::MAX - 4) as usize;
24
25 pub fn check_source_len(len: usize) -> Result<(), Self> {
26 if len > Self::MAX_SOURCE_BYTES {
27 return Err(Self::SourceLimit { len });
28 }
29 Ok(())
30 }
31
32 pub fn into_compiler_failure(self, stage: CompilerStage) -> CompilerFailure {
33 let message = self.to_string();
34 match self {
35 Self::SourceLimit { .. } | Self::FileLimit { .. } | Self::Allocation(_) => {
36 CompilerFailure::Limit {
37 stage,
38 span: None,
39 message,
40 help: vec!["split the source into smaller modules".into()],
41 }
42 }
43 _ => CompilerFailure::Internal {
44 stage,
45 span: None,
46 message,
47 },
48 }
49 }
50}
51
52impl std::fmt::Display for SourceError {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 match self {
55 Self::InvalidSpan { span, reason } => {
56 write!(f, "invalid source span {span:?}: {reason}")
57 }
58 Self::InvalidOffset { offset } => write!(f, "invalid source byte offset {offset}"),
59 Self::InvalidPosition { line, col } => {
60 write!(f, "invalid source position {line}:{col}")
61 }
62 Self::UnknownFile { file } => write!(f, "unknown source file {}", file.0),
63 Self::SourceLimit { len } => {
64 write!(f, "source length {len} exceeds the supported byte limit")
65 }
66 Self::FileLimit { len } => {
67 write!(f, "source file count {len} reaches the reserved file IDs")
68 }
69 Self::Allocation(error) => write!(f, "cannot reserve source storage: {error}"),
70 }
71 }
72}
73
74impl std::error::Error for SourceError {}
75
76#[derive(
80 Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
81)]
82pub struct ModulePath(String);
83
84impl ModulePath {
85 pub fn as_str(&self) -> &str {
86 &self.0
87 }
88
89 pub fn resolve_relative(&self, specifier: &str) -> Result<ModulePath, RelativeImportError> {
105 let mut stack: Vec<&str> = self
106 .0
107 .rsplit_once('/')
108 .map_or(Vec::new(), |(dir, _last)| dir.split('/').collect());
109 for segment in specifier.split('/') {
110 match segment {
111 "" | "." => {}
112 ".." => {
113 stack.pop().ok_or(RelativeImportError::EscapesRoot)?;
114 }
115 other => stack.push(other),
116 }
117 }
118 let joined = stack.join("/");
119 let canonical = joined
120 .strip_suffix(".ts")
121 .or_else(|| joined.strip_suffix(".subm"))
122 .unwrap_or(&joined);
123 Ok(ModulePath(canonical.to_string()))
124 }
125}
126
127#[derive(Clone, Copy, Debug, PartialEq, Eq)]
129pub enum RelativeImportError {
130 EscapesRoot,
132}
133
134pub fn is_relative_specifier(specifier: &str) -> bool {
139 specifier.starts_with("./") || specifier.starts_with("../")
140}
141
142impl From<&str> for ModulePath {
143 fn from(s: &str) -> Self {
144 ModulePath(s.to_string())
145 }
146}
147
148impl From<String> for ModulePath {
149 fn from(s: String) -> Self {
150 ModulePath(s)
151 }
152}
153
154impl std::fmt::Display for ModulePath {
155 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156 f.write_str(&self.0)
157 }
158}
159
160pub struct SourceFile {
163 pub path: ModulePath,
164 id: FileId,
165 line_index: LineIndex,
166}
167
168impl SourceFile {
169 pub fn text(&self) -> &str {
170 self.line_index.source()
171 }
172
173 pub fn span_text(&self, span: Span) -> Result<&str, SourceError> {
174 span.text(self.text(), self.id)
175 }
176
177 pub fn line_index(&self) -> &LineIndex {
178 &self.line_index
179 }
180}
181
182pub(crate) fn package_frame_path(package: &str, module: &str) -> String {
188 format!("{package}/{module}")
189}
190
191#[derive(Default)]
193pub struct Sources {
194 files: Vec<SourceFile>,
195}
196
197impl Sources {
198 pub fn new() -> Self {
199 Self::default()
200 }
201
202 pub fn add(
204 &mut self,
205 path: impl Into<ModulePath>,
206 text: impl AsRef<str>,
207 ) -> Result<FileId, SourceError> {
208 let id = next_file_id(self.files.len())?;
209 let line_index = LineIndex::new(text.as_ref())?;
210 self.files.try_reserve(1).map_err(SourceError::Allocation)?;
211 self.files.push(SourceFile {
212 path: path.into(),
213 id,
214 line_index,
215 });
216 Ok(id)
217 }
218
219 pub fn add_package_module(
222 &mut self,
223 package: &str,
224 module: &str,
225 text: impl AsRef<str>,
226 ) -> Result<FileId, SourceError> {
227 self.add(package_frame_path(package, module), text)
228 }
229
230 pub fn get(&self, id: FileId) -> Option<&SourceFile> {
231 self.files.get(id.0 as usize)
232 }
233
234 pub fn find_path(&self, path: &str) -> Option<(FileId, &SourceFile)> {
235 self.files
236 .iter()
237 .find(|file| file.path.as_str() == path)
238 .map(|file| (file.id, file))
239 }
240
241 pub fn single(
243 path: impl Into<ModulePath>,
244 text: impl AsRef<str>,
245 ) -> Result<(Self, FileId), SourceError> {
246 let mut sources = Self::new();
247 let id = sources.add(path, text)?;
248 Ok((sources, id))
249 }
250}
251
252fn next_file_id(len: usize) -> Result<FileId, SourceError> {
253 u32::try_from(len)
254 .ok()
255 .filter(|id| *id < FileId::FIRST_RESERVED)
256 .map(FileId)
257 .ok_or(SourceError::FileLimit { len })
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263
264 #[test]
265 fn checked_source_ids_stop_before_reserved_range() {
266 let limit = FileId::FIRST_RESERVED as usize;
267 assert_eq!(
268 next_file_id(limit - 1).unwrap().0,
269 FileId::FIRST_RESERVED - 1
270 );
271 assert!(matches!(
272 next_file_id(limit),
273 Err(SourceError::FileLimit { .. })
274 ));
275 assert!(next_file_id(usize::MAX).is_err());
276 }
277
278 #[test]
279 fn add_assigns_sequential_ids() {
280 let mut sources = Sources::new();
281 let a = sources.add("a.subm", "let x = 1;").unwrap();
282 let b = sources.add("b.subm", "let y = 2;").unwrap();
283 assert_eq!(a, FileId(0));
284 assert_eq!(b, FileId(1));
285 }
286
287 #[test]
288 fn get_resolves_path_and_text() {
289 let (sources, id) = Sources::single("script.subm", "let x = 1;").unwrap();
290 let file = sources.get(id).unwrap();
291 assert_eq!(file.path.as_str(), "script.subm");
292 assert_eq!(file.text(), "let x = 1;");
293 }
294
295 #[test]
296 fn get_unknown_file_is_none() {
297 let sources = Sources::new();
298 assert!(sources.get(FileId(0)).is_none());
299 assert!(sources.get(FileId::PRELUDE).is_none());
301 }
302
303 #[test]
304 fn find_path_returns_file_id_and_source() {
305 let mut sources = Sources::new();
306 sources.add("lib", "export const a = 1;").unwrap();
307 let util = sources.add("util", "export const b = 2;").unwrap();
308
309 let (id, file) = sources.find_path("util").expect("find util");
310
311 assert_eq!(id, util);
312 assert_eq!(file.text(), "export const b = 2;");
313 assert!(sources.find_path("missing").is_none());
314 }
315
316 fn resolve(importer: &str, specifier: &str) -> Result<String, RelativeImportError> {
317 ModulePath::from(importer)
318 .resolve_relative(specifier)
319 .map(|m| m.as_str().to_string())
320 }
321
322 #[test]
323 fn resolve_relative_canonicalizes_against_importer_dir() {
324 assert_eq!(resolve("lib", "./util").unwrap(), "util");
325 assert_eq!(
326 resolve("internal/handlers", "./util").unwrap(),
327 "internal/util"
328 );
329 assert_eq!(resolve("internal/handlers", "../math").unwrap(), "math");
330 assert_eq!(
331 resolve("internal/handlers", "../shared/x").unwrap(),
332 "shared/x"
333 );
334 assert_eq!(resolve("a/b/c", "./d/../e").unwrap(), "a/b/e");
335 }
336
337 #[test]
338 fn resolve_relative_strips_subm_suffix() {
339 assert_eq!(resolve("util", "./mod.subm").unwrap(), "mod");
340 }
341
342 #[test]
343 fn resolve_relative_strips_ts_suffix() {
344 assert_eq!(resolve("util", "./mod.ts").unwrap(), "mod");
345 }
346
347 #[test]
348 fn resolve_relative_rejects_escaping_root() {
349 assert_eq!(
350 resolve("lib", "../x"),
351 Err(RelativeImportError::EscapesRoot)
352 );
353 assert_eq!(
354 resolve("util", "../../x"),
355 Err(RelativeImportError::EscapesRoot)
356 );
357 }
358
359 #[test]
360 fn is_relative_specifier_classifies_dot_prefixes_only() {
361 assert!(is_relative_specifier("./util"));
362 assert!(is_relative_specifier("../math"));
363 assert!(!is_relative_specifier("submilli:http"));
364 assert!(!is_relative_specifier("@org/pkg"));
365 assert!(!is_relative_specifier("@mcp/linear"));
366 assert!(!is_relative_specifier("util"));
367 assert!(!is_relative_specifier("."));
369 assert!(!is_relative_specifier(".."));
370 }
371}