1#![forbid(unsafe_code)]
24
25mod exports;
26mod graph;
27mod imports;
28mod resolve;
29mod stdlib;
30
31use std::collections::HashMap;
32
33pub use exports::ModuleExports;
34pub use graph::{ModuleGraph, ModuleId, ModuleNode};
35pub use imports::{ImportKind, ResolvedImport};
36use maat_ast::{Program, Stmt, fold_constants};
37use maat_bytecode::Bytecode;
38use maat_codegen::Compiler;
39use maat_errors::{ModuleError, ModuleErrorKind};
40use maat_span::Span;
41use maat_types::TypeChecker;
42pub use resolve::resolve_module_graph;
43
44pub type ModuleResult<T> = std::result::Result<T, ModuleError>;
46
47type TypeCheckResult = (
49 HashMap<ModuleId, ModuleExports>,
50 HashMap<ModuleId, Vec<ResolvedImport>>,
51);
52
53pub fn check_and_compile(graph: &mut ModuleGraph) -> ModuleResult<Bytecode> {
79 let topo_order = graph.topo_order().to_vec();
80 let (exports, cached_imports) = type_check_modules(graph, &topo_order)?;
81 compile_modules(graph, &topo_order, &exports, &cached_imports)
82}
83
84fn type_check_modules(
86 graph: &mut ModuleGraph,
87 topo_order: &[ModuleId],
88) -> ModuleResult<TypeCheckResult> {
89 let mut exports: HashMap<ModuleId, ModuleExports> = HashMap::new();
90 let mut cached_imports: HashMap<ModuleId, Vec<ResolvedImport>> = HashMap::new();
91
92 for &module_id in topo_order {
93 let node = graph.node(module_id);
94 let file_path = node.path.clone();
95 let imports = resolve_imports(&node.program, &exports, graph)?;
96 let program = &mut graph.node_mut(module_id).program;
97 let mut checker = TypeChecker::new();
98 for import in &imports {
99 import.inject_into_env(checker.env_mut());
100 }
101 checker.check_program_mut(program);
102
103 let type_errors = checker.errors();
104 if !type_errors.is_empty() {
105 let messages = type_errors.iter().map(|e| e.kind.to_string()).collect();
106 return Err(ModuleErrorKind::TypeErrors {
107 file: file_path.clone(),
108 messages,
109 }
110 .at(Span::ZERO, file_path));
111 }
112 let module_exports = ModuleExports::from_checked(program, checker.env());
113 exports.insert(module_id, module_exports);
114
115 let fold_errors = fold_constants(program);
116 if !fold_errors.is_empty() {
117 let messages = fold_errors.iter().map(|e| e.kind.to_string()).collect();
118 return Err(ModuleErrorKind::TypeErrors {
119 file: file_path.clone(),
120 messages,
121 }
122 .at(Span::ZERO, file_path));
123 }
124 cached_imports.insert(module_id, imports);
125 }
126
127 Ok((exports, cached_imports))
128}
129
130fn compile_modules(
132 graph: &ModuleGraph,
133 topo_order: &[ModuleId],
134 exports: &HashMap<ModuleId, ModuleExports>,
135 cached_imports: &HashMap<ModuleId, Vec<ResolvedImport>>,
136) -> ModuleResult<Bytecode> {
137 let _ = exports; let mut compiler = Compiler::new();
139 for &module_id in topo_order {
140 let file_path = graph.node(module_id).path.clone();
141 if let Some(imports) = cached_imports.get(&module_id) {
142 for import in imports {
143 import.inject_into_compiler(&mut compiler);
144 }
145 }
146 let before = compiler.symbols_table_mut().global_symbol_names();
147 let program = &graph.node(module_id).program;
148 compiler.compile_program(program).map_err(|e| {
149 ModuleErrorKind::CompileErrors {
150 file: file_path.clone(),
151 messages: vec![e.to_string()],
152 }
153 .at(Span::ZERO, file_path.clone())
154 })?;
155
156 apply_module_visibility(&mut compiler, module_id, &before);
157 }
158 let root_path = graph.root().path.clone();
159 compiler.bytecode().map_err(|e| {
160 ModuleErrorKind::CompileErrors {
161 file: root_path.clone(),
162 messages: vec![e.to_string()],
163 }
164 .at(Span::ZERO, root_path)
165 })
166}
167
168fn apply_module_visibility(compiler: &mut Compiler, module_id: ModuleId, before: &[String]) {
176 if module_id != ModuleId::ROOT {
177 let after = compiler.symbols_table_mut().global_symbol_names();
178 for name in after {
179 if !before.contains(&name) {
180 compiler.symbols_table_mut().mask_symbol(&name);
181 }
182 }
183 }
184}
185
186pub fn check_exports(graph: &mut ModuleGraph) -> ModuleResult<HashMap<ModuleId, ModuleExports>> {
188 let mut exports: HashMap<ModuleId, ModuleExports> = HashMap::new();
189 let topo_order = graph.topo_order().to_vec();
190 for &module_id in &topo_order {
191 let node = graph.node(module_id);
192 let file_path = node.path.clone();
193 let imports = resolve_imports(&node.program, &exports, graph)?;
194 let program = &mut graph.node_mut(module_id).program;
195 let mut checker = TypeChecker::new();
196 for import in &imports {
197 import.inject_into_env(checker.env_mut());
198 }
199 checker.check_program_mut(program);
200 let type_errors = checker.errors();
201 if !type_errors.is_empty() {
202 let messages = type_errors.iter().map(|e| e.kind.to_string()).collect();
203 return Err(ModuleErrorKind::TypeErrors {
204 file: file_path.clone(),
205 messages,
206 }
207 .at(Span::ZERO, file_path));
208 }
209 let module_exports = ModuleExports::from_checked(program, checker.env());
210 exports.insert(module_id, module_exports);
211 }
212
213 Ok(exports)
214}
215
216fn resolve_imports(
218 program: &Program,
219 exports: &HashMap<ModuleId, ModuleExports>,
220 graph: &ModuleGraph,
221) -> ModuleResult<Vec<ResolvedImport>> {
222 let mut result = Vec::new();
223 for stmt in &program.statements {
224 let Stmt::Use(use_stmt) = stmt else {
225 continue;
226 };
227 let (module_path, items_to_import) = if let Some(items) = &use_stmt.items {
236 (use_stmt.path.as_slice(), items.clone())
237 } else if use_stmt.path.len() >= 2 {
238 let split = use_stmt.path.len() - 1;
239 (&use_stmt.path[..split], vec![use_stmt.path[split].clone()])
240 } else {
241 continue;
247 };
248 let target_id = find_module_by_path(graph, module_path);
249 let Some(target_id) = target_id else {
250 continue;
253 };
254 let Some(target_exports) = exports.get(&target_id) else {
255 continue;
256 };
257 for item_name in &items_to_import {
258 target_exports.resolve_item(item_name, &mut result);
259 }
260 }
261
262 Ok(result)
263}
264
265fn find_module_by_path(graph: &ModuleGraph, module_path: &[String]) -> Option<ModuleId> {
272 graph
273 .nodes()
274 .find(|n| {
275 if module_path.len() == 1 {
276 n.qualified_path
277 .last()
278 .is_some_and(|last| last == &module_path[0])
279 } else {
280 n.qualified_path == module_path
281 }
282 })
283 .map(|n| n.id)
284}
285
286#[cfg(test)]
287mod tests {
288 use std::fs;
289
290 use super::*;
291
292 fn setup_temp_project(pairs: &[(&str, &str)]) -> tempfile::TempDir {
294 let dir = tempfile::tempdir().expect("failed to create temp dir");
295 for (path, content) in pairs {
296 let full = dir.path().join(path);
297 if let Some(parent) = full.parent() {
298 fs::create_dir_all(parent).expect("failed to create directory");
299 }
300 fs::write(&full, content).expect("failed to write file");
301 }
302 dir
303 }
304
305 fn compile_project(
307 dir: &std::path::Path,
308 entry: &str,
309 ) -> ModuleResult<maat_bytecode::Bytecode> {
310 let mut graph = resolve_module_graph(&dir.join(entry))?;
311 check_and_compile(&mut graph)
312 }
313
314 #[test]
315 fn type_error_in_dependency_surfaces() {
316 let dir = setup_temp_project(&[
317 ("main.maat", "mod math; use math::add; add(1, 2);"),
318 (
319 "math.maat",
320 "pub fn add(a: i64, b: i64) -> i64 { a + b + true }",
321 ),
322 ]);
323 let result = compile_project(dir.path(), "main.maat");
324 assert!(result.is_err(), "type error in dependency should surface");
325 let err_msg = result.unwrap_err().to_string();
326 assert!(
327 err_msg.contains("type") || err_msg.contains("Type"),
328 "error should mention type: {err_msg}"
329 );
330 }
331
332 #[test]
333 fn cross_module_function_type_mismatch() {
334 let dir = setup_temp_project(&[
335 ("main.maat", "mod math; use math::add; add(true, false);"),
336 ("math.maat", "pub fn add(a: i64, b: i64) -> i64 { a + b }"),
337 ]);
338 let result = compile_project(dir.path(), "main.maat");
339 assert!(
340 result.is_err(),
341 "passing bool to i64 params should fail type check"
342 );
343 }
344
345 #[test]
346 fn valid_cross_module_compiles() {
347 let dir = setup_temp_project(&[
348 ("main.maat", "mod math; use math::double; double(21);"),
349 ("math.maat", "pub fn double(x: i64) -> i64 { x * 2 }"),
350 ]);
351 let result = compile_project(dir.path(), "main.maat");
352 assert!(
353 result.is_ok(),
354 "valid cross-module program should compile: {:?}",
355 result.err()
356 );
357 }
358
359 #[test]
360 fn bare_use_is_noop() {
361 let dir = setup_temp_project(&[
362 ("main.maat", "mod helper; use helper; let x: i64 = 42;"),
363 ("helper.maat", "pub fn noop() { }"),
364 ]);
365 let result = compile_project(dir.path(), "main.maat");
366 assert!(
367 result.is_ok(),
368 "bare `use helper;` should be a no-op: {:?}",
369 result.err()
370 );
371 }
372
373 #[test]
374 fn missing_module_import_produces_undefined_error() {
375 let dir = setup_temp_project(&[("main.maat", "use nonexistent::foo; foo();")]);
376 let result = compile_project(dir.path(), "main.maat");
377 assert!(
378 result.is_err(),
379 "importing from non-existent module should fail"
380 );
381 }
382
383 #[test]
384 fn grouped_imports() {
385 let dir = setup_temp_project(&[
386 (
387 "main.maat",
388 "mod math; use math::{add, sub}; add(1, 2); sub(3, 1);",
389 ),
390 (
391 "math.maat",
392 "pub fn add(a: i64, b: i64) -> i64 { a + b }\npub fn sub(a: i64, b: i64) -> i64 { a - b }",
393 ),
394 ]);
395 let result = compile_project(dir.path(), "main.maat");
396 assert!(
397 result.is_ok(),
398 "grouped imports should work: {:?}",
399 result.err()
400 );
401 }
402
403 #[test]
404 fn reexport_pub_use() {
405 let dir = setup_temp_project(&[
406 ("main.maat", "mod proxy; use proxy::double; double(5);"),
407 ("proxy.maat", "mod math; pub use math::double;"),
408 ("proxy/math.maat", "pub fn double(x: i64) -> i64 { x * 2 }"),
409 ]);
410 let result = compile_project(dir.path(), "main.maat");
411 assert!(
412 result.is_ok(),
413 "re-export via `pub use` should work: {:?}",
414 result.err()
415 );
416 }
417
418 #[test]
419 fn topo_order_compiles_dependencies_first() {
420 let dir = setup_temp_project(&[
421 (
422 "main.maat",
423 "mod a; mod b; use a::fa; use b::fb; fa(fb(1));",
424 ),
425 ("a.maat", "pub fn fa(x: i64) -> i64 { x + 10 }"),
426 ("b.maat", "pub fn fb(x: i64) -> i64 { x * 2 }"),
427 ]);
428 let result = compile_project(dir.path(), "main.maat");
429 assert!(
430 result.is_ok(),
431 "multi-dependency compilation should succeed: {:?}",
432 result.err()
433 );
434 }
435
436 #[test]
437 fn diamond_dependency_compiles() {
438 let dir = setup_temp_project(&[
439 (
440 "main.maat",
441 "mod a; mod b; use a::fa; use b::fb; fa(1); fb(2);",
442 ),
443 (
444 "a.maat",
445 "mod shared; use shared::helper; pub fn fa(x: i64) -> i64 { helper(x) }",
446 ),
447 (
448 "b.maat",
449 "mod shared; use shared::helper; pub fn fb(x: i64) -> i64 { helper(x) }",
450 ),
451 ("a/shared.maat", "pub fn helper(x: i64) -> i64 { x + 1 }"),
452 ("b/shared.maat", "pub fn helper(x: i64) -> i64 { x + 2 }"),
453 ]);
454 let result = compile_project(dir.path(), "main.maat");
455 assert!(
456 result.is_ok(),
457 "diamond dependency should compile: {:?}",
458 result.err()
459 );
460 }
461
462 #[test]
463 fn exports_only_pub_items() {
464 let dir = setup_temp_project(&[
465 ("main.maat", "mod lib; use lib::pub_fn; pub_fn();"),
466 ("lib.maat", "pub fn pub_fn() { }\nfn private_fn() { }"),
467 ]);
468 let mut graph = resolve_module_graph(&dir.path().join("main.maat")).unwrap();
469 let exports = check_exports(&mut graph).unwrap();
470 let lib_exports = exports
472 .iter()
473 .find(|&(&id, _)| id != ModuleId::ROOT)
474 .map(|(_, e)| e)
475 .expect("should have lib module exports");
476 let binding_names: Vec<&str> = lib_exports
477 .bindings
478 .iter()
479 .map(|(n, _)| n.as_str())
480 .collect();
481 assert!(binding_names.contains(&"pub_fn"), "should export pub_fn");
482 assert!(
483 !binding_names.contains(&"private_fn"),
484 "should not export private_fn"
485 );
486 }
487
488 #[test]
489 fn exports_pub_struct() {
490 let dir = setup_temp_project(&[
491 ("main.maat", "mod types; use types::Point;"),
492 ("types.maat", "pub struct Point { x: i64, y: i64 }"),
493 ]);
494 let mut graph = resolve_module_graph(&dir.path().join("main.maat")).unwrap();
495 let exports = check_exports(&mut graph).unwrap();
496 let types_exports = exports
497 .iter()
498 .find(|&(&id, _)| id != ModuleId::ROOT)
499 .map(|(_, e)| e)
500 .expect("should have types module exports");
501 assert_eq!(types_exports.structs.len(), 1);
502 assert_eq!(types_exports.structs[0].name, "Point");
503 }
504
505 #[test]
506 fn exports_pub_enum() {
507 let dir = setup_temp_project(&[
508 ("main.maat", "mod types; use types::Color;"),
509 ("types.maat", "pub enum Color { Red, Green, Blue }"),
510 ]);
511 let mut graph = resolve_module_graph(&dir.path().join("main.maat")).unwrap();
512 let exports = check_exports(&mut graph).unwrap();
513 let types_exports = exports
514 .iter()
515 .find(|&(&id, _)| id != ModuleId::ROOT)
516 .map(|(_, e)| e)
517 .expect("should have types module exports");
518 assert_eq!(types_exports.enums.len(), 1);
519 assert_eq!(types_exports.enums[0].name, "Color");
520 }
521
522 #[test]
523 fn private_symbols_do_not_leak_across_modules() {
524 let dir = setup_temp_project(&[
525 ("main.maat", "mod a; mod b; use b::result; result();"),
526 ("a.maat", "fn private_helper() -> i64 { 42 }"),
527 ("b.maat", "pub fn result() -> i64 { 1 }"),
528 ]);
529 let result = compile_project(dir.path(), "main.maat");
531 assert!(
532 result.is_ok(),
533 "private symbols should not leak: {:?}",
534 result.err()
535 );
536 }
537
538 #[test]
539 fn find_module_single_segment() {
540 let dir = setup_temp_project(&[
541 ("main.maat", "mod math;"),
542 ("math.maat", "pub fn add(a: i64, b: i64) -> i64 { a + b }"),
543 ]);
544 let graph = resolve_module_graph(&dir.path().join("main.maat")).unwrap();
545 let found = find_module_by_path(&graph, &["math".to_string()]);
546 assert!(found.is_some(), "should find module by single segment");
547 }
548
549 #[test]
550 fn find_module_returns_none_for_unknown() {
551 let dir = setup_temp_project(&[("main.maat", "let x: i64 = 1;")]);
552 let graph = resolve_module_graph(&dir.path().join("main.maat")).unwrap();
553 let found = find_module_by_path(&graph, &["nonexistent".to_string()]);
554 assert!(found.is_none(), "should not find non-existent module");
555 }
556}