- 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:
parent
c18ee2c226
commit
d30420324a
|
|
@ -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<String> { 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<Decompos
|
|||
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.
|
||||
pub fn decompose_stmts(stmts: &[Stmt], hook: &dyn DecomposeHook) -> Result<Decomposed, String> {
|
||||
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<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) {
|
||||
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<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]
|
||||
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}");
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -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<String> {
|
||||
pub fn read_module_source(path: &Path) -> std::io::Result<String> {
|
||||
// direct .cord file
|
||||
if path.is_file() {
|
||||
return std::fs::read_to_string(path);
|
||||
|
|
|
|||
Loading…
Reference in New Issue