1use crate::error::LoadError;
4use crate::manifest::ProjectManifest;
5use sage_parser::ast::Program;
6use sage_parser::parse;
7use std::collections::{HashMap, HashSet};
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10
11pub type ModulePath = Vec<String>;
13
14#[derive(Debug)]
16pub struct ModuleTree {
17 pub modules: HashMap<ModulePath, ParsedModule>,
19 pub root: ModulePath,
21 pub project_root: PathBuf,
23 pub external_roots: HashMap<String, PathBuf>,
26}
27
28#[derive(Debug)]
30pub struct ParsedModule {
31 pub path: ModulePath,
33 pub file_path: PathBuf,
35 pub source: Arc<str>,
37 pub program: Program,
39}
40
41pub fn load_single_file(path: &Path) -> Result<ModuleTree, Vec<LoadError>> {
43 let source = std::fs::read_to_string(path).map_err(|e| {
44 vec![LoadError::IoError {
45 path: path.to_path_buf(),
46 source: e,
47 }]
48 })?;
49
50 let source_arc: Arc<str> = Arc::from(source.as_str());
51 let lex_result = sage_lexer::lex(&source).map_err(|e| {
52 vec![LoadError::ParseError {
53 file: path.to_path_buf(),
54 errors: vec![format!("{e}")],
55 }]
56 })?;
57
58 let (program, parse_errors) = parse(lex_result.tokens(), Arc::clone(&source_arc));
59
60 if !parse_errors.is_empty() {
61 return Err(vec![LoadError::ParseError {
62 file: path.to_path_buf(),
63 errors: parse_errors.iter().map(|e| format!("{e}")).collect(),
64 }]);
65 }
66
67 let program = program.ok_or_else(|| {
68 vec![LoadError::ParseError {
69 file: path.to_path_buf(),
70 errors: vec!["failed to parse program".to_string()],
71 }]
72 })?;
73
74 let root_path = vec![];
75 let mut modules = HashMap::new();
76 modules.insert(
77 root_path.clone(),
78 ParsedModule {
79 path: root_path.clone(),
80 file_path: path.to_path_buf(),
81 source: source_arc,
82 program,
83 },
84 );
85
86 Ok(ModuleTree {
87 modules,
88 root: root_path,
89 project_root: path
90 .parent()
91 .map(Path::to_path_buf)
92 .unwrap_or_else(|| PathBuf::from(".")),
93 external_roots: HashMap::new(),
94 })
95}
96
97pub fn load_project(project_path: &Path) -> Result<ModuleTree, Vec<LoadError>> {
101 let manifest_path = if project_path.is_file() && project_path.ends_with("sage.toml") {
103 project_path.to_path_buf()
104 } else if project_path.is_dir() {
105 project_path.join("sage.toml")
106 } else {
107 return load_single_file(project_path);
109 };
110
111 if !manifest_path.exists() {
112 if project_path.extension().is_some_and(|e| e == "sg") {
114 return load_single_file(project_path);
115 }
116 return Err(vec![LoadError::NoManifest {
117 dir: project_path.to_path_buf(),
118 }]);
119 }
120
121 let manifest = ProjectManifest::load(&manifest_path).map_err(|e| vec![e])?;
122 let project_root = manifest_path.parent().unwrap().to_path_buf();
123 let entry_path = project_root.join(&manifest.project.entry);
124
125 if !entry_path.exists() {
126 return Err(vec![LoadError::MissingEntry { path: entry_path }]);
127 }
128
129 let mut loader = ModuleLoader::new(project_root.clone());
131 let root_path: ModulePath = vec![];
132 loader.load_module(&root_path, &entry_path)?;
133
134 Ok(ModuleTree {
135 modules: loader.modules,
136 root: vec![],
137 project_root,
138 external_roots: HashMap::new(),
139 })
140}
141
142pub fn load_project_with_packages(
150 project_path: &Path,
151) -> Result<(ModuleTree, bool), Vec<LoadError>> {
152 use sage_package::{check_lock_freshness, install_from_lock, resolve_dependencies, LockFile};
153
154 let manifest_path = if project_path.is_file() && project_path.ends_with("sage.toml") {
156 project_path.to_path_buf()
157 } else if project_path.is_dir() {
158 project_path.join("sage.toml")
159 } else {
160 let tree = load_single_file(project_path)?;
162 return Ok((tree, false));
163 };
164
165 if !manifest_path.exists() {
166 if project_path.extension().is_some_and(|e| e == "sg") {
167 let tree = load_single_file(project_path)?;
168 return Ok((tree, false));
169 }
170 return Err(vec![LoadError::NoManifest {
171 dir: project_path.to_path_buf(),
172 }]);
173 }
174
175 let manifest = ProjectManifest::load(&manifest_path).map_err(|e| vec![e])?;
176 let project_root = manifest_path.parent().unwrap().to_path_buf();
177
178 let deps = manifest.parse_dependencies().map_err(|e| vec![e])?;
180
181 let external_roots = if deps.is_empty() {
183 HashMap::new()
184 } else {
185 let lock_path = project_root.join("sage.lock");
186 let packages = if lock_path.exists() {
187 let lock = LockFile::load(&lock_path)
188 .map_err(|e| vec![LoadError::PackageError { source: e }])?;
189 if check_lock_freshness(&deps, &lock) {
190 install_from_lock(&lock).map_err(|e| vec![LoadError::PackageError { source: e }])?
192 } else {
193 let resolved = resolve_dependencies(&project_root, &deps, Some(&lock))
195 .map_err(|e| vec![LoadError::PackageError { source: e }])?;
196 resolved.packages
197 }
198 } else {
199 let resolved = resolve_dependencies(&project_root, &deps, None)
201 .map_err(|e| vec![LoadError::PackageError { source: e }])?;
202 resolved.packages
203 };
204
205 packages
206 .into_iter()
207 .map(|(name, pkg)| (name, pkg.path))
208 .collect()
209 };
210
211 let entry_path = project_root.join(&manifest.project.entry);
213 if !entry_path.exists() {
214 return Err(vec![LoadError::MissingEntry { path: entry_path }]);
215 }
216
217 let mut loader = ModuleLoader::new(project_root.clone());
218 let root_path: ModulePath = vec![];
219 loader.load_module(&root_path, &entry_path)?;
220
221 let installed = !external_roots.is_empty();
222
223 Ok((
224 ModuleTree {
225 modules: loader.modules,
226 root: vec![],
227 project_root,
228 external_roots,
229 },
230 installed,
231 ))
232}
233
234struct ModuleLoader {
236 #[allow(dead_code)]
237 project_root: PathBuf,
238 modules: HashMap<ModulePath, ParsedModule>,
239 loading: HashSet<PathBuf>, }
241
242impl ModuleLoader {
243 fn new(project_root: PathBuf) -> Self {
244 Self {
245 project_root,
246 modules: HashMap::new(),
247 loading: HashSet::new(),
248 }
249 }
250
251 fn load_module(&mut self, path: &ModulePath, file_path: &Path) -> Result<(), Vec<LoadError>> {
252 let canonical = file_path
253 .canonicalize()
254 .unwrap_or_else(|_| file_path.to_path_buf());
255
256 if self.loading.contains(&canonical) {
258 let cycle: Vec<String> = self
259 .loading
260 .iter()
261 .map(|p| p.display().to_string())
262 .collect();
263 return Err(vec![LoadError::CircularDependency { cycle }]);
264 }
265
266 if self.modules.contains_key(path) {
268 return Ok(());
269 }
270
271 self.loading.insert(canonical.clone());
272
273 let source = std::fs::read_to_string(file_path).map_err(|e| {
275 vec![LoadError::IoError {
276 path: file_path.to_path_buf(),
277 source: e,
278 }]
279 })?;
280
281 let source_arc: Arc<str> = Arc::from(source.as_str());
282 let lex_result = sage_lexer::lex(&source).map_err(|e| {
283 vec![LoadError::ParseError {
284 file: file_path.to_path_buf(),
285 errors: vec![format!("{e}")],
286 }]
287 })?;
288
289 let (program, parse_errors) = parse(lex_result.tokens(), Arc::clone(&source_arc));
290
291 if !parse_errors.is_empty() {
292 return Err(vec![LoadError::ParseError {
293 file: file_path.to_path_buf(),
294 errors: parse_errors.iter().map(|e| format!("{e}")).collect(),
295 }]);
296 }
297
298 let program = program.ok_or_else(|| {
299 vec![LoadError::ParseError {
300 file: file_path.to_path_buf(),
301 errors: vec!["failed to parse program".to_string()],
302 }]
303 })?;
304
305 let parent_dir = file_path.parent().unwrap();
307 let file_stem = file_path.file_stem().unwrap().to_str().unwrap();
308 let is_mod_file = file_stem == "mod";
309
310 for mod_decl in &program.mod_decls {
311 let child_name = &mod_decl.name.name;
312 let mut child_path = path.clone();
313 child_path.push(child_name.clone());
314
315 let child_file = self.find_module_file(parent_dir, child_name, is_mod_file)?;
317
318 self.load_module(&child_path, &child_file)?;
320 }
321
322 self.loading.remove(&canonical);
323
324 self.modules.insert(
326 path.clone(),
327 ParsedModule {
328 path: path.clone(),
329 file_path: file_path.to_path_buf(),
330 source: source_arc,
331 program,
332 },
333 );
334
335 Ok(())
336 }
337
338 fn find_module_file(
339 &self,
340 parent_dir: &Path,
341 mod_name: &str,
342 _parent_is_mod_file: bool,
343 ) -> Result<PathBuf, Vec<LoadError>> {
344 let sibling = parent_dir.join(format!("{mod_name}.sg"));
348 let nested = parent_dir.join(mod_name).join("mod.sg");
349
350 let sibling_exists = sibling.exists();
351 let nested_exists = nested.exists();
352
353 match (sibling_exists, nested_exists) {
354 (true, true) => Err(vec![LoadError::AmbiguousModule {
355 mod_name: mod_name.to_string(),
356 candidates: vec![sibling, nested],
357 }]),
358 (true, false) => Ok(sibling),
359 (false, true) => Ok(nested),
360 (false, false) => Err(vec![LoadError::FileNotFound {
361 mod_name: mod_name.to_string(),
362 searched: vec![sibling, nested],
363 span: (0, 0).into(),
364 source_code: String::new(),
365 }]),
366 }
367 }
368}
369
370#[cfg(test)]
371mod tests {
372 use super::*;
373 use std::fs;
374 use tempfile::TempDir;
375
376 #[test]
377 fn load_single_file_works() {
378 let dir = TempDir::new().unwrap();
379 let file = dir.path().join("test.sg");
380 fs::write(
381 &file,
382 r#"
383agent Main {
384 on start {
385 emit(42);
386 }
387}
388run Main;
389"#,
390 )
391 .unwrap();
392
393 let tree = load_single_file(&file).unwrap();
394 assert_eq!(tree.modules.len(), 1);
395 assert!(tree.modules.contains_key(&vec![]));
396 }
397
398 #[test]
399 fn load_project_with_manifest() {
400 let dir = TempDir::new().unwrap();
401
402 fs::write(
404 dir.path().join("sage.toml"),
405 r#"
406[project]
407name = "test"
408entry = "src/main.sg"
409"#,
410 )
411 .unwrap();
412
413 fs::create_dir_all(dir.path().join("src")).unwrap();
415 fs::write(
416 dir.path().join("src/main.sg"),
417 r#"
418agent Main {
419 on start {
420 emit(0);
421 }
422}
423run Main;
424"#,
425 )
426 .unwrap();
427
428 let tree = load_project(dir.path()).unwrap();
429 assert_eq!(tree.modules.len(), 1);
430 }
431
432 #[test]
433 fn load_project_with_submodule() {
434 let dir = TempDir::new().unwrap();
435
436 fs::write(
438 dir.path().join("sage.toml"),
439 r#"
440[project]
441name = "test"
442entry = "src/main.sg"
443"#,
444 )
445 .unwrap();
446
447 fs::create_dir_all(dir.path().join("src")).unwrap();
449 fs::write(
450 dir.path().join("src/main.sg"),
451 r#"
452mod agents;
453
454agent Main {
455 on start {
456 emit(0);
457 }
458}
459run Main;
460"#,
461 )
462 .unwrap();
463
464 fs::write(
466 dir.path().join("src/agents.sg"),
467 r#"
468pub agent Worker {
469 on start {
470 emit(1);
471 }
472}
473"#,
474 )
475 .unwrap();
476
477 let tree = load_project(dir.path()).unwrap();
478 assert_eq!(tree.modules.len(), 2);
479 assert!(tree.modules.contains_key(&vec![]));
480 assert!(tree.modules.contains_key(&vec!["agents".to_string()]));
481 }
482}