From 4dfdc75545d2244c0dac7bdba28b9431eac03964 Mon Sep 17 00:00:00 2001 From: Michael Schimmel Date: Sun, 8 Mar 2026 12:09:32 +0100 Subject: [PATCH] Refactor: Introduce system library - Add `rtl/system.myc` as the system library, containing core utilities like `while` and `cache`. - Remove the duplicate `prelude.myc` from `src/ast/rtl/`. - Modify `Environment::collect_dependencies` to implicitly add `#use system` when necessary. - Update `Environment::with_cwd` to also add the `rtl` subdirectory to search paths. - Add `target/` to `.geminiignore` to exclude build artifacts. - Adjust example `sma.myc` to use the new `cache` macro and reduce sample data size. - Update `rtl/sma.myc` to correctly initialize the `history` series with its `length`. - Add `preload_dependencies` calls to `dump_ast` and `run_script` for proper module loading. --- .geminiignore | 1 + examples/sma.myc | 30 ++++++++++++++---------------- rtl/sma.myc | 2 +- rtl/system.myc | 19 +++++++++++++++++++ src/ast/environment.rs | 32 +++++++++++++++++++------------- src/ast/rtl/prelude.myc | 31 ------------------------------- src/integration_test.rs | 4 ++-- 7 files changed, 56 insertions(+), 63 deletions(-) create mode 100644 rtl/system.myc delete mode 100644 src/ast/rtl/prelude.myc diff --git a/.geminiignore b/.geminiignore index d2186ef..15a2026 100644 --- a/.geminiignore +++ b/.geminiignore @@ -1,2 +1,3 @@ *.snap *.snap.new +target/ diff --git a/examples/sma.myc b/examples/sma.myc index c5790a1..578642c 100644 --- a/examples/sma.myc +++ b/examples/sma.myc @@ -1,22 +1,20 @@ #use rtl (do - (def src (create-random-ohlc 42 20000)) + (def src (create-random-ohlc 42 200)) (macro printx [s] `(fn [x] (print ~s x))) -; (pipe src print) -; (pipe [(pipe [(.close src)] (SMA 20))] (printx "sma20=")) -; (pipe [(pipe [(.close src)] (SMA 5))] (printx "sma5=")) -; (pipe [(pipe [(.close src)] (SMA 100))] (printx "sma100=")) - -(macro cache [lookback type src] `(do (def data (series ~lookback ~type)) (pipe [~src] (fn [s] (do (push data s)))) data)) - - - -; (def bars (series 20 :float)) -; (pipe [src] (fn [s] (do (push bars (.close s))))) - -(cache 20 :float src) - -) \ No newline at end of file + (pipe src print) + (pipe [(pipe [(.close src)] (SMA 20))] (printx "sma20=")) + (pipe [(pipe [(.close src)] (SMA 5))] (printx "sma5=")) + (pipe [(pipe [(.close src)] (SMA 100))] (printx "sma100=")) + + (macro cache [lookback type src] `(do (def data (series ~lookback ~type)) (pipe [~src] (fn [s] (do (push data s)))) data)) + +; (def bars (series 20 :float)) +; (pipe [src] (fn [s] (do (push bars (.close s))))) + + (cache 20 :float src) + +) diff --git a/rtl/sma.myc b/rtl/sma.myc index 8ee3190..e17f549 100644 --- a/rtl/sma.myc +++ b/rtl/sma.myc @@ -2,7 +2,7 @@ (def SMA (fn [length] (do - (def history (series :float)) + (def history (series length :float)) (def sum 0.0) (fn [val] diff --git a/rtl/system.myc b/rtl/system.myc new file mode 100644 index 0000000..c7d81a4 --- /dev/null +++ b/rtl/system.myc @@ -0,0 +1,19 @@ +;; Myc System Library +;; Standard macros and core utilities. + +(do + (macro while [cond body] + `((fn [] (if ~cond + (do ~body (again)) + ))) + ) + + ;; Creates a stateful cache (Series) from a stateless stream. + (macro cache [lookback type src] + `(do + (def data (series ~lookback ~type)) + (pipe [~src] (fn [s] (do (push data s)))) + data + ) + ) +) diff --git a/src/ast/environment.rs b/src/ast/environment.rs index 54e4b56..13d3744 100644 --- a/src/ast/environment.rs +++ b/src/ast/environment.rs @@ -177,7 +177,12 @@ impl Environment { /// 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.add_search_path(&cwd); + // Also add the rtl subdirectory if it exists + let rtl_path = cwd.join("rtl"); + if rtl_path.exists() { + self.add_search_path(rtl_path); + } } self } @@ -213,15 +218,6 @@ impl Environment { MacroExpander::new(self.macro_registry.borrow().clone(), evaluator) } - fn eval_prelude(&self, source: &str) { - let mut parser = crate::ast::parser::Parser::new(source); - let untyped_ast = parser.parse_expression(); - let mut expander = self.get_expander(); - let _ = expander - .expand(untyped_ast) - .expect("Failed to expand prelude"); - *self.macro_registry.borrow_mut() = expander.into_registry(); - } /// Resolves a #use module path relative to a base path, then falls back to search paths. /// Returns a list of all matching .myc files (single file, or all files in a directory). @@ -274,14 +270,22 @@ impl Environment { Err(format!("Could not find module or directory '{}'", module_path)) } - /// 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)>, ) -> Result<(), String> { - let directives = self.extract_use_directives(source); + let mut directives = self.extract_use_directives(source); + + // Implicitly add #use system if it's not already there and we can resolve it. + // This is only done for the root script (when loaded_modules is empty) to avoid redundancy. + if !directives.contains(&"system".to_string()) + && self.loaded_modules.borrow().is_empty() + && self.resolve_module_paths("system", base_path).is_ok() + { + directives.insert(0, "system".to_string()); + } for module_path in directives { let abs_paths = self.resolve_module_paths(&module_path, base_path)?; @@ -528,10 +532,10 @@ impl Environment { fn register_stdlib(&self) { rtl::register(self); - self.eval_prelude(include_str!("rtl/prelude.myc")); } pub fn dump_ast(&self, source: &str) -> Result { + self.preload_dependencies(source, None)?; let compiled = self.compile(source).into_result()?; let linked = self.link(compiled); Ok(Dumper::dump(&linked)) @@ -717,6 +721,7 @@ impl Environment { } pub fn run_script(&self, source: &str) -> Result { + self.preload_dependencies(source, None)?; if self.debug_mode { let (res, logs) = self.run_debug(source)?; for line in logs { @@ -739,6 +744,7 @@ impl Environment { } pub fn run_debug(&self, source: &str) -> Result<(Result, Vec), String> { + self.preload_dependencies(source, None)?; let compiled = self.compile(source).into_result()?; let linked = self.link(compiled); let mut vm = VM::new(self.global_values.clone()); diff --git a/src/ast/rtl/prelude.myc b/src/ast/rtl/prelude.myc deleted file mode 100644 index c6402cd..0000000 --- a/src/ast/rtl/prelude.myc +++ /dev/null @@ -1,31 +0,0 @@ -;; Myc Prelude -;; This file is evaluated during Environment bootstrapping. -;; It contains standard macros and functions. - -(macro while [cond body] - `((fn [] (if ~cond - (do ~body (again)) - ))) -) - -;; Creates a stateful cache (Series) from a stateless stream. -;; -;; Pipes are highly optimized, stateless stream-routers that do not allocate memory -;; for historical data. If multiple indicators need to read historical data from -;; a stream, or if you need to access previous items via index (e.g. `(my_data 5)`), -;; you must explicitly cache the stream. -;; -;; Parameters: -;; lookback: The maximum number of items to keep in memory (e.g. 100). -;; type: The data type or schema of the series (e.g. :float or {:price :float}). -;; src: The source stream to observe and cache. -;; -;; Returns: -;; A stateful Series object that automatically updates when the source stream fires. -(macro cache [lookback type src] - `(do - (def data (series ~lookback ~type)) - (pipe [~src] (fn [s] (do (push data s)))) - data - ) -) diff --git a/src/integration_test.rs b/src/integration_test.rs index e11dde9..de857cf 100644 --- a/src/integration_test.rs +++ b/src/integration_test.rs @@ -578,7 +578,7 @@ mod tests { #[test] fn test_record_inlining_in_while_loop() { - let env_ast = Environment::new(); + let env_ast = Environment::new().with_cwd(); // Ensure that constants (like the config record) are inherited into inner lambda scopes (like the body of a while loop). let source = r#" (do @@ -603,7 +603,7 @@ mod tests { ); // The result of running it should obviously still be correct. - let env_run = Environment::new(); + let env_run = Environment::new().with_cwd(); let res = env_run.run_script(source).unwrap(); assert_eq!(format!("{}", res), "10"); }