- 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.
|
//! 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}");
|
||||||
|
|
|
||||||
|
|
@ -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};
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue