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.
4.6 KiB
Myc Dependency Management: The #use Directive
Motivation
As the Myc language and its ecosystem grow, scripts become larger and need to be split into multiple files (libraries). Until now, the AST compiler tool (ast.exe) only supported loading libraries via the --lib command-line argument.
We need a way to explicitly define dependencies within a Myc script, ensuring that a script can autonomously declare what it needs to run.
Constraints & Design Decisions
- No AST Pollution: The dependency declaration must not become part of the compiled AST. The AST is designed to be a pure representation of the runtime logic, which makes it suitable for visual programming and graphical representation. Dependency management is a compile-time/environment concern, not a runtime AST concern.
- Macro Availability: Libraries often define macros. If script
Adepends on libraryB, andBdefines a macrofoo, scriptAmust have access tofooduring its own macro-expansion phase. - Idempotency: If
Adepends onBandC, andBalso depends onC, libraryCmust only be parsed and evaluated once. - Location: Dependencies must be declared at the very beginning of the file, before any actual Myc code.
- Path Format Restrictions:
- Whitespace is not allowed in library paths and file names.
- The file extension is always omitted (the system implies
.myc). - The folder separator is replaced by
->.
- Search Paths: The
--libcommand-line argument will no longer proactively load all.mycfiles. Instead, it will append directories to a list of global search paths. When#useis resolved, the compiler first checks relative to the current file, and then falls back to searching within the provided--libsearch paths.
The Solution: #use Preprocessor Directives
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
#use math->indicators
#use utils
#use mylib
(do
;; Actual Myc code begins here
(def my_var (indicators.calculate ...))
)
(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
-
Environment State: The
Environmentwill be extended with aHashSet<PathBuf>(e.g.,loaded_modules) to keep track of canonical paths that have already been loaded, ensuring idempotency. -
Pre-Parse Extraction: Before the AST
Parserprocesses a script, a lightweight scraper will read the top lines of the file. It will extract all#usepaths and stop as soon as it encounters a line that is not empty, not a comment (;or#), and not a#usedirective. -
Recursive Resolution:
- The scraper extracts the module path and replaces
->with the OS-specific directory separator. - It first checks if
<path>.mycexists as a file. If it does, it resolves to that single file. - If not, it checks if
<path>is a directory. If so, it resolves to all.mycfiles inside that directory (sorted alphabetically). - Paths are resolved relative to the file that declares the dependency, with a fallback to the global
--libsearch paths. - If a path is not in
loaded_modules, it is added to the set. - 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.
- The scraper extracts the module path and replaces
-
Two-Pass Compilation: We reuse the existing robust two-pass mechanism used by the
--libflag:- Pass 1 (Discovery): Parse all discovered dependency files and traverse their untyped ASTs to register
Def(Globals) andMacroDecl(Macros) in the Environment. This makes all exported symbols and macros available. - Pass 2 (Compilation): Type-check and compile all dependency files, then execute them to initialize their global state.
- Finally, compile and execute the original main script.
- Pass 1 (Discovery): Parse all discovered dependency files and traverse their untyped ASTs to register
-
Lexer Adaptation: Since
#useis handled before the main parser, we will modify theLexerto treat#as the start of a single-line comment (similar to;). This prevents the parser from crashing on the directives while keeping the line numbers accurate.
Summary
By using a preprocessor directive, we keep the core Lisp-like syntax and AST completely clean of module-loading mechanics, while providing a powerful, recursive, and idempotent dependency resolution system that plays perfectly with our macro expansion rules.