From 84ef3f9aedf1310a117390692dd4aafeceb9e8e2 Mon Sep 17 00:00:00 2001 From: Michael Schimmel Date: Fri, 6 Mar 2026 21:07:27 +0100 Subject: [PATCH] feat: Enhance `#use` directive for directories and `cwd` The `#use` directive now supports referencing entire directories, automatically including all `.myc` files within them. Additionally, environments are now initialized with the current working directory as a default search path, simplifying module resolution. --- docs/dependency_management.md | 15 +++-- examples/sma.myc | 5 +- {lib => rtl}/sma.myc | 0 src/ast/environment.rs | 108 +++++++++++++++++++++++----------- src/bin/ast.rs | 4 +- src/main.rs | 8 ++- src/utils/tester.rs | 9 +-- 7 files changed, 94 insertions(+), 55 deletions(-) rename {lib => rtl}/sma.myc (100%) diff --git a/docs/dependency_management.md b/docs/dependency_management.md index 96ed165..d0b7402 100644 --- a/docs/dependency_management.md +++ b/docs/dependency_management.md @@ -20,20 +20,21 @@ We need a way to explicitly define dependencies *within* a Myc script, ensuring ## The Solution: `#use` Preprocessor Directives -We introduce a preprocessor directive `#use` that is evaluated *before* the main parsing phase begins. +We introduce a preprocessor directive `#use` that is evaluated *before* the main parsing phase begins. It can target either single files or entire directories. ### Syntax ```myc #use math->indicators #use utils +#use mylib (do ;; Actual Myc code begins here (def my_var (indicators.calculate ...)) ) ``` -*(Here `math->indicators` resolves to `math/indicators.myc` and `utils` resolves to `utils.myc`)* +*(Here `math->indicators` resolves to the file `math/indicators.myc`. `utils` resolves to `utils.myc`. If `mylib` is a directory instead of a file, it resolves to all `.myc` files within that directory.)* ### Implementation Plan @@ -41,13 +42,15 @@ We introduce a preprocessor directive `#use` that is evaluated *before* the main The `Environment` will be extended with a `HashSet` (e.g., `loaded_modules`) to keep track of canonical paths that have already been loaded, ensuring idempotency. 2. **Pre-Parse Extraction:** - Before the AST `Parser` processes a script, a lightweight scraper will read the top lines of the file. It will extract all `#use` paths and stop as soon as it encounters a line that is not empty, not a comment (`;`), and not a `#use` directive. + Before the AST `Parser` processes a script, a lightweight scraper will read the top lines of the file. It will extract all `#use` paths and stop as soon as it encounters a line that is not empty, not a comment (`;` or `#`), and not a `#use` directive. 3. **Recursive Resolution:** - - The scraper extracts the module path, replaces `->` with the OS-specific directory separator, and appends `.myc`. - - Paths are resolved relative to the file that declares the dependency. + - The scraper extracts the module path and replaces `->` with the OS-specific directory separator. + - It first checks if `.myc` exists as a file. If it does, it resolves to that single file. + - If not, it checks if `` is a directory. If so, it resolves to all `.myc` files inside that directory (sorted alphabetically). + - Paths are resolved relative to the file that declares the dependency, with a fallback to the global `--lib` search paths. - If a path is not in `loaded_modules`, it is added to the set. - - The file is read, and the extraction process runs recursively on its content. + - The files are read, and the extraction process runs recursively on their content. - This creates a flat list of all dependencies in a valid topological order. 4. **Two-Pass Compilation:** diff --git a/examples/sma.myc b/examples/sma.myc index 6f17ac9..617c17d 100644 --- a/examples/sma.myc +++ b/examples/sma.myc @@ -1,10 +1,9 @@ -#use lib->sma -;; Output: void +#use rtl (do (def sma20 (SMA 20)) - (def n 1000) + (def n 100) (while (> n 0) (do (print "val=" n " sma20=" (sma20 n)) diff --git a/lib/sma.myc b/rtl/sma.myc similarity index 100% rename from lib/sma.myc rename to rtl/sma.myc diff --git a/src/ast/environment.rs b/src/ast/environment.rs index 30deac5..90f4b76 100644 --- a/src/ast/environment.rs +++ b/src/ast/environment.rs @@ -173,6 +173,15 @@ impl Environment { env } + /// Adds the current working directory to the global search paths. + /// This is a builder method to easily initialize an environment for file-based usage. + pub fn with_cwd(self) -> Self { + if let Ok(cwd) = std::env::current_dir() { + self.add_search_path(cwd); + } + self + } + pub fn add_search_path(&self, path: impl AsRef) { self.search_paths.borrow_mut().push(path.as_ref().to_path_buf()); } @@ -215,27 +224,54 @@ impl Environment { } /// Resolves a #use module path relative to a base path, then falls back to search paths. - fn resolve_module_path(&self, module_path: &str, base_path: &Path) -> Result { + /// Returns a list of all matching .myc files (single file, or all files in a directory). + fn resolve_module_paths(&self, module_path: &str, base_path: &Path) -> Result, String> { let relative_path = module_path.replace("->", std::path::MAIN_SEPARATOR_STR); let file_name = format!("{}.myc", relative_path); + let check_path = |base: &Path| -> Option> { + // Check for file first + let mut file_p = base.to_path_buf(); + file_p.push(&file_name); + if file_p.is_file() && let Ok(canon) = file_p.canonicalize() { + return Some(vec![canon]); + } + + // Check for directory + let mut dir_p = base.to_path_buf(); + dir_p.push(&relative_path); + if dir_p.is_dir() { + let mut files = Vec::new(); + if let Ok(entries) = std::fs::read_dir(&dir_p) { + let mut valid_entries: Vec<_> = entries.filter_map(|e| e.ok()).collect(); + valid_entries.sort_by_key(|e| e.path()); + for entry in valid_entries { + let p = entry.path(); + if p.is_file() && p.extension().is_some_and(|ext| ext == "myc") && let Ok(canon) = p.canonicalize() { + files.push(canon); + } + } + } + if !files.is_empty() { + return Some(files); + } + } + None + }; + // 1. Try relative to base_path - let mut path = base_path.to_path_buf(); - path.push(&file_name); - if path.is_file() { - return path.canonicalize().map_err(|e| format!("IO Error: {}", e)); + if let Some(paths) = check_path(base_path) { + return Ok(paths); } // 2. Try search paths for sp in self.search_paths.borrow().iter() { - let mut path = sp.clone(); - path.push(&file_name); - if path.is_file() { - return path.canonicalize().map_err(|e| format!("IO Error: {}", e)); + if let Some(paths) = check_path(sp) { + return Ok(paths); } } - Err(format!("Could not find module '{}' (looked for {})", module_path, file_name)) + Err(format!("Could not find module or directory '{}'", module_path)) } /// Recursively collects all dependencies starting from a given source string. @@ -248,33 +284,35 @@ impl Environment { let directives = self.extract_use_directives(source); for module_path in directives { - let abs_path = self.resolve_module_path(&module_path, base_path)?; + let abs_paths = self.resolve_module_paths(&module_path, base_path)?; - if self.loaded_modules.borrow().contains(&abs_path) { - continue; + for abs_path in abs_paths { + if self.loaded_modules.borrow().contains(&abs_path) { + continue; + } + + self.loaded_modules.borrow_mut().insert(abs_path.clone()); + + let lib_source = std::fs::read_to_string(&abs_path) + .map_err(|e| format!("Failed to read module {:?}: {}", abs_path, e))?; + + // Pre-process dependencies of the library BEFORE adding it, ensuring topological order + let lib_base = abs_path.parent().unwrap_or(Path::new(".")); + self.collect_dependencies(&lib_source, lib_base, all_files)?; + + let mut parser = Parser::new(&lib_source); + let untyped_ast = parser.parse_expression(); + + if parser.diagnostics.has_errors() { + return Err(format!( + "Parser error in module {:?}:\n{}", + abs_path, + parser.diagnostics.items[0].message + )); + } + + all_files.push((abs_path, untyped_ast)); } - - self.loaded_modules.borrow_mut().insert(abs_path.clone()); - - let lib_source = std::fs::read_to_string(&abs_path) - .map_err(|e| format!("Failed to read module {:?}: {}", abs_path, e))?; - - // Pre-process dependencies of the library BEFORE adding it, ensuring topological order - let lib_base = abs_path.parent().unwrap_or(Path::new(".")); - self.collect_dependencies(&lib_source, lib_base, all_files)?; - - let mut parser = Parser::new(&lib_source); - let untyped_ast = parser.parse_expression(); - - if parser.diagnostics.has_errors() { - return Err(format!( - "Parser error in module {:?}:\n{}", - abs_path, - parser.diagnostics.items[0].message - )); - } - - all_files.push((abs_path, untyped_ast)); } Ok(()) } diff --git a/src/bin/ast.rs b/src/bin/ast.rs index 6c6d0a5..ead0969 100644 --- a/src/bin/ast.rs +++ b/src/bin/ast.rs @@ -42,9 +42,9 @@ struct Cli { fn main() { let cli = Cli::parse(); - let mut env = Environment::new(); + let mut env = Environment::new().with_cwd(); env.optimization = !cli.no_opt; - + // Load libraries (Now just search paths) for lib_path in &cli.lib { env.add_search_path(lib_path); diff --git a/src/main.rs b/src/main.rs index 5baa1dc..01cad17 100644 --- a/src/main.rs +++ b/src/main.rs @@ -61,6 +61,8 @@ impl Default for CompilerApp { }) .unwrap_or_default(); + let env = Environment::new().with_cwd(); + Self { source_code: String::from( r#" @@ -81,7 +83,7 @@ impl Default for CompilerApp { trace_logs: Vec::new(), active_tab: AppTab::Output, debug_enabled: false, - env: Environment::new(), + env, is_first_frame: true, examples, } @@ -94,7 +96,7 @@ impl CompilerApp { self.trace_logs.clear(); // Reset environment for a fresh run - self.env = Environment::new(); + self.env = Environment::new().with_cwd(); self.env.optimization = true; let res = (|| -> Result<(myc::ast::types::Value, std::time::Duration), String> { @@ -160,7 +162,7 @@ impl CompilerApp { } fn dump_ast(&mut self) { - self.env = Environment::new(); + self.env = Environment::new().with_cwd(); self.env.optimization = true; // Enable optimization for AST dump if let Err(e) = self.env.preload_dependencies(&self.source_code, self.current_example_path.as_deref()) { diff --git a/src/utils/tester.rs b/src/utils/tester.rs index 33c7e31..69fc37c 100644 --- a/src/utils/tester.rs +++ b/src/utils/tester.rs @@ -27,8 +27,7 @@ pub fn run_functional_tests_with_optimization(enabled: bool) -> Vec let output_re = Regex::new(r";; Output: (.*)").unwrap(); for entry in entries.filter_map(|e| e.ok()) { - let mut env = Environment::new(); // Fresh environment per test file - env.add_search_path("."); + let mut env = Environment::new().with_cwd(); // Fresh environment per test file env.optimization = enabled; let path = entry.path(); if path.extension().is_some_and(|ext| ext == "myc") { @@ -121,8 +120,7 @@ pub fn run_benchmarks(update: bool, filter: Option<&str>) -> Vec) -> Vec Result { - let env = Environment::new(); - env.add_search_path("."); + let env = Environment::new().with_cwd(); if let Err(e) = env.preload_dependencies(&content, Some(&path)) { return Err(format!("Dependency Error in measurement: {}", e)); }