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:
Michael Schimmel
2026-03-06 20:43:45 +01:00
parent d4ca9c4620
commit a59367ba61
7 changed files with 221 additions and 38 deletions
+104 -31
View File
@@ -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);
+1 -1
View File
@@ -136,7 +136,7 @@ impl<'a> Lexer<'a> {
self.col += 1;
}
self.input.next();
} else if c == ';' {
} else if c == ';' || c == '#' {
for c in self.input.by_ref() {
if c == '\n' {
self.line += 1;