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}
24
25#[derive(Debug)]
27pub struct ParsedModule {
28 pub path: ModulePath,
30 pub file_path: PathBuf,
32 pub source: Arc<str>,
34 pub program: Program,
36}
37
38pub fn load_single_file(path: &Path) -> Result<ModuleTree, Vec<LoadError>> {
40 let source = std::fs::read_to_string(path).map_err(|e| {
41 vec![LoadError::IoError {
42 path: path.to_path_buf(),
43 source: e,
44 }]
45 })?;
46
47 let source_arc: Arc<str> = Arc::from(source.as_str());
48 let lex_result = sage_lexer::lex(&source).map_err(|e| {
49 vec![LoadError::ParseError {
50 file: path.to_path_buf(),
51 errors: vec![format!("{e}")],
52 }]
53 })?;
54
55 let (program, parse_errors) = parse(lex_result.tokens(), Arc::clone(&source_arc));
56
57 if !parse_errors.is_empty() {
58 return Err(vec![LoadError::ParseError {
59 file: path.to_path_buf(),
60 errors: parse_errors.iter().map(|e| format!("{e}")).collect(),
61 }]);
62 }
63
64 let program = program.ok_or_else(|| {
65 vec![LoadError::ParseError {
66 file: path.to_path_buf(),
67 errors: vec!["failed to parse program".to_string()],
68 }]
69 })?;
70
71 let root_path = vec![];
72 let mut modules = HashMap::new();
73 modules.insert(
74 root_path.clone(),
75 ParsedModule {
76 path: root_path.clone(),
77 file_path: path.to_path_buf(),
78 source: source_arc,
79 program,
80 },
81 );
82
83 Ok(ModuleTree {
84 modules,
85 root: root_path,
86 project_root: path
87 .parent()
88 .map(Path::to_path_buf)
89 .unwrap_or_else(|| PathBuf::from(".")),
90 })
91}
92
93pub fn load_project(project_path: &Path) -> Result<ModuleTree, Vec<LoadError>> {
95 let manifest_path = if project_path.is_file() && project_path.ends_with("sage.toml") {
97 project_path.to_path_buf()
98 } else if project_path.is_dir() {
99 project_path.join("sage.toml")
100 } else {
101 return load_single_file(project_path);
103 };
104
105 if !manifest_path.exists() {
106 if project_path.extension().is_some_and(|e| e == "sg") {
108 return load_single_file(project_path);
109 }
110 return Err(vec![LoadError::NoManifest {
111 dir: project_path.to_path_buf(),
112 }]);
113 }
114
115 let manifest = ProjectManifest::load(&manifest_path).map_err(|e| vec![e])?;
116 let project_root = manifest_path.parent().unwrap().to_path_buf();
117 let entry_path = project_root.join(&manifest.project.entry);
118
119 if !entry_path.exists() {
120 return Err(vec![LoadError::MissingEntry { path: entry_path }]);
121 }
122
123 let mut loader = ModuleLoader::new(project_root.clone());
125 let root_path: ModulePath = vec![];
126 loader.load_module(&root_path, &entry_path)?;
127
128 Ok(ModuleTree {
129 modules: loader.modules,
130 root: vec![],
131 project_root,
132 })
133}
134
135struct ModuleLoader {
137 #[allow(dead_code)]
138 project_root: PathBuf,
139 modules: HashMap<ModulePath, ParsedModule>,
140 loading: HashSet<PathBuf>, }
142
143impl ModuleLoader {
144 fn new(project_root: PathBuf) -> Self {
145 Self {
146 project_root,
147 modules: HashMap::new(),
148 loading: HashSet::new(),
149 }
150 }
151
152 fn load_module(&mut self, path: &ModulePath, file_path: &Path) -> Result<(), Vec<LoadError>> {
153 let canonical = file_path
154 .canonicalize()
155 .unwrap_or_else(|_| file_path.to_path_buf());
156
157 if self.loading.contains(&canonical) {
159 let cycle: Vec<String> = self
160 .loading
161 .iter()
162 .map(|p| p.display().to_string())
163 .collect();
164 return Err(vec![LoadError::CircularDependency { cycle }]);
165 }
166
167 if self.modules.contains_key(path) {
169 return Ok(());
170 }
171
172 self.loading.insert(canonical.clone());
173
174 let source = std::fs::read_to_string(file_path).map_err(|e| {
176 vec![LoadError::IoError {
177 path: file_path.to_path_buf(),
178 source: e,
179 }]
180 })?;
181
182 let source_arc: Arc<str> = Arc::from(source.as_str());
183 let lex_result = sage_lexer::lex(&source).map_err(|e| {
184 vec![LoadError::ParseError {
185 file: file_path.to_path_buf(),
186 errors: vec![format!("{e}")],
187 }]
188 })?;
189
190 let (program, parse_errors) = parse(lex_result.tokens(), Arc::clone(&source_arc));
191
192 if !parse_errors.is_empty() {
193 return Err(vec![LoadError::ParseError {
194 file: file_path.to_path_buf(),
195 errors: parse_errors.iter().map(|e| format!("{e}")).collect(),
196 }]);
197 }
198
199 let program = program.ok_or_else(|| {
200 vec![LoadError::ParseError {
201 file: file_path.to_path_buf(),
202 errors: vec!["failed to parse program".to_string()],
203 }]
204 })?;
205
206 let parent_dir = file_path.parent().unwrap();
208 let file_stem = file_path.file_stem().unwrap().to_str().unwrap();
209 let is_mod_file = file_stem == "mod";
210
211 for mod_decl in &program.mod_decls {
212 let child_name = &mod_decl.name.name;
213 let mut child_path = path.clone();
214 child_path.push(child_name.clone());
215
216 let child_file = self.find_module_file(parent_dir, child_name, is_mod_file)?;
218
219 self.load_module(&child_path, &child_file)?;
221 }
222
223 self.loading.remove(&canonical);
224
225 self.modules.insert(
227 path.clone(),
228 ParsedModule {
229 path: path.clone(),
230 file_path: file_path.to_path_buf(),
231 source: source_arc,
232 program,
233 },
234 );
235
236 Ok(())
237 }
238
239 fn find_module_file(
240 &self,
241 parent_dir: &Path,
242 mod_name: &str,
243 _parent_is_mod_file: bool,
244 ) -> Result<PathBuf, Vec<LoadError>> {
245 let sibling = parent_dir.join(format!("{mod_name}.sg"));
249 let nested = parent_dir.join(mod_name).join("mod.sg");
250
251 let sibling_exists = sibling.exists();
252 let nested_exists = nested.exists();
253
254 match (sibling_exists, nested_exists) {
255 (true, true) => Err(vec![LoadError::AmbiguousModule {
256 mod_name: mod_name.to_string(),
257 candidates: vec![sibling, nested],
258 }]),
259 (true, false) => Ok(sibling),
260 (false, true) => Ok(nested),
261 (false, false) => Err(vec![LoadError::FileNotFound {
262 mod_name: mod_name.to_string(),
263 searched: vec![sibling, nested],
264 span: (0, 0).into(),
265 source_code: String::new(),
266 }]),
267 }
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274 use std::fs;
275 use tempfile::TempDir;
276
277 #[test]
278 fn load_single_file_works() {
279 let dir = TempDir::new().unwrap();
280 let file = dir.path().join("test.sg");
281 fs::write(
282 &file,
283 r#"
284agent Main {
285 on start {
286 emit(42);
287 }
288}
289run Main;
290"#,
291 )
292 .unwrap();
293
294 let tree = load_single_file(&file).unwrap();
295 assert_eq!(tree.modules.len(), 1);
296 assert!(tree.modules.contains_key(&vec![]));
297 }
298
299 #[test]
300 fn load_project_with_manifest() {
301 let dir = TempDir::new().unwrap();
302
303 fs::write(
305 dir.path().join("sage.toml"),
306 r#"
307[project]
308name = "test"
309entry = "src/main.sg"
310"#,
311 )
312 .unwrap();
313
314 fs::create_dir_all(dir.path().join("src")).unwrap();
316 fs::write(
317 dir.path().join("src/main.sg"),
318 r#"
319agent Main {
320 on start {
321 emit(0);
322 }
323}
324run Main;
325"#,
326 )
327 .unwrap();
328
329 let tree = load_project(dir.path()).unwrap();
330 assert_eq!(tree.modules.len(), 1);
331 }
332
333 #[test]
334 fn load_project_with_submodule() {
335 let dir = TempDir::new().unwrap();
336
337 fs::write(
339 dir.path().join("sage.toml"),
340 r#"
341[project]
342name = "test"
343entry = "src/main.sg"
344"#,
345 )
346 .unwrap();
347
348 fs::create_dir_all(dir.path().join("src")).unwrap();
350 fs::write(
351 dir.path().join("src/main.sg"),
352 r#"
353mod agents;
354
355agent Main {
356 on start {
357 emit(0);
358 }
359}
360run Main;
361"#,
362 )
363 .unwrap();
364
365 fs::write(
367 dir.path().join("src/agents.sg"),
368 r#"
369pub agent Worker {
370 on start {
371 emit(1);
372 }
373}
374"#,
375 )
376 .unwrap();
377
378 let tree = load_project(dir.path()).unwrap();
379 assert_eq!(tree.modules.len(), 2);
380 assert!(tree.modules.contains_key(&vec![]));
381 assert!(tree.modules.contains_key(&vec!["agents".to_string()]));
382 }
383}