1use std::collections::HashMap;
13use std::path::{Path, PathBuf};
14
15pub(super) use crate::ast::{Script, Stmt};
16pub(super) use crate::diagnostics::Diagnostic;
17pub(super) use crate::project::DependencyMap;
18pub(super) use crate::stdlib::{self, Module};
19pub(super) use crate::token::Span;
20
21mod diag;
22use diag::*;
23
24#[cfg(test)]
25mod tests;
26
27pub struct Program {
28 pub files: Vec<ProgramFile>,
29 pub init_order: Vec<u32>,
34}
35
36pub struct ProgramFile {
38 pub file_id: u32,
39 pub is_entry: bool,
40 pub name: String,
42 pub path: String,
43 pub source: String,
44 pub script: Script,
45 pub stdlib_imports: Vec<(String, &'static Module)>,
47 pub user_imports: Vec<(String, u32)>,
49}
50
51pub fn load_program(entry_path: &str, entry_source: &str) -> Result<Program, Diagnostic> {
55 load_program_with_deps(entry_path, entry_source, DependencyMap::new())
56}
57
58pub fn load_program_with_deps(
61 entry_path: &str,
62 entry_source: &str,
63 deps: DependencyMap,
64) -> Result<Program, Diagnostic> {
65 let entry_script = crate::parser::parse(entry_path, entry_source)?;
66 let mut loader = Loader {
67 modules: Vec::new(),
68 by_path: HashMap::new(),
69 init_order: Vec::new(),
70 active: Vec::new(),
71 next_id: 1,
72 entry_key: canonical_key(Path::new(entry_path)),
73 deps,
74 };
75 let (stdlib_imports, user_imports) =
76 loader.resolve_imports(entry_path, entry_source, &entry_script)?;
77
78 let entry = ProgramFile {
79 file_id: 0,
80 is_entry: true,
81 name: file_stem(entry_path),
82 path: entry_path.to_string(),
83 source: entry_source.to_string(),
84 script: entry_script,
85 stdlib_imports,
86 user_imports,
87 };
88
89 let init_order = loader.modules.iter().map(|m| m.file_id).collect();
92 let mut slots: Vec<Option<ProgramFile>> = (0..=loader.modules.len()).map(|_| None).collect();
93 slots[0] = Some(entry);
94 for module in loader.modules {
95 let id = module.file_id as usize;
96 slots[id] = Some(module);
97 }
98 let files = slots
99 .into_iter()
100 .map(|s| s.expect("compiler bug: unfilled program file slot"))
101 .collect();
102
103 Ok(Program { files, init_order })
104}
105
106pub fn single_file_program(
110 path: &str,
111 source: &str,
112 script: Script,
113) -> Result<Program, Diagnostic> {
114 let mut stdlib_imports = Vec::new();
115 for stmt in &script.stmts {
116 if let Stmt::Import {
117 module,
118 path: import_path,
119 span,
120 } = stmt
121 {
122 match (import_path, stdlib::module(module)) {
125 (None, Some(m)) => stdlib_imports.push((module.clone(), m)),
126 _ => return Err(unknown_stdlib_module(path, source, module, *span)),
127 }
128 }
129 }
130 let entry = ProgramFile {
131 file_id: 0,
132 is_entry: true,
133 name: file_stem(path),
134 path: path.to_string(),
135 source: source.to_string(),
136 script,
137 stdlib_imports,
138 user_imports: Vec::new(),
139 };
140 Ok(Program {
141 files: vec![entry],
142 init_order: Vec::new(),
143 })
144}
145
146type ResolvedImports = (Vec<(String, &'static Module)>, Vec<(String, u32)>);
148
149struct Loader {
150 modules: Vec<ProgramFile>,
152 by_path: HashMap<PathBuf, u32>,
155 init_order: Vec<u32>,
157 active: Vec<ActiveModule>,
159 next_id: u32,
161 entry_key: PathBuf,
164 deps: DependencyMap,
167}
168
169struct ActiveModule {
172 name: String,
173 key: PathBuf,
174}
175
176impl Loader {
177 fn resolve_imports(
180 &mut self,
181 importer_path: &str,
182 importer_source: &str,
183 script: &Script,
184 ) -> Result<ResolvedImports, Diagnostic> {
185 let dir = Path::new(importer_path).parent();
186 let mut stdlib_imports = Vec::new();
187 let mut user_imports = Vec::new();
188
189 for stmt in &script.stmts {
190 let Stmt::Import { module, path, span } = stmt else {
191 continue;
192 };
193
194 match path {
198 None => {
199 if let Some(entry) = stdlib::module(module) {
200 if module_file_exists(dir, module) {
201 return Err(shadow_diag(importer_path, importer_source, module, *span));
202 }
203 stdlib_imports.push((module.clone(), entry));
204 continue;
205 }
206 }
207 Some(_) if stdlib::module(module).is_some() => {
208 return Err(shadow_diag(importer_path, importer_source, module, *span));
209 }
210 Some(_) => {}
211 }
212
213 let target = match path {
214 Some(raw) => path_import_path(dir, raw),
215 None => match self.dep_entry(importer_path, module) {
216 Some(entry) => {
220 if module_file_exists(dir, module) {
221 return Err(dep_conflict_diag(
222 importer_path,
223 importer_source,
224 module,
225 *span,
226 ));
227 }
228 entry
229 }
230 None => module_path(dir, module),
231 },
232 };
233 let target_id = self.resolve_user_module(
234 importer_path,
235 importer_source,
236 module,
237 path.as_deref(),
238 &target,
239 *span,
240 )?;
241 user_imports.push((module.clone(), target_id));
242 }
243
244 Ok((stdlib_imports, user_imports))
245 }
246
247 fn dep_entry(&self, importer_path: &str, alias: &str) -> Option<PathBuf> {
250 let pkg = self.owning_package(importer_path)?;
251 self.deps.get(&pkg)?.get(alias).cloned()
252 }
253
254 fn owning_package(&self, importer_path: &str) -> Option<PathBuf> {
258 let canon = std::fs::canonicalize(importer_path).ok()?;
259 let mut dir = canon.parent();
260 while let Some(candidate) = dir {
261 if self.deps.contains_key(candidate) {
262 return Some(candidate.to_path_buf());
263 }
264 dir = candidate.parent();
265 }
266 None
267 }
268
269 fn resolve_user_module(
273 &mut self,
274 importer_path: &str,
275 importer_source: &str,
276 name: &str,
277 raw_path: Option<&str>,
278 target: &Path,
279 span: Span,
280 ) -> Result<u32, Diagnostic> {
281 let key = match std::fs::canonicalize(target) {
282 Ok(key) => key,
283 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
284 return Err(match raw_path {
285 Some(raw) => {
286 missing_path_module_diag(importer_path, importer_source, raw, target, span)
287 }
288 None => missing_module_diag(importer_path, importer_source, name, span),
289 });
290 }
291 Err(err) => {
292 return Err(read_error_diag(
293 importer_path,
294 importer_source,
295 name,
296 target,
297 &err,
298 span,
299 ))
300 }
301 };
302
303 if key == self.entry_key {
304 return Err(entry_import_diag(importer_path, importer_source, span));
305 }
306
307 if self.active.iter().any(|m| m.key == key) {
308 let chain: Vec<String> = self.active.iter().map(|m| m.name.clone()).collect();
309 return Err(cycle_diag(
310 importer_path,
311 importer_source,
312 &chain,
313 name,
314 span,
315 ));
316 }
317
318 if let Some(id) = self.by_path.get(&key) {
319 return Ok(*id);
320 }
321
322 self.load_module(name, target, key, importer_path, importer_source, span)
323 }
324
325 fn load_module(
328 &mut self,
329 name: &str,
330 target: &Path,
331 key: PathBuf,
332 importer_path: &str,
333 importer_source: &str,
334 span: Span,
335 ) -> Result<u32, Diagnostic> {
336 let source = match std::fs::read_to_string(target) {
337 Ok(source) => source,
338 Err(err) => {
339 return Err(read_error_diag(
340 importer_path,
341 importer_source,
342 name,
343 target,
344 &err,
345 span,
346 ))
347 }
348 };
349
350 let path_str = target.to_string_lossy().into_owned();
351 let script = crate::parser::parse(&path_str, &source)?;
352
353 let file_id = self.next_id;
354 self.next_id += 1;
355 self.by_path.insert(key.clone(), file_id);
356
357 self.active.push(ActiveModule {
358 name: name.to_string(),
359 key,
360 });
361 let (stdlib_imports, user_imports) = self.resolve_imports(&path_str, &source, &script)?;
362 self.active.pop();
363
364 self.modules.push(ProgramFile {
365 file_id,
366 is_entry: false,
367 name: name.to_string(),
368 path: path_str,
369 source,
370 script,
371 stdlib_imports,
372 user_imports,
373 });
374 self.init_order.push(file_id);
375 Ok(file_id)
376 }
377}
378
379fn module_path(dir: Option<&Path>, name: &str) -> PathBuf {
380 let file = format!("{name}.doge");
381 match dir {
382 Some(dir) => dir.join(file),
383 None => PathBuf::from(file),
384 }
385}
386
387fn path_import_path(dir: Option<&Path>, raw: &str) -> PathBuf {
390 match dir {
391 Some(dir) => dir.join(raw),
392 None => PathBuf::from(raw),
393 }
394}
395
396fn canonical_key(path: &Path) -> PathBuf {
400 std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
401}
402
403fn module_file_exists(dir: Option<&Path>, name: &str) -> bool {
404 module_path(dir, name).is_file()
405}
406
407fn file_stem(path: &str) -> String {
408 Path::new(path)
409 .file_stem()
410 .and_then(|s| s.to_str())
411 .unwrap_or(path)
412 .to_string()
413}