From e8ea4ecb75677237ebfb0bf72ca0a96de9da7c4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Christiansen=20S=C3=B8rby?= Date: Sun, 24 May 2026 15:20:42 +0200 Subject: [PATCH] =?UTF-8?q?packaging:=20Phase=201=20=E2=80=94=20SearchPath?= =?UTF-8?q?s=20+=20resolve=5Ffile=5Fpath=20for=20mote=20discovery?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add SearchPaths struct and resolve_file_path on ModulePath in term.rs. Add search_paths field to LoadedModules with accessor/setter. Modify load_decls to search across paths with to_file_path fallback. Add build_default_search_paths() with priority: source dir → CWD → motes/ → MONAD_PATH → CLI extra paths. Wire through run, run_tests, repl. Add --package-path/-p CLI flag to run, test, compile commands. test: add SearchPaths unit tests + example mote with e2e test Add Rust unit tests for SearchPaths (empty, new/push, default) and resolve_file_path (found in first dir, found in second dir when first missing, not found, empty search paths). Add example mote at motes/example/ with monad.toml manifest and greet.mo module. Add self-contained e2e Monad test (examples/test_mote.mo) verifying mote resolution concept. All 464 Rust + 175 Monad tests pass. motes: auto-discover mote src/ dirs in build_default_search_paths Scan motes/*/src/ for each mote subdirectory as additional search paths. The example mote at motes/example/src/ is now found by the test runner without --mote-path. Updates test_mote.mo to actually load from the mote via 'use greet'. All 517 tests pass (464 Rust + 53 Monad files). --- cli/src/main.rs | 11 ++++++ core/src/lib.rs | 57 ++++++++++++++++++++++++++- core/src/term.rs | 37 ++++++++++++++++++ core/src/term/module.rs | 25 ++++++++++-- core/src/term/test.rs | 80 ++++++++++++++++++++++++++++++++++++++ examples/test_mote.mo | 9 +++++ motes/example/mote.toml | 6 +++ motes/example/src/greet.mo | 2 + 8 files changed, 222 insertions(+), 5 deletions(-) create mode 100644 examples/test_mote.mo create mode 100644 motes/example/mote.toml create mode 100644 motes/example/src/greet.mo diff --git a/cli/src/main.rs b/cli/src/main.rs index 197cdaf..94a50f2 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -39,6 +39,8 @@ enum Commands { no_color: bool, #[arg(long)] max_depth: Option, + #[arg(short = 'p', long = "mote-path", value_name = "DIR")] + mote_path: Vec, }, Test { @@ -60,6 +62,8 @@ enum Commands { jobs: Option, #[arg(long, default_value_t = false)] sequential: bool, + #[arg(short = 'p', long = "mote-path", value_name = "DIR")] + mote_path: Vec, }, #[cfg(feature = "llvm")] @@ -76,6 +80,8 @@ enum Commands { keep_intermediates: bool, #[arg(short, long, default_value_t = false)] debug: bool, + #[arg(short = 'p', long = "mote-path", value_name = "DIR")] + mote_path: Vec, }, } @@ -123,6 +129,7 @@ fn main() -> Result<(), String> { color, no_color, max_depth, + mote_path, } => { let use_colors = color && !no_color; let result = run( @@ -134,6 +141,7 @@ fn main() -> Result<(), String> { use_colors, max_recursion_depth: max_depth, }, + mote_path, ); match result { Ok(_) => (), @@ -153,6 +161,7 @@ fn main() -> Result<(), String> { timeout, jobs, sequential, + mote_path, } => { let use_colors = color && !no_color; let num_threads = if sequential { @@ -174,6 +183,7 @@ fn main() -> Result<(), String> { }, num_threads, timeout.map(|s| std::time::Duration::from_secs_f64(s)), + mote_path, ); match result { Ok(_) => (), @@ -191,6 +201,7 @@ fn main() -> Result<(), String> { output_kind, keep_intermediates, debug, + mote_path: _mote_path, } => { let output_kind = match output_kind.as_str() { "exe" => OutputKind::Executable, diff --git a/core/src/lib.rs b/core/src/lib.rs index 59b3eb1..7fb85ed 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -26,7 +26,7 @@ use crate::term::module::module; use crate::term::module::{ LoadedModules, default_modules, load_module_files, load_module_from_text, }; -use crate::term::{Constructor, ModulePath, mpt, strings_to_list_term}; +use crate::term::{Constructor, ModulePath, SearchPaths, mpt, strings_to_list_term}; use crate::term::{app, id}; pub mod diag; @@ -64,6 +64,7 @@ pub fn repl(options: EvalOptions) -> Result<(), String> { } let mut loaded_modules = default_modules().map_err(|e| format!("{e}"))?; loaded_modules.config.benchmark = options.benchmark; + loaded_modules.set_search_paths(build_default_search_paths(&PathBuf::from("."), &[])); let module_path = ModulePath::top("'repl"); let module = module( module_path.clone(), @@ -180,11 +181,18 @@ pub fn eval_kernel(term: Term, scope: &crate::term::module::Scope) -> Result, options: EvalOptions) -> Result<(), String> { +pub fn run( + input: PathBuf, + args: Vec, + options: EvalOptions, + extra_mote_paths: Vec, +) -> Result<(), String> { let path: ModulePath = input.clone().into(); let source = fs::read_to_string(&input).map_err(|e| format!("{e}"))?; let mut loaded = default_modules().map_err(|e| format!("{e}"))?; loaded.config.benchmark = options.benchmark; + let search_paths = build_default_search_paths(&input, &extra_mote_paths); + loaded.set_search_paths(search_paths); load_module_from_text(&source, path.clone(), &mut loaded).map_err(|e| format!("{e}"))?; let module = loaded .get_module(&path) @@ -240,6 +248,43 @@ pub fn vec_fmt(v: &[T]) -> String { .join(", ") } +fn build_default_search_paths(input: &PathBuf, extra_paths: &[PathBuf]) -> SearchPaths { + let mut paths = SearchPaths::empty(); + + if let Some(parent) = input.parent() { + paths.push(parent.to_path_buf()); + } + + paths.push(PathBuf::from(".")); + + if let Ok(cwd) = std::env::current_dir() { + let motes_dir = cwd.join("motes"); + if motes_dir.is_dir() { + paths.push(motes_dir.clone()); + if let Ok(entries) = std::fs::read_dir(&motes_dir) { + for entry in entries.flatten() { + let src_dir = entry.path().join("src"); + if src_dir.is_dir() { + paths.push(src_dir); + } + } + } + } + } + + if let Ok(monad_path) = std::env::var("MONAD_PATH") { + for dir in std::env::split_paths(&monad_path) { + paths.push(dir); + } + } + + for p in extra_paths { + paths.push(p.clone()); + } + + paths +} + const GREEN: &str = "\x1b[32m"; const RED: &str = "\x1b[31m"; const YELLOW: &str = "\x1b[33m"; @@ -651,6 +696,7 @@ pub fn run_tests( options: EvalOptions, num_threads: usize, test_timeout: Option, + extra_mote_paths: Vec, ) -> Result<(), String> { let mut files: Vec = Vec::new(); for input in &inputs { @@ -667,6 +713,13 @@ pub fn run_tests( master_loaded.set_test_mode(true); master_loaded.config.benchmark = options.benchmark; + let search_paths = if let Some(first_input) = inputs.first() { + build_default_search_paths(first_input, &extra_mote_paths) + } else { + build_default_search_paths(&PathBuf::from("."), &extra_mote_paths) + }; + master_loaded.set_search_paths(search_paths); + // Ensure std/test is loaded (for Test.assert) let test_path: ModulePath = ModulePath::new(vec![id("std"), id("test")]); if master_loaded.get_module(&test_path).is_none() { diff --git a/core/src/term.rs b/core/src/term.rs index 31f97d7..b391a53 100644 --- a/core/src/term.rs +++ b/core/src/term.rs @@ -2051,6 +2051,16 @@ impl ModulePath { p } + pub fn resolve_file_path(&self, search_paths: &SearchPaths) -> Option { + for dir in &search_paths.0 { + let candidate = dir.join(self.to_file_path()); + if candidate.exists() { + return Some(candidate); + } + } + None + } + pub fn open(&self, opens: &Vec<&Open>) -> Vec { opens .iter() @@ -2106,6 +2116,33 @@ impl ModulePath { } } +#[derive(Debug, Clone, PartialEq)] +pub struct SearchPaths(pub(crate) Vec); + +impl SearchPaths { + pub fn new(paths: Vec) -> Self { + SearchPaths(paths) + } + + pub fn empty() -> Self { + SearchPaths(Vec::new()) + } + + pub fn push(&mut self, path: PathBuf) { + self.0.push(path); + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +impl Default for SearchPaths { + fn default() -> Self { + SearchPaths::empty() + } +} + pub fn mp(v: Vec<&str>) -> ModulePath { ModulePath::new(v.iter().map(|i| id(i)).collect()) } diff --git a/core/src/term/module.rs b/core/src/term/module.rs index 3d12251..f1fb22d 100644 --- a/core/src/term/module.rs +++ b/core/src/term/module.rs @@ -173,6 +173,7 @@ pub struct LoadedModules { builtins: Builtins, native: Map, pub config: LoadedModulesConfig, + search_paths: SearchPaths, } impl Display for LoadedModules { @@ -198,6 +199,7 @@ impl LoadedModules { builtins, native, config: Default::default(), + search_paths: SearchPaths::empty(), } } pub fn test_mode(&self) -> bool { @@ -206,6 +208,12 @@ impl LoadedModules { pub fn set_test_mode(&mut self, test_mode: bool) { self.config.test_mode = test_mode; } + pub fn search_paths(&self) -> &SearchPaths { + &self.search_paths + } + pub fn set_search_paths(&mut self, paths: SearchPaths) { + self.search_paths = paths; + } pub fn get_module_mut(&mut self, path: &ModulePath) -> Option<&mut Module> { self.modules.get_mut(path) } @@ -231,6 +239,7 @@ impl LoadedModules { modules: Map::new(), builtins: Builtins::new(), config: Default::default(), + search_paths: SearchPaths::empty(), } } @@ -1738,7 +1747,8 @@ fn load_module_files_impl( return Ok(loaded); } let parse_start = Instant::now(); - let decls = load_decls(path)?; + let search_paths = loaded.search_paths().clone(); + let decls = load_decls(path, &search_paths)?; let parse_dur = parse_start.elapsed(); let mut loaded = load_decl_uses_modules(&decls, loaded, in_progress)?; let decls = filter_cfg_test_decls(decls, loaded.config.test_mode); @@ -1764,8 +1774,17 @@ fn load_module_files_impl( Ok(loaded) } -pub fn load_decls(path: &ModulePath) -> Result>, String> { - let file_path = path.to_file_path(); +pub fn load_decls( + path: &ModulePath, + search_paths: &SearchPaths, +) -> Result>, String> { + let file_path = path + .resolve_file_path(search_paths) + .or_else(|| { + let p = path.to_file_path(); + if p.exists() { Some(p) } else { None } + }) + .ok_or_else(|| format!("module not found: {path}"))?; let text = read_to_string(&file_path).map_err(|e| e.to_string())?; load_decls_from_text_with_path(&text, Some(&file_path)) } diff --git a/core/src/term/test.rs b/core/src/term/test.rs index 84e5ba1..2cf1544 100644 --- a/core/src/term/test.rs +++ b/core/src/term/test.rs @@ -568,3 +568,83 @@ fn test_merge_detect_rejects_duplicate() { assert!(result.is_err()); assert!(result.unwrap_err().contains("duplicate")); } + +#[test] +fn test_search_paths_empty() { + let sp = SearchPaths::empty(); + assert!(sp.is_empty()); +} + +#[test] +fn test_search_paths_new_and_push() { + let mut sp = SearchPaths::new(vec![PathBuf::from("/a")]); + assert!(!sp.is_empty()); + sp.push(PathBuf::from("/b")); + assert_eq!(sp.0.len(), 2); +} + +#[test] +fn test_search_paths_default_is_empty() { + let sp = SearchPaths::default(); + assert!(sp.is_empty()); +} + +fn random_tmp_dir(label: &str) -> PathBuf { + let prefix = format!("monad-test-{}-{:x}", label, std::process::id()); + PathBuf::from("/tmp").join(prefix) +} + +#[test] +fn test_resolve_file_path_finds_in_first_dir() { + let dir1 = random_tmp_dir("s1a"); + let dir2 = random_tmp_dir("s1b"); + + let sp = SearchPaths::new(vec![dir1.clone(), dir2]); + + std::fs::create_dir_all(&dir1).unwrap(); + std::fs::write(dir1.join("test_mod.mo"), "").unwrap(); + + let mp = mpt("test_mod"); + let found = mp.resolve_file_path(&sp); + assert!(found.is_some()); + + std::fs::remove_dir_all(&dir1).unwrap(); +} + +#[test] +fn test_resolve_file_path_finds_in_second_dir_when_first_missing() { + let dir1 = random_tmp_dir("s2a"); + let dir2 = random_tmp_dir("s2b"); + + let sp = SearchPaths::new(vec![dir1, dir2.clone()]); + + std::fs::create_dir_all(&dir2).unwrap(); + std::fs::write(dir2.join("test_mod.mo"), "").unwrap(); + + let mp = mpt("test_mod"); + let found = mp.resolve_file_path(&sp); + assert!(found.is_some()); + assert!(found.unwrap().starts_with(dir2.to_str().unwrap())); + + std::fs::remove_dir_all(&dir2).unwrap(); +} + +#[test] +fn test_resolve_file_path_none_when_not_in_any_dir() { + let dir1 = random_tmp_dir("nonexistent-1"); + let dir2 = random_tmp_dir("nonexistent-2"); + + let sp = SearchPaths::new(vec![dir1, dir2]); + + let mp = mpt("no_such_module"); + let found = mp.resolve_file_path(&sp); + assert!(found.is_none()); +} + +#[test] +fn test_resolve_file_path_empty_search_paths_returns_none() { + let sp = SearchPaths::empty(); + let mp = mpt("anything"); + let found = mp.resolve_file_path(&sp); + assert!(found.is_none()); +} diff --git a/examples/test_mote.mo b/examples/test_mote.mo new file mode 100644 index 0000000..b3ce69d --- /dev/null +++ b/examples/test_mote.mo @@ -0,0 +1,9 @@ +use greet + +@[test] +def test_mote_greet : Bool := + greet.greet "World" == "Hello, World!" + +@[test] +def test_mote_greet_empty : Bool := + greet.greet "" == "Hello, !" diff --git a/motes/example/mote.toml b/motes/example/mote.toml new file mode 100644 index 0000000..56a65e5 --- /dev/null +++ b/motes/example/mote.toml @@ -0,0 +1,6 @@ +[mote] +name = "example" +version = "0.1.0" +edition = "2026" + +[dependencies] diff --git a/motes/example/src/greet.mo b/motes/example/src/greet.mo new file mode 100644 index 0000000..df259c4 --- /dev/null +++ b/motes/example/src/greet.mo @@ -0,0 +1,2 @@ +def greet (name : String) : String := + String.concat "Hello, " (String.concat name "!") -- 2.51.2