From d30420324a151a5c2aad4d7476880f4d160f7b42 Mon Sep 17 00:00:00 2001 From: jess Date: Sat, 30 May 2026 18:52:43 -0700 Subject: [PATCH] - prelude_overrides: plugins may swap the compiler's built-in runtime helpers for their own. - decompose_module + read_module_source: compiles a module straight from a disk path. --- compile/src/lib.rs | 147 ++++++++++++++++++++++++++++++++++--- core/src/interp/mod.rs | 2 +- core/src/interp/modules.rs | 4 +- 3 files changed, 141 insertions(+), 12 deletions(-) diff --git a/compile/src/lib.rs b/compile/src/lib.rs index a9f882e..73d2e43 100644 --- a/compile/src/lib.rs +++ b/compile/src/lib.rs @@ -1,6 +1,9 @@ //! cordial source decomposer -- produces self-contained Rust from Cordial. -use acord_core::interp::{parse_program, Expr, Op, Stmt}; +use std::collections::HashMap; +use std::path::Path; + +use acord_core::interp::{parse_program, read_module_source, Expr, Op, Stmt}; /// extension point for external projects adding custom decomposition rules. pub trait DecomposeHook { @@ -15,6 +18,9 @@ pub trait DecomposeHook { /// intercepts a statement before the default decomposition. fn stmt(&self, _stmt: &Stmt) -> Option { None } + + /// replaces named prelude runtime functions with full custom definitions. + fn prelude_overrides(&self) -> Vec<(&'static str, String)> { Vec::new() } } struct NoHook; @@ -52,19 +58,54 @@ pub fn decompose_with(source: &str, hook: &dyn DecomposeHook) -> Result Result { + let source = read_module_source(path) + .map_err(|e| format!("reading module '{}': {}", path.display(), e))?; + let stmts = parse_program(&source)?; + decompose_stmts(&stmts, hook) +} + /// decomposes a pre-parsed statement list. pub fn decompose_stmts(stmts: &[Stmt], hook: &dyn DecomposeHook) -> Result { let mut deps = Vec::new(); let mut out = String::new(); - out.push_str(GENERATED_PRELUDE); + out.push_str(PRELUDE_HEADER); + + let overrides: HashMap<&str, String> = hook.prelude_overrides().into_iter().collect(); + for (name, body) in split_runtime_fns(PRELUDE_FNS) { + match overrides.get(name.as_str()) { + Some(custom) => { out.push_str(custom); out.push('\n'); } + None => out.push_str(&body), + } + } + out.push('\n'); + if let Some(extra) = hook.preamble() { out.push_str(&extra); out.push('\n'); } + out.push_str("\n// --- generated code below ---\n\n"); emit_program(&mut out, stmts, &mut deps, hook)?; Ok(Decomposed { code: out, deps }) } +/// splits a runtime source block into (fn name, full definition) pairs. +fn split_runtime_fns(src: &str) -> Vec<(String, String)> { + let mut out: Vec<(String, String)> = Vec::new(); + for line in src.lines() { + if let Some(rest) = line.strip_prefix("fn ") { + let name: String = rest.chars().take_while(|c| c.is_alphanumeric() || *c == '_').collect(); + out.push((name, String::new())); + } + if let Some(last) = out.last_mut() { + last.1.push_str(line); + last.1.push('\n'); + } + } + out +} + /// dispatch-table entry: type, method, fn name, arity. struct MethodReg { ty: String, @@ -449,7 +490,7 @@ const RESERVED: &[&str] = &[ "async", "await", "dyn", ]; -const GENERATED_PRELUDE: &str = r#"#![allow(unused_variables, unused_mut, dead_code, unused_imports, non_snake_case)] +const PRELUDE_HEADER: &str = r#"#![allow(unused_variables, unused_mut, dead_code, unused_imports, non_snake_case)] use std::collections::BTreeMap; #[derive(Clone, Debug, PartialEq)] @@ -489,7 +530,10 @@ impl std::fmt::Display for V { } } -fn v_num(v: &V) -> f64 { match v { V::Num(n) => *n, V::Bool(b) => if *b { 1.0 } else { 0.0 }, _ => 0.0 } } +enum Step { Field(String), Index(i64) } +"#; + +const PRELUDE_FNS: &str = r#"fn v_num(v: &V) -> f64 { match v { V::Num(n) => *n, V::Bool(b) => if *b { 1.0 } else { 0.0 }, _ => 0.0 } } fn v_truthy(v: &V) -> bool { match v { V::Bool(b) => *b, V::Num(n) => *n != 0.0, V::Str(s) => !s.is_empty(), V::Array(a) => !a.is_empty(), V::Struct(m) => !m.is_empty(), _ => false } } fn v_struct_type(v: &V) -> Option { @@ -566,8 +610,6 @@ fn v_field(base: &V, name: &str) -> V { } } -enum Step { Field(String), Index(i64) } - fn v_assign_path(root: &mut V, steps: &[Step], val: V) { let mut cur = root; for (i, step) in steps.iter().enumerate() { @@ -615,9 +657,6 @@ fn v_method_call(recv: &V, method: &str, args: &[V]) -> V { } V::Void } - -// --- generated code below --- - "#; #[cfg(test)] @@ -747,6 +786,96 @@ mod tests { assert_eq!(r.deps[0].segments, vec!["sitter", "scene", "primitives"]); } + struct FieldOverride; + impl DecomposeHook for FieldOverride { + fn preamble(&self) -> Option { + Some("fn ext_read(id: i64, f: &str) -> V { V::Num(id as f64 + f.len() as f64) }".into()) + } + fn prelude_overrides(&self) -> Vec<(&'static str, String)> { + vec![("v_field", "fn v_field(base: &V, name: &str) -> V { if let V::Struct(m) = base { if let Some(V::Num(id)) = m.get(\"__id\") { return ext_read(*id as i64, name); } return m.get(name).cloned().unwrap_or(V::Void); } V::Void }".into())] + } + } + + #[test] + fn prelude_override_replaces_named_fn() { + let out = decompose_with("let x = 1", &FieldOverride).unwrap().code; + assert_eq!(out.matches("fn v_field(").count(), 1, "override must not duplicate the fn"); + assert!(out.contains("ext_read(*id as i64, name)"), "custom body must be emitted"); + assert!(!out.contains("\"len\" => V::Num(a.len()"), "default v_field body must be gone"); + assert!(out.contains("fn v_assign_path(")); + assert!(out.contains("fn v_method_call(")); + } + + #[test] + fn overridden_prelude_compiles_and_runs() { + let mut code = decompose_with("let x = 1", &FieldOverride).unwrap().code; + code.push_str(r#" +fn main() { + let s = V::Struct([("__id".to_string(), V::Num(7.0))].into_iter().collect()); + assert_eq!(v_field(&s, "ab"), V::Num(9.0)); + let plain = V::Struct([("x".to_string(), V::Num(3.0))].into_iter().collect()); + assert_eq!(v_field(&plain, "x"), V::Num(3.0)); + let _ = run(); + println!("OK"); +} +"#); + let dir = std::env::temp_dir().join(format!("acord-dec-ovr-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let src_path = dir.join("prog.rs"); + let bin_path = dir.join("prog"); + std::fs::write(&src_path, &code).unwrap(); + + let compile = std::process::Command::new("rustc") + .arg("--edition").arg("2021").arg("-O") + .arg(&src_path).arg("-o").arg(&bin_path) + .output(); + let compile = match compile { + Ok(c) => c, + Err(_) => { std::fs::remove_dir_all(&dir).ok(); return; } + }; + assert!(compile.status.success(), "rustc failed:\n{}", String::from_utf8_lossy(&compile.stderr)); + let run = std::process::Command::new(&bin_path).output().unwrap(); + assert_eq!(String::from_utf8_lossy(&run.stdout).trim(), "OK"); + std::fs::remove_dir_all(&dir).ok(); + } + + #[test] + fn decompose_module_reads_single_file() { + let dir = std::env::temp_dir().join(format!("acord-decmod-file-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let file = dir.join("mathy.cord"); + std::fs::write(&file, "fn double(x) {\n return x * 2\n}").unwrap(); + + let out = decompose_module(&file, &NoHook).unwrap().code; + assert!(out.contains("pub fn double(mut x: V) -> V")); + + std::fs::remove_dir_all(&dir).ok(); + } + + #[test] + fn decompose_module_discovers_mod_cord_and_concats() { + let dir = std::env::temp_dir().join(format!("acord-decmod-dir-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + + let with_mod = dir.join("withmod"); + std::fs::create_dir_all(&with_mod).unwrap(); + std::fs::write(with_mod.join("mod.cord"), "fn helper() {\n return 1\n}").unwrap(); + let out = decompose_module(&with_mod, &NoHook).unwrap().code; + assert!(out.contains("pub fn helper(")); + + let concat = dir.join("concat"); + std::fs::create_dir_all(&concat).unwrap(); + std::fs::write(concat.join("a.cord"), "fn alpha() {\n return 1\n}").unwrap(); + std::fs::write(concat.join("b.cord"), "fn beta() {\n return 2\n}").unwrap(); + let out = decompose_module(&concat, &NoHook).unwrap().code; + assert!(out.contains("pub fn alpha(")); + assert!(out.contains("pub fn beta(")); + + std::fs::remove_dir_all(&dir).ok(); + } + #[test] fn impl_method_emits_free_fn_and_dispatch() { let out = dec("impl Vec2 {\n fn new(x, y) {\n return {__type: \"Vec2\", x: x, y: y}\n }\n fn add(self, other) {\n return Vec2::new(self.x + other.x, self.y + other.y)\n }\n}"); diff --git a/core/src/interp/mod.rs b/core/src/interp/mod.rs index 737f44c..d62eab6 100644 --- a/core/src/interp/mod.rs +++ b/core/src/interp/mod.rs @@ -24,7 +24,7 @@ pub use builtins::seed_rng; pub use formulas::{parse_formula, parse_formula_with_spice, FormulaRef, ParsedFormula}; pub use hooks::{DisplayFormat, InterpreterHook}; pub use module_paths::ModuleRegistry; -pub use modules::{extract_use_declarations, ModuleExports, UseDecl}; +pub use modules::{extract_use_declarations, read_module_source, ModuleExports, UseDecl}; pub use parse::{parse_program, Parser}; pub use ring::RingBuf; pub use tables::{display_addr, parse_cell_address}; diff --git a/core/src/interp/modules.rs b/core/src/interp/modules.rs index df64670..3e6edb3 100644 --- a/core/src/interp/modules.rs +++ b/core/src/interp/modules.rs @@ -1,5 +1,5 @@ use std::collections::HashMap; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use crate::interp::ast::Stmt; use crate::interp::parse::Parser; @@ -147,7 +147,7 @@ impl Interpreter { } /// reads module source from a resolved registry path. -fn read_module_source(path: &PathBuf) -> std::io::Result { +pub fn read_module_source(path: &Path) -> std::io::Result { // direct .cord file if path.is_file() { return std::fs::read_to_string(path);