1use std::collections::{HashMap, HashSet};
7use std::path::{Path, PathBuf};
8
9use crate::ast;
10use crate::parser::ParseError;
11
12#[derive(Debug)]
14pub struct ModuleGraph {
15 pub root: PathBuf,
17 pub modules: HashMap<PathBuf, ast::File>,
19 pub order: Vec<PathBuf>,
21}
22
23#[derive(Debug, thiserror::Error)]
25pub enum ResolveError {
26 #[error("parse error in {path}: {source}")]
27 Parse {
28 path: PathBuf,
29 #[source]
30 source: ParseError,
31 },
32 #[error("cannot read {path}: {source}")]
33 Io {
34 path: PathBuf,
35 #[source]
36 source: std::io::Error,
37 },
38 #[error("import cycle detected: {cycle}")]
39 Cycle { cycle: String },
40 #[error("module not found: `use {module_name}` — expected file {expected_path}")]
41 ModuleNotFound {
42 module_name: String,
43 expected_path: PathBuf,
44 },
45}
46
47pub fn resolve(root_path: &Path) -> Result<ModuleGraph, ResolveError> {
52 let root_path = std::fs::canonicalize(root_path).map_err(|e| ResolveError::Io {
53 path: root_path.to_path_buf(),
54 source: e,
55 })?;
56
57 let mut modules: HashMap<PathBuf, ast::File> = HashMap::new();
58 let mut order: Vec<PathBuf> = Vec::new();
59 let mut visiting: HashSet<PathBuf> = HashSet::new();
60
61 resolve_recursive(&root_path, &mut modules, &mut order, &mut visiting)?;
62
63 Ok(ModuleGraph {
64 root: root_path,
65 modules,
66 order,
67 })
68}
69
70fn resolve_recursive(
71 path: &Path,
72 modules: &mut HashMap<PathBuf, ast::File>,
73 order: &mut Vec<PathBuf>,
74 visiting: &mut HashSet<PathBuf>,
75) -> Result<(), ResolveError> {
76 if modules.contains_key(path) {
78 return Ok(());
79 }
80
81 if !visiting.insert(path.to_path_buf()) {
83 let cycle = path.display().to_string();
84 return Err(ResolveError::Cycle { cycle });
85 }
86
87 let source = std::fs::read_to_string(path).map_err(|e| ResolveError::Io {
89 path: path.to_path_buf(),
90 source: e,
91 })?;
92
93 let file = crate::parse_file(&source).map_err(|e| ResolveError::Parse {
94 path: path.to_path_buf(),
95 source: e,
96 })?;
97
98 let dir = path.parent().unwrap_or(Path::new("."));
100 for use_decl in &file.imports {
101 let import_filename = format!("{}.intent", use_decl.module_name);
102 let import_path = dir.join(&import_filename);
103
104 let canonical =
105 std::fs::canonicalize(&import_path).map_err(|_| ResolveError::ModuleNotFound {
106 module_name: use_decl.module_name.clone(),
107 expected_path: import_path.clone(),
108 })?;
109
110 resolve_recursive(&canonical, modules, order, visiting)?;
111 }
112
113 visiting.remove(path);
115 order.push(path.to_path_buf());
116 modules.insert(path.to_path_buf(), file);
117
118 Ok(())
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124 use std::fs;
125
126 fn setup_temp_dir() -> tempfile::TempDir {
127 tempfile::tempdir().unwrap()
128 }
129
130 #[test]
131 fn resolve_single_file_no_imports() {
132 let dir = setup_temp_dir();
133 let root = dir.path().join("main.intent");
134 fs::write(&root, "module Main\n\nentity Foo {\n id: UUID\n}\n").unwrap();
135
136 let graph = resolve(&root).unwrap();
137 assert_eq!(graph.modules.len(), 1);
138 assert_eq!(graph.order.len(), 1);
139 }
140
141 #[test]
142 fn resolve_two_modules() {
143 let dir = setup_temp_dir();
144
145 fs::write(
146 dir.path().join("Types.intent"),
147 "module Types\n\nentity Account {\n id: UUID\n balance: Int\n}\n",
148 )
149 .unwrap();
150
151 fs::write(
152 dir.path().join("Main.intent"),
153 "module Main\n\nuse Types\n\naction Transfer {\n from: Account\n to: Account\n}\n",
154 )
155 .unwrap();
156
157 let graph = resolve(&dir.path().join("Main.intent")).unwrap();
158 assert_eq!(graph.modules.len(), 2);
159 let names: Vec<&str> = graph
161 .order
162 .iter()
163 .map(|p| graph.modules[p].module.name.as_str())
164 .collect();
165 assert_eq!(names, vec!["Types", "Main"]);
166 }
167
168 #[test]
169 fn resolve_selective_import() {
170 let dir = setup_temp_dir();
171
172 fs::write(
173 dir.path().join("Types.intent"),
174 "module Types\n\nentity Account {\n id: UUID\n}\n\nentity User {\n name: String\n}\n",
175 )
176 .unwrap();
177
178 fs::write(
179 dir.path().join("Main.intent"),
180 "module Main\n\nuse Types.Account\n\naction Foo {\n a: Account\n}\n",
181 )
182 .unwrap();
183
184 let graph = resolve(&dir.path().join("Main.intent")).unwrap();
185 assert_eq!(graph.modules.len(), 2);
186 let main_file =
188 &graph.modules[&std::fs::canonicalize(dir.path().join("Main.intent")).unwrap()];
189 assert_eq!(main_file.imports[0].module_name, "Types");
190 assert_eq!(main_file.imports[0].item.as_deref(), Some("Account"));
191 }
192
193 #[test]
194 fn resolve_cycle_detected() {
195 let dir = setup_temp_dir();
196
197 fs::write(dir.path().join("A.intent"), "module A\n\nuse B\n").unwrap();
198
199 fs::write(dir.path().join("B.intent"), "module B\n\nuse A\n").unwrap();
200
201 let err = resolve(&dir.path().join("A.intent")).unwrap_err();
202 assert!(matches!(err, ResolveError::Cycle { .. }));
203 }
204
205 #[test]
206 fn resolve_module_not_found() {
207 let dir = setup_temp_dir();
208
209 fs::write(
210 dir.path().join("Main.intent"),
211 "module Main\n\nuse NonExistent\n",
212 )
213 .unwrap();
214
215 let err = resolve(&dir.path().join("Main.intent")).unwrap_err();
216 assert!(matches!(err, ResolveError::ModuleNotFound { .. }));
217 }
218
219 #[test]
220 fn resolve_transitive_imports() {
221 let dir = setup_temp_dir();
222
223 fs::write(
224 dir.path().join("Base.intent"),
225 "module Base\n\nentity Id {\n value: UUID\n}\n",
226 )
227 .unwrap();
228
229 fs::write(
230 dir.path().join("Types.intent"),
231 "module Types\n\nuse Base\n\nentity Account {\n id: UUID\n}\n",
232 )
233 .unwrap();
234
235 fs::write(
236 dir.path().join("Main.intent"),
237 "module Main\n\nuse Types\n\naction Foo {\n a: Account\n}\n",
238 )
239 .unwrap();
240
241 let graph = resolve(&dir.path().join("Main.intent")).unwrap();
242 assert_eq!(graph.modules.len(), 3);
243 let names: Vec<&str> = graph
244 .order
245 .iter()
246 .map(|p| graph.modules[p].module.name.as_str())
247 .collect();
248 assert_eq!(names, vec!["Base", "Types", "Main"]);
249 }
250
251 #[test]
252 fn resolve_diamond_dependency() {
253 let dir = setup_temp_dir();
254
255 fs::write(
256 dir.path().join("Base.intent"),
257 "module Base\n\nentity Id {\n value: UUID\n}\n",
258 )
259 .unwrap();
260
261 fs::write(
262 dir.path().join("Left.intent"),
263 "module Left\n\nuse Base\n\nentity Foo {\n id: UUID\n}\n",
264 )
265 .unwrap();
266
267 fs::write(
268 dir.path().join("Right.intent"),
269 "module Right\n\nuse Base\n\nentity Bar {\n id: UUID\n}\n",
270 )
271 .unwrap();
272
273 fs::write(
274 dir.path().join("Main.intent"),
275 "module Main\n\nuse Left\nuse Right\n",
276 )
277 .unwrap();
278
279 let graph = resolve(&dir.path().join("Main.intent")).unwrap();
280 assert_eq!(graph.modules.len(), 4);
281 let names: Vec<&str> = graph
283 .order
284 .iter()
285 .map(|p| graph.modules[p].module.name.as_str())
286 .collect();
287 assert_eq!(names[0], "Base");
288 assert!(names.contains(&"Left"));
289 assert!(names.contains(&"Right"));
290 assert_eq!(names[3], "Main");
291 }
292}