Add module loader and related tests
Introduce `ModuleLoader` to handle `#use` directives and dependency resolution. This involves adding `tempfile` dependency, modifying `Cargo.toml` and `Cargo.lock`, creating a new `module_loader.rs` file, and updating `environment.rs` to use the new module. Also, add comprehensive tests for `Diagnostics::format_errors`, `CompilationResult`, and the new `ModuleLoader` functionality. These tests cover various scenarios including empty diagnostics, multiple errors, error formatting, compilation success/failure, and complex dependency loading with topological ordering and search path resolution.
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashSet;
|
||||
use std::fs;
|
||||
use std::rc::Rc;
|
||||
|
||||
use myc::ast::compiler::module_loader::ModuleLoader;
|
||||
|
||||
fn make_loader() -> ModuleLoader {
|
||||
ModuleLoader::new(
|
||||
Rc::new(RefCell::new(vec![])),
|
||||
Rc::new(RefCell::new(HashSet::new())),
|
||||
)
|
||||
}
|
||||
|
||||
fn make_loader_with_search_path(path: std::path::PathBuf) -> ModuleLoader {
|
||||
ModuleLoader::new(
|
||||
Rc::new(RefCell::new(vec![path])),
|
||||
Rc::new(RefCell::new(HashSet::new())),
|
||||
)
|
||||
}
|
||||
|
||||
// --- Basic resolution ---
|
||||
|
||||
#[test]
|
||||
fn simple_use_directive_resolves_file() {
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
fs::write(dir.path().join("utils.myc"), "(def x 42)").unwrap();
|
||||
|
||||
let loader = make_loader();
|
||||
let files = loader
|
||||
.collect_dependency_files("#use utils\n(+ x 1)", dir.path())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(files.len(), 1);
|
||||
assert!(files[0].0.ends_with("utils.myc"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_module_returns_descriptive_error() {
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
|
||||
let loader = make_loader();
|
||||
let result = loader.collect_dependency_files("#use nonexistent\n42", dir.path());
|
||||
|
||||
assert!(result.is_err());
|
||||
let msg = result.unwrap_err();
|
||||
assert!(
|
||||
msg.contains("nonexistent"),
|
||||
"error message should name the missing module, got: {msg}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_use_directives_returns_empty() {
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
|
||||
let loader = make_loader();
|
||||
let files = loader
|
||||
.collect_dependency_files("(+ 1 2)", dir.path())
|
||||
.unwrap();
|
||||
|
||||
assert!(files.is_empty());
|
||||
}
|
||||
|
||||
// --- Topological ordering ---
|
||||
|
||||
#[test]
|
||||
fn transitive_deps_returned_in_topological_order() {
|
||||
// Dependency chain: source → b → c
|
||||
// Expected order in result: [c, b] (dependencies before dependents)
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
fs::write(dir.path().join("c.myc"), "(def c_val 1)").unwrap();
|
||||
fs::write(dir.path().join("b.myc"), "#use c\n(def b_val 2)").unwrap();
|
||||
|
||||
let loader = make_loader();
|
||||
let files = loader
|
||||
.collect_dependency_files("#use b\n(+ b_val c_val)", dir.path())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(files.len(), 2);
|
||||
assert!(
|
||||
files[0].0.ends_with("c.myc"),
|
||||
"c must come first (topological order)"
|
||||
);
|
||||
assert!(
|
||||
files[1].0.ends_with("b.myc"),
|
||||
"b must come second (depends on c)"
|
||||
);
|
||||
}
|
||||
|
||||
// --- Deduplication ---
|
||||
|
||||
#[test]
|
||||
fn shared_dependency_loaded_only_once() {
|
||||
// Both b and the source use c — c must appear exactly once.
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
fs::write(dir.path().join("c.myc"), "(def c_val 1)").unwrap();
|
||||
fs::write(dir.path().join("b.myc"), "#use c\n(def b_val 2)").unwrap();
|
||||
|
||||
let loader = make_loader();
|
||||
let source = "#use b\n#use c\n(+ b_val c_val)";
|
||||
let files = loader
|
||||
.collect_dependency_files(source, dir.path())
|
||||
.unwrap();
|
||||
|
||||
let c_count = files.iter().filter(|(p, _)| p.ends_with("c.myc")).count();
|
||||
assert_eq!(c_count, 1, "c.myc must appear exactly once");
|
||||
assert_eq!(files.len(), 2); // c, b
|
||||
}
|
||||
|
||||
// --- Search path resolution ---
|
||||
|
||||
#[test]
|
||||
fn module_found_via_search_path() {
|
||||
// base_path has no .myc files; module lives in lib_dir (a search path)
|
||||
let base_dir = tempfile::TempDir::new().unwrap();
|
||||
let lib_dir = tempfile::TempDir::new().unwrap();
|
||||
fs::write(lib_dir.path().join("utils.myc"), "(def util_val 99)").unwrap();
|
||||
|
||||
let loader = make_loader_with_search_path(lib_dir.path().to_path_buf());
|
||||
let files = loader
|
||||
.collect_dependency_files("#use utils\n42", base_dir.path())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(files.len(), 1);
|
||||
assert!(files[0].0.ends_with("utils.myc"));
|
||||
}
|
||||
|
||||
// --- Directory modules ---
|
||||
|
||||
#[test]
|
||||
fn directory_module_loads_all_myc_files_alphabetically() {
|
||||
// #use lib should load lib/a.myc, lib/b.myc, lib/c.myc in that order
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
let lib_dir = dir.path().join("lib");
|
||||
fs::create_dir(&lib_dir).unwrap();
|
||||
fs::write(lib_dir.join("c.myc"), "(def c 3)").unwrap();
|
||||
fs::write(lib_dir.join("a.myc"), "(def a 1)").unwrap();
|
||||
fs::write(lib_dir.join("b.myc"), "(def b 2)").unwrap();
|
||||
|
||||
let loader = make_loader();
|
||||
let files = loader
|
||||
.collect_dependency_files("#use lib\n42", dir.path())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(files.len(), 3);
|
||||
assert!(files[0].0.ends_with("a.myc"), "first: a.myc");
|
||||
assert!(files[1].0.ends_with("b.myc"), "second: b.myc");
|
||||
assert!(files[2].0.ends_with("c.myc"), "third: c.myc");
|
||||
}
|
||||
Reference in New Issue
Block a user