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.
This commit is contained in:
Michael Schimmel
2026-03-06 21:07:27 +01:00
parent a59367ba61
commit 84ef3f9aed
7 changed files with 94 additions and 55 deletions
+73 -35
View File
@@ -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<Path>) {
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<PathBuf, String> {
/// 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<Vec<PathBuf>, 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<Vec<PathBuf>> {
// 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(())
}
+2 -2
View File
@@ -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);
+5 -3
View File
@@ -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()) {
+3 -6
View File
@@ -27,8 +27,7 @@ pub fn run_functional_tests_with_optimization(enabled: bool) -> Vec<TestResult>
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<BenchmarkResult
.unwrap_or(1);
// Compile once for this file (symbols/types are compatible with all fresh environments)
let initial_env = Environment::new();
initial_env.add_search_path(".");
let initial_env = Environment::new().with_cwd();
if let Err(e) = initial_env.preload_dependencies(&content, Some(&path)) {
results.push(BenchmarkResult {
@@ -152,8 +150,7 @@ pub fn run_benchmarks(update: bool, filter: Option<&str>) -> Vec<BenchmarkResult
// Helper to measure sum of VM execution times over N executions in one environment
let measure_sum =
|n: u32, node: &crate::ast::compiler::TypedNode| -> Result<Duration, String> {
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));
}