- 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.
This commit is contained in:
jess 2026-05-30 18:52:43 -07:00
parent c18ee2c226
commit d30420324a
3 changed files with 141 additions and 12 deletions

View File

@ -1,6 +1,9 @@
//! cordial source decomposer -- produces self-contained Rust from Cordial. //! 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. /// extension point for external projects adding custom decomposition rules.
pub trait DecomposeHook { pub trait DecomposeHook {
@ -15,6 +18,9 @@ pub trait DecomposeHook {
/// intercepts a statement before the default decomposition. /// intercepts a statement before the default decomposition.
fn stmt(&self, _stmt: &Stmt) -> Option<String> { None } fn stmt(&self, _stmt: &Stmt) -> Option<String> { None }
/// replaces named prelude runtime functions with full custom definitions.
fn prelude_overrides(&self) -> Vec<(&'static str, String)> { Vec::new() }
} }
struct NoHook; struct NoHook;
@ -52,19 +58,54 @@ pub fn decompose_with(source: &str, hook: &dyn DecomposeHook) -> Result<Decompos
decompose_stmts(&stmts, hook) decompose_stmts(&stmts, hook)
} }
/// decomposes a module resolved on disk, applying registry file discovery.
pub fn decompose_module(path: &Path, hook: &dyn DecomposeHook) -> Result<Decomposed, String> {
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. /// decomposes a pre-parsed statement list.
pub fn decompose_stmts(stmts: &[Stmt], hook: &dyn DecomposeHook) -> Result<Decomposed, String> { pub fn decompose_stmts(stmts: &[Stmt], hook: &dyn DecomposeHook) -> Result<Decomposed, String> {
let mut deps = Vec::new(); let mut deps = Vec::new();
let mut out = String::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() { if let Some(extra) = hook.preamble() {
out.push_str(&extra); out.push_str(&extra);
out.push('\n'); out.push('\n');
} }
out.push_str("\n// --- generated code below ---\n\n");
emit_program(&mut out, stmts, &mut deps, hook)?; emit_program(&mut out, stmts, &mut deps, hook)?;
Ok(Decomposed { code: out, deps }) 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. /// dispatch-table entry: type, method, fn name, arity.
struct MethodReg { struct MethodReg {
ty: String, ty: String,
@ -449,7 +490,7 @@ const RESERVED: &[&str] = &[
"async", "await", "dyn", "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; use std::collections::BTreeMap;
#[derive(Clone, Debug, PartialEq)] #[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_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<String> { fn v_struct_type(v: &V) -> Option<String> {
@ -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) { fn v_assign_path(root: &mut V, steps: &[Step], val: V) {
let mut cur = root; let mut cur = root;
for (i, step) in steps.iter().enumerate() { for (i, step) in steps.iter().enumerate() {
@ -615,9 +657,6 @@ fn v_method_call(recv: &V, method: &str, args: &[V]) -> V {
} }
V::Void V::Void
} }
// --- generated code below ---
"#; "#;
#[cfg(test)] #[cfg(test)]
@ -747,6 +786,96 @@ mod tests {
assert_eq!(r.deps[0].segments, vec!["sitter", "scene", "primitives"]); assert_eq!(r.deps[0].segments, vec!["sitter", "scene", "primitives"]);
} }
struct FieldOverride;
impl DecomposeHook for FieldOverride {
fn preamble(&self) -> Option<String> {
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] #[test]
fn impl_method_emits_free_fn_and_dispatch() { 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}"); 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}");

View File

@ -24,7 +24,7 @@ pub use builtins::seed_rng;
pub use formulas::{parse_formula, parse_formula_with_spice, FormulaRef, ParsedFormula}; pub use formulas::{parse_formula, parse_formula_with_spice, FormulaRef, ParsedFormula};
pub use hooks::{DisplayFormat, InterpreterHook}; pub use hooks::{DisplayFormat, InterpreterHook};
pub use module_paths::ModuleRegistry; 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 parse::{parse_program, Parser};
pub use ring::RingBuf; pub use ring::RingBuf;
pub use tables::{display_addr, parse_cell_address}; pub use tables::{display_addr, parse_cell_address};

View File

@ -1,5 +1,5 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::path::PathBuf; use std::path::{Path, PathBuf};
use crate::interp::ast::Stmt; use crate::interp::ast::Stmt;
use crate::interp::parse::Parser; use crate::interp::parse::Parser;
@ -147,7 +147,7 @@ impl Interpreter {
} }
/// reads module source from a resolved registry path. /// reads module source from a resolved registry path.
fn read_module_source(path: &PathBuf) -> std::io::Result<String> { pub fn read_module_source(path: &Path) -> std::io::Result<String> {
// direct .cord file // direct .cord file
if path.is_file() { if path.is_file() {
return std::fs::read_to_string(path); return std::fs::read_to_string(path);