Implement #use directive for dependency management
Introduces the `#use` preprocessor directive, allowing Myc scripts to explicitly declare dependencies on other Myc libraries. This change moves dependency management from a command-line argument to an in-script declaration, ensuring scripts are self-contained and can correctly resolve macros and other symbols. Key features include: - Declarations at the beginning of a file, evaluated before parsing. - Support for relative paths and a new `->` separator. - Automatic resolution of dependencies from specified search paths. - Idempotent loading to prevent duplicate parsing and evaluation. - No AST pollution; dependency management is a compile-time concern. The compiler's lexer has been updated to recognize `#` as a comment character, and the environment now manages search paths and loaded modules. This lays the groundwork for more complex library structures and improved code organization.
This commit is contained in:
+104
-31
@@ -5,7 +5,8 @@ use crate::ast::nodes::{Node, Symbol, UntypedKind};
|
||||
use crate::ast::parser::Parser;
|
||||
use crate::ast::vm::{TracingObserver, VM};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::ast::compiler::bound_nodes::{
|
||||
@@ -79,6 +80,8 @@ pub struct Environment {
|
||||
pub optimization: bool,
|
||||
pub macro_registry: Rc<RefCell<MacroRegistry>>,
|
||||
pub pipeline_generators: Rc<RefCell<Vec<PipelineGenerator>>>,
|
||||
pub search_paths: Rc<RefCell<Vec<PathBuf>>>,
|
||||
pub loaded_modules: Rc<RefCell<HashSet<PathBuf>>>,
|
||||
}
|
||||
|
||||
struct EnvFunctionRegistry {
|
||||
@@ -163,11 +166,17 @@ impl Environment {
|
||||
optimization: true,
|
||||
macro_registry: Rc::new(RefCell::new(MacroRegistry::new())),
|
||||
pipeline_generators: Rc::new(RefCell::new(Vec::new())),
|
||||
search_paths: Rc::new(RefCell::new(Vec::new())),
|
||||
loaded_modules: Rc::new(RefCell::new(HashSet::new())),
|
||||
};
|
||||
env.register_stdlib();
|
||||
env
|
||||
}
|
||||
|
||||
pub fn add_search_path(&self, path: impl AsRef<Path>) {
|
||||
self.search_paths.borrow_mut().push(path.as_ref().to_path_buf());
|
||||
}
|
||||
|
||||
pub fn set_debug_mode(&mut self, enabled: bool) {
|
||||
self.debug_mode = enabled;
|
||||
}
|
||||
@@ -205,41 +214,105 @@ impl Environment {
|
||||
*self.macro_registry.borrow_mut() = expander.into_registry();
|
||||
}
|
||||
|
||||
/// Loads all .myc files from a directory as a library.
|
||||
/// This uses a two-pass approach:
|
||||
/// 1. Discovery: Identify all top-level globals and macros across all files.
|
||||
/// 2. Compilation: Expand, bind, and type-check each file, then run its initialization.
|
||||
pub fn load_library(&self, path: impl AsRef<std::path::Path>) -> Result<(), String> {
|
||||
let dir = path.as_ref();
|
||||
if !dir.is_dir() {
|
||||
return Err(format!("Path {} is not a directory", dir.display()));
|
||||
/// 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> {
|
||||
let relative_path = module_path.replace("->", std::path::MAIN_SEPARATOR_STR);
|
||||
let file_name = format!("{}.myc", relative_path);
|
||||
|
||||
// 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));
|
||||
}
|
||||
|
||||
let mut files = Vec::new();
|
||||
let mut entries: Vec<_> = std::fs::read_dir(dir)
|
||||
.map_err(|e| e.to_string())?
|
||||
.filter_map(|e| e.ok())
|
||||
.collect();
|
||||
// Sort entries for deterministic loading order
|
||||
entries.sort_by_key(|e| e.path());
|
||||
|
||||
for entry in entries {
|
||||
let path = entry.path();
|
||||
if path.is_file() && path.extension().is_some_and(|ext| ext == "myc") {
|
||||
let source = std::fs::read_to_string(&path).map_err(|e| e.to_string())?;
|
||||
let mut parser = Parser::new(&source);
|
||||
let untyped_ast = parser.parse_expression();
|
||||
if parser.diagnostics.has_errors() {
|
||||
return Err(format!(
|
||||
"Parser errors in {}:\n{}",
|
||||
path.display(),
|
||||
parser.diagnostics.items[0].message
|
||||
));
|
||||
}
|
||||
files.push((path, untyped_ast));
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
|
||||
Err(format!("Could not find module '{}' (looked for {})", module_path, file_name))
|
||||
}
|
||||
|
||||
/// Recursively collects all dependencies starting from a given source string.
|
||||
fn collect_dependencies(
|
||||
&self,
|
||||
source: &str,
|
||||
base_path: &Path,
|
||||
all_files: &mut Vec<(PathBuf, Node<UntypedKind>)>,
|
||||
) -> Result<(), String> {
|
||||
let directives = self.extract_use_directives(source);
|
||||
|
||||
for module_path in directives {
|
||||
let abs_path = self.resolve_module_path(&module_path, base_path)?;
|
||||
|
||||
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));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn extract_use_directives(&self, source: &str) -> Vec<String> {
|
||||
let mut paths = Vec::new();
|
||||
for line in source.lines() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.is_empty() || trimmed.starts_with(';') {
|
||||
continue;
|
||||
}
|
||||
if let Some(stripped) = trimmed.strip_prefix("#use ") {
|
||||
let path = stripped.trim();
|
||||
// Strip optional quotes if they somehow got in, though user spec says no spaces/quotes.
|
||||
let clean_path = if (path.starts_with('"') && path.ends_with('"'))
|
||||
|| (path.starts_with('\'') && path.ends_with('\''))
|
||||
{
|
||||
&path[1..path.len() - 1]
|
||||
} else {
|
||||
path
|
||||
};
|
||||
paths.push(clean_path.to_string());
|
||||
} else {
|
||||
// Stop at first non-directive/non-comment line
|
||||
break;
|
||||
}
|
||||
}
|
||||
paths
|
||||
}
|
||||
|
||||
/// Used to pre-load all dependencies of a script before compiling it.
|
||||
/// It returns the base path which can be used to resolve further things if necessary.
|
||||
pub fn preload_dependencies(&self, source: &str, file_path: Option<&Path>) -> Result<(), String> {
|
||||
let base_path = file_path.and_then(|p| p.parent()).unwrap_or_else(|| Path::new("."));
|
||||
let mut files = Vec::new();
|
||||
|
||||
self.collect_dependencies(source, base_path, &mut files)?;
|
||||
|
||||
// Pass 1: Discovery (Globals and Macros)
|
||||
for (_, untyped_ast) in &files {
|
||||
self.discover_globals(untyped_ast);
|
||||
|
||||
Reference in New Issue
Block a user